@mathscan/math-exercise-engine 1.1.15 → 1.1.17
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 +940 -248
- package/dist/index.js +762 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +763 -46
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +278 -11
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createToken, Lexer, CstParser } from "chevrotain";
|
|
2
1
|
import { z } from "zod";
|
|
2
|
+
import { createToken, Lexer, CstParser } from "chevrotain";
|
|
3
3
|
import { create, all } from "mathjs";
|
|
4
4
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
5
|
-
import React, { useLayoutEffect, useEffect, useRef, useMemo, useCallback, useState, createContext, memo, useReducer, useContext, forwardRef, cloneElement, useImperativeHandle } from "react";
|
|
5
|
+
import React, { useLayoutEffect, useEffect, useRef, useMemo, useCallback, useState, createContext, memo, useReducer, useContext, forwardRef, cloneElement, useId, useImperativeHandle } from "react";
|
|
6
6
|
import { unstable_batchedUpdates, createPortal } from "react-dom";
|
|
7
7
|
import { createRoot } from "react-dom/client";
|
|
8
8
|
const AST_TYPES = {
|
|
@@ -54,6 +54,8 @@ const AST_TYPES = {
|
|
|
54
54
|
VOP_CARRY_ROW: "VopCarryRow",
|
|
55
55
|
/** Number line layout block */
|
|
56
56
|
NUMBER_LINE_BLOCK: "NumberLineBlock",
|
|
57
|
+
/** Symmetry exercise on a square grid */
|
|
58
|
+
GRID_SYMMETRY_BLOCK: "GridSymmetryBlock",
|
|
57
59
|
/** A position marker on the number line */
|
|
58
60
|
NUMBER_LINE_POSITION: "NumberLinePosition",
|
|
59
61
|
/** A draggable cursor on the number line */
|
|
@@ -185,6 +187,8 @@ const JSON_TYPES = {
|
|
|
185
187
|
FRACTION: "fraction",
|
|
186
188
|
/** Visual fraction widget */
|
|
187
189
|
VISUAL_FRACTION: "visual-fraction",
|
|
190
|
+
/** Interactive symmetry exercise on a grid */
|
|
191
|
+
GRID_SYMMETRY: "grid-symmetry",
|
|
188
192
|
/** Ruler geometry tool widget */
|
|
189
193
|
RULER: "ruler",
|
|
190
194
|
/** Protractor geometry tool widget */
|
|
@@ -199,9 +203,11 @@ const SET_SQUARE_WIDGET_ID = "_set_square";
|
|
|
199
203
|
const STATEFUL_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
200
204
|
JSON_TYPES.MATCHER,
|
|
201
205
|
JSON_TYPES.PAIR_MATCHER,
|
|
202
|
-
JSON_TYPES.VISUAL_FRACTION
|
|
206
|
+
JSON_TYPES.VISUAL_FRACTION,
|
|
207
|
+
JSON_TYPES.GRID_SYMMETRY
|
|
203
208
|
]);
|
|
204
209
|
const INLINE_DRAG_OPT_PREFIX = "_inline_drag_opt_";
|
|
210
|
+
const GRID_SYMMETRY_WIDGET_PREFIX = "_grid_symmetry_";
|
|
205
211
|
const RTL_INLINE_WIDGET_TYPES = /* @__PURE__ */ new Set([
|
|
206
212
|
JSON_TYPES.INLINE_INPUT,
|
|
207
213
|
JSON_TYPES.INLINE_DRAG_OPTION,
|
|
@@ -229,6 +235,9 @@ const SIDE_PAD = 7;
|
|
|
229
235
|
const CSS_VAR_SIDE_PAD = "--nl-side-pad";
|
|
230
236
|
const NUMBER_LINE_LABELS = ["all", "extremes", "min-only", "max-only", "none"];
|
|
231
237
|
const NUMBER_LINE_HIGHLIGHT_COLORS = ["blue", "red", "green", "orange", "purple", "yellow"];
|
|
238
|
+
const HEX_COLOR_REGEX = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
|
|
239
|
+
const isNumberLineHighlightColor = (value) => NUMBER_LINE_HIGHLIGHT_COLORS.includes(value) || HEX_COLOR_REGEX.test(value);
|
|
240
|
+
const highlightColorMessage = (block) => `${block}: color must be one of ${NUMBER_LINE_HIGHLIGHT_COLORS.join(", ")} or a hex value (e.g. "#3B82F6")`;
|
|
232
241
|
const NUMBER_LINE_DRAG_MODES = ["axis", "inputs"];
|
|
233
242
|
function getFractionKey(widgetId, part) {
|
|
234
243
|
return `${widgetId}__${part}`;
|
|
@@ -241,6 +250,11 @@ function parseFractionKey(key2) {
|
|
|
241
250
|
if (!match) return null;
|
|
242
251
|
return { widgetId: match[1], part: match[2] };
|
|
243
252
|
}
|
|
253
|
+
const GridCellSchema = z.tuple([
|
|
254
|
+
z.number().int().positive(),
|
|
255
|
+
z.number().int().positive()
|
|
256
|
+
]);
|
|
257
|
+
const gridCellKey = ([row, col]) => `${row},${col}`;
|
|
244
258
|
const Newline = createToken({
|
|
245
259
|
name: "Newline",
|
|
246
260
|
pattern: /\r?\n/,
|
|
@@ -322,6 +336,10 @@ const NUMBER_LINE = createToken({
|
|
|
322
336
|
name: "NUMBER_LINE",
|
|
323
337
|
pattern: /NUMBER_LINE/
|
|
324
338
|
});
|
|
339
|
+
const GRID_SYMMETRY = createToken({
|
|
340
|
+
name: "GRID_SYMMETRY",
|
|
341
|
+
pattern: /GRID_SYMMETRY/
|
|
342
|
+
});
|
|
325
343
|
const POSITION = createToken({
|
|
326
344
|
name: "POSITION",
|
|
327
345
|
pattern: /POSITION/
|
|
@@ -530,6 +548,7 @@ const allTokens = [
|
|
|
530
548
|
W_N_I,
|
|
531
549
|
NUMERIC_INPUT,
|
|
532
550
|
NUMBER_LINE,
|
|
551
|
+
GRID_SYMMETRY,
|
|
533
552
|
POSITION,
|
|
534
553
|
CURSOR,
|
|
535
554
|
ARROW,
|
|
@@ -705,6 +724,10 @@ class MathExerciseParser extends CstParser {
|
|
|
705
724
|
GATE: this.BACKTRACK(this.numberLine),
|
|
706
725
|
ALT: () => this.SUBRULE(this.numberLine)
|
|
707
726
|
},
|
|
727
|
+
{
|
|
728
|
+
GATE: this.BACKTRACK(this.gridSymmetry),
|
|
729
|
+
ALT: () => this.SUBRULE(this.gridSymmetry)
|
|
730
|
+
},
|
|
708
731
|
{
|
|
709
732
|
GATE: this.BACKTRACK(this.pairMatcher),
|
|
710
733
|
ALT: () => this.SUBRULE(this.pairMatcher)
|
|
@@ -720,6 +743,18 @@ class MathExerciseParser extends CstParser {
|
|
|
720
743
|
{ ALT: () => this.SUBRULE(this.line) }
|
|
721
744
|
]);
|
|
722
745
|
});
|
|
746
|
+
this.gridSymmetry = this.RULE("gridSymmetry", () => {
|
|
747
|
+
this.MANY(() => {
|
|
748
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
749
|
+
});
|
|
750
|
+
this.CONSUME(GRID_SYMMETRY);
|
|
751
|
+
this.CONSUME(LBracket);
|
|
752
|
+
this.SUBRULE(this.parameterList);
|
|
753
|
+
this.CONSUME(RBracket);
|
|
754
|
+
this.MANY2(() => {
|
|
755
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
723
758
|
this.table = this.RULE("table", () => {
|
|
724
759
|
this.MANY(() => {
|
|
725
760
|
this.OR2([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
@@ -1433,27 +1468,51 @@ class MathExerciseParser extends CstParser {
|
|
|
1433
1468
|
});
|
|
1434
1469
|
this.parameterList = this.RULE("parameterList", () => {
|
|
1435
1470
|
this.MANY(() => {
|
|
1436
|
-
this.
|
|
1471
|
+
this.OR([
|
|
1472
|
+
{ ALT: () => this.CONSUME(Space) },
|
|
1473
|
+
{ ALT: () => this.CONSUME(Newline) }
|
|
1474
|
+
]);
|
|
1437
1475
|
});
|
|
1438
1476
|
this.SUBRULE(this.parameter);
|
|
1439
1477
|
this.MANY2(() => {
|
|
1440
1478
|
this.MANY3(() => {
|
|
1441
|
-
this.
|
|
1479
|
+
this.OR2([
|
|
1480
|
+
{ ALT: () => this.CONSUME2(Space) },
|
|
1481
|
+
{ ALT: () => this.CONSUME2(Newline) }
|
|
1482
|
+
]);
|
|
1442
1483
|
});
|
|
1443
1484
|
this.CONSUME(Comma);
|
|
1444
1485
|
this.MANY4(() => {
|
|
1445
|
-
this.
|
|
1486
|
+
this.OR3([
|
|
1487
|
+
{ ALT: () => this.CONSUME3(Space) },
|
|
1488
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
1489
|
+
]);
|
|
1446
1490
|
});
|
|
1447
1491
|
this.SUBRULE2(this.parameter);
|
|
1448
1492
|
});
|
|
1449
1493
|
this.MANY5(() => {
|
|
1450
|
-
this.
|
|
1494
|
+
this.OR4([
|
|
1495
|
+
{ ALT: () => this.CONSUME4(Space) },
|
|
1496
|
+
{ ALT: () => this.CONSUME4(Newline) }
|
|
1497
|
+
]);
|
|
1451
1498
|
});
|
|
1452
1499
|
});
|
|
1453
1500
|
this.parameter = this.RULE("parameter", () => {
|
|
1454
1501
|
this.CONSUME(Identifier, { LABEL: "key" });
|
|
1502
|
+
this.MANY(() => {
|
|
1503
|
+
this.OR([
|
|
1504
|
+
{ ALT: () => this.CONSUME(Space) },
|
|
1505
|
+
{ ALT: () => this.CONSUME(Newline) }
|
|
1506
|
+
]);
|
|
1507
|
+
});
|
|
1455
1508
|
this.CONSUME(Equals);
|
|
1456
|
-
this.
|
|
1509
|
+
this.MANY2(() => {
|
|
1510
|
+
this.OR2([
|
|
1511
|
+
{ ALT: () => this.CONSUME2(Space) },
|
|
1512
|
+
{ ALT: () => this.CONSUME2(Newline) }
|
|
1513
|
+
]);
|
|
1514
|
+
});
|
|
1515
|
+
this.OR3([
|
|
1457
1516
|
// String value
|
|
1458
1517
|
{ ALT: () => this.CONSUME(StringLiteral, { LABEL: "value" }) },
|
|
1459
1518
|
// Number value
|
|
@@ -1462,6 +1521,11 @@ class MathExerciseParser extends CstParser {
|
|
|
1462
1521
|
{ ALT: () => this.CONSUME2(Identifier, { LABEL: "value" }) },
|
|
1463
1522
|
// Tuple value: (start, end)
|
|
1464
1523
|
{ ALT: () => this.SUBRULE(this.tuple, { LABEL: "value" }) },
|
|
1524
|
+
// Number-pair list enclosed in braces: {[1, 2], [3, 4]}
|
|
1525
|
+
{
|
|
1526
|
+
GATE: () => this.isBracedNumberPairListAhead(),
|
|
1527
|
+
ALT: () => this.SUBRULE(this.bracedNumberPairList, { LABEL: "value" })
|
|
1528
|
+
},
|
|
1465
1529
|
// Curly-brace string list: {"a", "b", "c"}
|
|
1466
1530
|
{ ALT: () => this.SUBRULE(this.curlyStringList, { LABEL: "value" }) },
|
|
1467
1531
|
// Square-bracket number array: [1, 2, 3] — used by VISUAL_FRACTION filled/answer
|
|
@@ -1517,8 +1581,52 @@ class MathExerciseParser extends CstParser {
|
|
|
1517
1581
|
this.SUBRULE(this.stringList);
|
|
1518
1582
|
this.CONSUME(RBrace);
|
|
1519
1583
|
});
|
|
1584
|
+
this.bracedNumberPairList = this.RULE("bracedNumberPairList", () => {
|
|
1585
|
+
this.CONSUME(LBrace);
|
|
1586
|
+
this.MANY(() => {
|
|
1587
|
+
this.OR([{ ALT: () => this.CONSUME(Space) }, { ALT: () => this.CONSUME(Newline) }]);
|
|
1588
|
+
});
|
|
1589
|
+
this.OPTION(() => {
|
|
1590
|
+
this.SUBRULE(this.numberPair);
|
|
1591
|
+
this.MANY2(() => {
|
|
1592
|
+
this.MANY3(() => {
|
|
1593
|
+
this.OR2([{ ALT: () => this.CONSUME2(Space) }, { ALT: () => this.CONSUME2(Newline) }]);
|
|
1594
|
+
});
|
|
1595
|
+
this.CONSUME(Comma);
|
|
1596
|
+
this.MANY4(() => {
|
|
1597
|
+
this.OR3([{ ALT: () => this.CONSUME3(Space) }, { ALT: () => this.CONSUME3(Newline) }]);
|
|
1598
|
+
});
|
|
1599
|
+
this.SUBRULE2(this.numberPair);
|
|
1600
|
+
});
|
|
1601
|
+
});
|
|
1602
|
+
this.MANY5(() => {
|
|
1603
|
+
this.OR4([{ ALT: () => this.CONSUME4(Space) }, { ALT: () => this.CONSUME4(Newline) }]);
|
|
1604
|
+
});
|
|
1605
|
+
this.CONSUME(RBrace);
|
|
1606
|
+
});
|
|
1607
|
+
this.numberPair = this.RULE("numberPair", () => {
|
|
1608
|
+
this.CONSUME(LBracket);
|
|
1609
|
+
this.MANY(() => this.CONSUME(Space));
|
|
1610
|
+
this.CONSUME(NumberLiteral, { LABEL: "row" });
|
|
1611
|
+
this.MANY2(() => this.CONSUME2(Space));
|
|
1612
|
+
this.CONSUME(Comma);
|
|
1613
|
+
this.MANY3(() => this.CONSUME3(Space));
|
|
1614
|
+
this.CONSUME2(NumberLiteral, { LABEL: "col" });
|
|
1615
|
+
this.MANY4(() => this.CONSUME4(Space));
|
|
1616
|
+
this.CONSUME(RBracket);
|
|
1617
|
+
});
|
|
1520
1618
|
this.performSelfAnalysis();
|
|
1521
1619
|
}
|
|
1620
|
+
/** Distinguish `{[number, number], ...}` and the valid empty form `{}` from string lists. */
|
|
1621
|
+
isBracedNumberPairListAhead() {
|
|
1622
|
+
if (this.LA(1).tokenType !== LBrace) return false;
|
|
1623
|
+
let offset = 2;
|
|
1624
|
+
while (this.LA(offset).tokenType === Space || this.LA(offset).tokenType === Newline) {
|
|
1625
|
+
offset++;
|
|
1626
|
+
}
|
|
1627
|
+
const firstContentToken = this.LA(offset).tokenType;
|
|
1628
|
+
return firstContentToken === LBracket || firstContentToken === RBrace;
|
|
1629
|
+
}
|
|
1522
1630
|
}
|
|
1523
1631
|
const parser = new MathExerciseParser();
|
|
1524
1632
|
function parseOrThrow(schema, data, label) {
|
|
@@ -1599,7 +1707,9 @@ const ImageInlineSchema = z.object({
|
|
|
1599
1707
|
}),
|
|
1600
1708
|
alt: z.string(),
|
|
1601
1709
|
width: z.number().positive().optional(),
|
|
1602
|
-
height: z.number().positive().optional()
|
|
1710
|
+
height: z.number().positive().optional(),
|
|
1711
|
+
/** Opt-in: show an expand control that opens the image fullscreen. */
|
|
1712
|
+
fullscreen: z.boolean().optional()
|
|
1603
1713
|
});
|
|
1604
1714
|
const VisualFractionInlineSchema = z.object({
|
|
1605
1715
|
type: z.literal(AST_TYPES.VISUAL_FRACTION),
|
|
@@ -1904,10 +2014,8 @@ const NumberLineHighlightSchema = z.object({
|
|
|
1904
2014
|
type: z.literal(AST_TYPES.NUMBER_LINE_HIGHLIGHT),
|
|
1905
2015
|
from: z.number().finite({ message: "HIGHLIGHT: from must be a finite number" }),
|
|
1906
2016
|
to: z.number().finite({ message: "HIGHLIGHT: to must be a finite number" }),
|
|
1907
|
-
color: z.
|
|
1908
|
-
|
|
1909
|
-
message: `HIGHLIGHT: color must be one of ${NUMBER_LINE_HIGHLIGHT_COLORS.join(", ")}`
|
|
1910
|
-
})
|
|
2017
|
+
color: z.string().refine(isNumberLineHighlightColor, {
|
|
2018
|
+
message: highlightColorMessage("HIGHLIGHT")
|
|
1911
2019
|
}).optional(),
|
|
1912
2020
|
label: z.string().optional()
|
|
1913
2021
|
}).refine((d) => d.from < d.to, {
|
|
@@ -1926,7 +2034,7 @@ const NumberLineCursorSchema = z.object({
|
|
|
1926
2034
|
type: z.literal(AST_TYPES.NUMBER_LINE_CURSOR),
|
|
1927
2035
|
value: z.number().finite({ message: "CURSOR: value must be a finite number" }).default(0),
|
|
1928
2036
|
answer: z.number({ required_error: "CURSOR: answer is required" }).finite({ message: "CURSOR: answer must be a finite number" }),
|
|
1929
|
-
color: z.string().regex(
|
|
2037
|
+
color: z.string().regex(HEX_COLOR_REGEX, {
|
|
1930
2038
|
message: 'CURSOR: color must be a hex value (e.g. "#3B82F6" or "#F00")'
|
|
1931
2039
|
}).optional(),
|
|
1932
2040
|
fillRange: z.boolean().default(false),
|
|
@@ -1948,8 +2056,8 @@ const NumberLineArrowSchema = z.object({
|
|
|
1948
2056
|
to: z.number().finite({ message: "ARROW: to must be a finite number" }),
|
|
1949
2057
|
color: z.string().optional(),
|
|
1950
2058
|
inlines: z.array(InlineNodeSchema)
|
|
1951
|
-
}).refine((d) => d.from
|
|
1952
|
-
message: "ARROW: from must be
|
|
2059
|
+
}).refine((d) => d.from !== d.to, {
|
|
2060
|
+
message: "ARROW: from and to must be different"
|
|
1953
2061
|
});
|
|
1954
2062
|
function createNumberLineArrow(from, to, inlines, color) {
|
|
1955
2063
|
return NumberLineArrowSchema.parse({
|
|
@@ -2014,7 +2122,9 @@ const NumberLineBlockSchema = z.object({
|
|
|
2014
2122
|
}
|
|
2015
2123
|
}
|
|
2016
2124
|
for (const arrow of data.arrows ?? []) {
|
|
2017
|
-
|
|
2125
|
+
const lo = Math.min(arrow.from, arrow.to);
|
|
2126
|
+
const hi = Math.max(arrow.from, arrow.to);
|
|
2127
|
+
if (lo < data.min || hi > data.max) {
|
|
2018
2128
|
ctx.addIssue({
|
|
2019
2129
|
code: z.ZodIssueCode.custom,
|
|
2020
2130
|
message: `NUMBER_LINE: ARROW range [${arrow.from}, ${arrow.to}] must be within [${data.min}, ${data.max}]`
|
|
@@ -2061,6 +2171,135 @@ function createNumberLineBlock(min, max, positions, step, minorTicks, style, lab
|
|
|
2061
2171
|
positions
|
|
2062
2172
|
});
|
|
2063
2173
|
}
|
|
2174
|
+
const GRID_SYMMETRY_AXES = ["vertical", "horizontal"];
|
|
2175
|
+
const GRID_SYMMETRY_INTERACTIVE_AREAS = ["left", "right", "top", "bottom"];
|
|
2176
|
+
const GRID_SYMMETRY_MAX_ROWS = 20;
|
|
2177
|
+
const GRID_SYMMETRY_MAX_COLS = 20;
|
|
2178
|
+
const GRID_SYMMETRY_MIN_CELL_SIZE = 0.25;
|
|
2179
|
+
const GRID_SYMMETRY_MAX_CELL_SIZE = 2;
|
|
2180
|
+
function isCellInInteractiveArea(cell, rows, cols, area) {
|
|
2181
|
+
const [row, col] = cell;
|
|
2182
|
+
switch (area) {
|
|
2183
|
+
case "left":
|
|
2184
|
+
return col <= Math.floor(cols / 2);
|
|
2185
|
+
case "right":
|
|
2186
|
+
return col > Math.ceil(cols / 2);
|
|
2187
|
+
case "top":
|
|
2188
|
+
return row <= Math.floor(rows / 2);
|
|
2189
|
+
case "bottom":
|
|
2190
|
+
return row > Math.ceil(rows / 2);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
function reflectGridSymmetryCell(cell, rows, cols, axis) {
|
|
2194
|
+
const [row, col] = cell;
|
|
2195
|
+
return axis === "vertical" ? [row, cols + 1 - col] : [rows + 1 - row, col];
|
|
2196
|
+
}
|
|
2197
|
+
function computeGridSymmetryExpectedCells(data) {
|
|
2198
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2199
|
+
const expected = [];
|
|
2200
|
+
for (const cell of data.cells) {
|
|
2201
|
+
if (isCellInInteractiveArea(cell, data.rows, data.cols, data.interactiveArea)) continue;
|
|
2202
|
+
const reflected = reflectGridSymmetryCell(cell, data.rows, data.cols, data.axis);
|
|
2203
|
+
if (!isCellInInteractiveArea(reflected, data.rows, data.cols, data.interactiveArea)) continue;
|
|
2204
|
+
const key2 = gridCellKey(reflected);
|
|
2205
|
+
if (!seen.has(key2)) {
|
|
2206
|
+
seen.add(key2);
|
|
2207
|
+
expected.push(reflected);
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
return expected.sort(([ar, ac], [br, bc]) => ar - br || ac - bc);
|
|
2211
|
+
}
|
|
2212
|
+
function computeGridSymmetryCellsToFill(data) {
|
|
2213
|
+
const initial = new Set(data.cells.map(gridCellKey));
|
|
2214
|
+
return computeGridSymmetryExpectedCells(data).filter((cell) => !initial.has(gridCellKey(cell)));
|
|
2215
|
+
}
|
|
2216
|
+
const GridSymmetryBlockSchema = z.object({
|
|
2217
|
+
type: z.literal(AST_TYPES.GRID_SYMMETRY_BLOCK),
|
|
2218
|
+
rows: z.number().int("The number of rows must be a whole number.").positive("The number of rows must be greater than 0.").max(GRID_SYMMETRY_MAX_ROWS, {
|
|
2219
|
+
message: `The number of rows cannot exceed ${GRID_SYMMETRY_MAX_ROWS}.`
|
|
2220
|
+
}),
|
|
2221
|
+
cols: z.number().int("The number of columns must be a whole number.").positive("The number of columns must be greater than 0.").max(GRID_SYMMETRY_MAX_COLS, {
|
|
2222
|
+
message: `The number of columns cannot exceed ${GRID_SYMMETRY_MAX_COLS}.`
|
|
2223
|
+
}),
|
|
2224
|
+
axis: z.enum(GRID_SYMMETRY_AXES),
|
|
2225
|
+
interactiveArea: z.enum(GRID_SYMMETRY_INTERACTIVE_AREAS),
|
|
2226
|
+
cellSize: z.number().finite().min(
|
|
2227
|
+
GRID_SYMMETRY_MIN_CELL_SIZE,
|
|
2228
|
+
`Cell size must be between ${GRID_SYMMETRY_MIN_CELL_SIZE} and ${GRID_SYMMETRY_MAX_CELL_SIZE}.`
|
|
2229
|
+
).max(
|
|
2230
|
+
GRID_SYMMETRY_MAX_CELL_SIZE,
|
|
2231
|
+
`Cell size must be between ${GRID_SYMMETRY_MIN_CELL_SIZE} and ${GRID_SYMMETRY_MAX_CELL_SIZE}.`
|
|
2232
|
+
).default(1),
|
|
2233
|
+
cells: z.array(GridCellSchema)
|
|
2234
|
+
}).superRefine((data, ctx) => {
|
|
2235
|
+
const compatible = data.axis === "vertical" ? data.interactiveArea === "left" || data.interactiveArea === "right" : data.interactiveArea === "top" || data.interactiveArea === "bottom";
|
|
2236
|
+
if (!compatible) {
|
|
2237
|
+
ctx.addIssue({
|
|
2238
|
+
code: z.ZodIssueCode.custom,
|
|
2239
|
+
path: ["interactiveArea"],
|
|
2240
|
+
message: data.axis === "vertical" ? 'For a vertical axis, the interactive area must be "left" or "right".' : 'For a horizontal axis, the interactive area must be "top" or "bottom".'
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
if (data.axis === "vertical" && data.cols % 2 !== 0) {
|
|
2244
|
+
ctx.addIssue({
|
|
2245
|
+
code: z.ZodIssueCode.custom,
|
|
2246
|
+
path: ["cols"],
|
|
2247
|
+
message: `For a vertical axis, the number of columns must be even. Received: ${data.cols}.`
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
if (data.axis === "horizontal" && data.rows % 2 !== 0) {
|
|
2251
|
+
ctx.addIssue({
|
|
2252
|
+
code: z.ZodIssueCode.custom,
|
|
2253
|
+
path: ["rows"],
|
|
2254
|
+
message: `For a horizontal axis, the number of rows must be even. Received: ${data.rows}.`
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2258
|
+
data.cells.forEach((cell, index) => {
|
|
2259
|
+
const [row, col] = cell;
|
|
2260
|
+
if (row > data.rows || col > data.cols) {
|
|
2261
|
+
ctx.addIssue({
|
|
2262
|
+
code: z.ZodIssueCode.custom,
|
|
2263
|
+
path: ["cells", index],
|
|
2264
|
+
message: `Cell [${row},${col}] is outside the ${data.rows} × ${data.cols} grid.`
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
const key2 = gridCellKey(cell);
|
|
2268
|
+
if (seen.has(key2)) {
|
|
2269
|
+
ctx.addIssue({
|
|
2270
|
+
code: z.ZodIssueCode.custom,
|
|
2271
|
+
path: ["cells", index],
|
|
2272
|
+
message: `Cell [${row},${col}] is listed more than once.`
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
seen.add(key2);
|
|
2276
|
+
});
|
|
2277
|
+
if (!compatible) return;
|
|
2278
|
+
const expected = new Set(computeGridSymmetryExpectedCells(data).map(gridCellKey));
|
|
2279
|
+
data.cells.forEach((cell, index) => {
|
|
2280
|
+
if (isCellInInteractiveArea(cell, data.rows, data.cols, data.interactiveArea) && !expected.has(gridCellKey(cell))) {
|
|
2281
|
+
ctx.addIssue({
|
|
2282
|
+
code: z.ZodIssueCode.custom,
|
|
2283
|
+
path: ["cells", index],
|
|
2284
|
+
message: `Prefilled cell [${cell[0]},${cell[1]}] does not match the expected symmetry.`
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
});
|
|
2288
|
+
});
|
|
2289
|
+
function createGridSymmetryBlock(rows, cols, axis, interactiveArea, cells, cellSize = 1) {
|
|
2290
|
+
return GridSymmetryBlockSchema.parse({
|
|
2291
|
+
type: AST_TYPES.GRID_SYMMETRY_BLOCK,
|
|
2292
|
+
rows,
|
|
2293
|
+
cols,
|
|
2294
|
+
axis,
|
|
2295
|
+
interactiveArea,
|
|
2296
|
+
cellSize,
|
|
2297
|
+
cells
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
function isGridSymmetryBlock(value) {
|
|
2301
|
+
return GridSymmetryBlockSchema.safeParse(value).success;
|
|
2302
|
+
}
|
|
2064
2303
|
const TableCellSchema = z.object({
|
|
2065
2304
|
type: z.literal(AST_TYPES.TABLE_CELL),
|
|
2066
2305
|
colspan: z.number().int().positive().optional(),
|
|
@@ -2560,7 +2799,7 @@ function isMixedValidation(value) {
|
|
|
2560
2799
|
function isValidationNode(value) {
|
|
2561
2800
|
return ValidationNodeSchema.safeParse(value).success;
|
|
2562
2801
|
}
|
|
2563
|
-
const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema, VerticalOperationBlockSchema]);
|
|
2802
|
+
const ContentBlockSchema = z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema, MatcherBlockSchema, PairMatcherBlockSchema, VerticalOperationBlockSchema, GridSymmetryBlockSchema]);
|
|
2564
2803
|
const ExerciseDocumentSchema = z.object({
|
|
2565
2804
|
type: z.literal(AST_TYPES.EXERCISE_DOCUMENT),
|
|
2566
2805
|
question: TextBlockSchema.optional(),
|
|
@@ -2662,12 +2901,24 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
2662
2901
|
if (ctx.table) return this.visit(ctx.table);
|
|
2663
2902
|
if (ctx.radioButtons) return this.visit(ctx.radioButtons);
|
|
2664
2903
|
if (ctx.numberLine) return this.visit(ctx.numberLine);
|
|
2904
|
+
if (ctx.gridSymmetry) return this.visit(ctx.gridSymmetry);
|
|
2665
2905
|
if (ctx.pairMatcher) return this.visit(ctx.pairMatcher);
|
|
2666
2906
|
if (ctx.matcher) return this.visit(ctx.matcher);
|
|
2667
2907
|
if (ctx.verticalOperation) return this.visit(ctx.verticalOperation);
|
|
2668
2908
|
if (ctx.line) return this.visit(ctx.line);
|
|
2669
2909
|
throw new Error("Unknown content block");
|
|
2670
2910
|
}
|
|
2911
|
+
gridSymmetry(ctx) {
|
|
2912
|
+
const params = this.visit(ctx.parameterList);
|
|
2913
|
+
return createGridSymmetryBlock(
|
|
2914
|
+
Number(params.rows),
|
|
2915
|
+
Number(params.cols),
|
|
2916
|
+
params.axis,
|
|
2917
|
+
params.interactiveArea,
|
|
2918
|
+
params.cells,
|
|
2919
|
+
params.cellSize === void 0 ? 1 : Number(params.cellSize)
|
|
2920
|
+
);
|
|
2921
|
+
}
|
|
2671
2922
|
radioButtons(ctx) {
|
|
2672
2923
|
const params = this.visit(ctx.parameterList);
|
|
2673
2924
|
const name = params.name;
|
|
@@ -3106,7 +3357,8 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
3106
3357
|
const alt = params.alt;
|
|
3107
3358
|
const width = params.width;
|
|
3108
3359
|
const height = params.height;
|
|
3109
|
-
|
|
3360
|
+
const fullscreen = params.fullscreen !== void 0 ? params.fullscreen === true || params.fullscreen === "true" : void 0;
|
|
3361
|
+
return createImage(name, src, alt, { width, height, fullscreen });
|
|
3110
3362
|
}
|
|
3111
3363
|
visualFraction(ctx) {
|
|
3112
3364
|
const params = this.visit(ctx.parameterList);
|
|
@@ -3255,6 +3507,15 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
3255
3507
|
curlyStringList(ctx) {
|
|
3256
3508
|
return this.visit(ctx.stringList);
|
|
3257
3509
|
}
|
|
3510
|
+
bracedNumberPairList(ctx) {
|
|
3511
|
+
return ctx.numberPair ? ctx.numberPair.map((pair) => this.visit(pair)) : [];
|
|
3512
|
+
}
|
|
3513
|
+
numberPair(ctx) {
|
|
3514
|
+
return [
|
|
3515
|
+
parseFloat(ctx.row[0].image),
|
|
3516
|
+
parseFloat(ctx.col[0].image)
|
|
3517
|
+
];
|
|
3518
|
+
}
|
|
3258
3519
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
3259
3520
|
// Helpers
|
|
3260
3521
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -3328,7 +3589,9 @@ const ImageItemSchema = z.object({
|
|
|
3328
3589
|
}),
|
|
3329
3590
|
alt: z.string(),
|
|
3330
3591
|
width: z.number().positive().optional(),
|
|
3331
|
-
height: z.number().positive().optional()
|
|
3592
|
+
height: z.number().positive().optional(),
|
|
3593
|
+
/** Opt-in: show an expand control that opens the image fullscreen. */
|
|
3594
|
+
fullscreen: z.boolean().optional()
|
|
3332
3595
|
});
|
|
3333
3596
|
const SpaceItemSchema = z.object({
|
|
3334
3597
|
id: z.string(),
|
|
@@ -3433,7 +3696,9 @@ const NumberLineHighlightItemSchema = z.object({
|
|
|
3433
3696
|
type: z.literal(JSON_TYPES.NUMBER_LINE_HIGHLIGHT),
|
|
3434
3697
|
from: z.number().finite(),
|
|
3435
3698
|
to: z.number().finite(),
|
|
3436
|
-
color: z.
|
|
3699
|
+
color: z.string().refine(isNumberLineHighlightColor, {
|
|
3700
|
+
message: highlightColorMessage("HIGHLIGHT")
|
|
3701
|
+
}).optional(),
|
|
3437
3702
|
label: z.string().optional()
|
|
3438
3703
|
});
|
|
3439
3704
|
const NumberLineCursorItemSchema = z.object({
|
|
@@ -3441,7 +3706,7 @@ const NumberLineCursorItemSchema = z.object({
|
|
|
3441
3706
|
type: z.literal(JSON_TYPES.NUMBER_LINE_CURSOR),
|
|
3442
3707
|
value: z.number().finite(),
|
|
3443
3708
|
answer: z.number().finite(),
|
|
3444
|
-
color: z.string().regex(
|
|
3709
|
+
color: z.string().regex(HEX_COLOR_REGEX).optional(),
|
|
3445
3710
|
fillRange: z.boolean(),
|
|
3446
3711
|
label: z.string().optional()
|
|
3447
3712
|
});
|
|
@@ -3627,6 +3892,34 @@ const VisualFractionJSONSchema = z.object({
|
|
|
3627
3892
|
*/
|
|
3628
3893
|
size: z.number().positive().optional()
|
|
3629
3894
|
}).superRefine(validateVisualFractionCapacity);
|
|
3895
|
+
const GridSymmetryJSONSchema = z.object({
|
|
3896
|
+
type: z.literal(JSON_TYPES.GRID_SYMMETRY),
|
|
3897
|
+
rows: z.number().int().positive(),
|
|
3898
|
+
cols: z.number().int().positive(),
|
|
3899
|
+
axis: z.enum(["vertical", "horizontal"]),
|
|
3900
|
+
interactiveArea: z.enum(["left", "right", "top", "bottom"]),
|
|
3901
|
+
cellSize: z.number().finite().positive().default(1),
|
|
3902
|
+
cells: z.array(GridCellSchema)
|
|
3903
|
+
}).superRefine((data, ctx) => {
|
|
3904
|
+
const result = GridSymmetryBlockSchema.safeParse({
|
|
3905
|
+
type: AST_TYPES.GRID_SYMMETRY_BLOCK,
|
|
3906
|
+
rows: data.rows,
|
|
3907
|
+
cols: data.cols,
|
|
3908
|
+
axis: data.axis,
|
|
3909
|
+
interactiveArea: data.interactiveArea,
|
|
3910
|
+
cellSize: data.cellSize,
|
|
3911
|
+
cells: data.cells
|
|
3912
|
+
});
|
|
3913
|
+
if (!result.success) {
|
|
3914
|
+
for (const issue of result.error.issues) {
|
|
3915
|
+
ctx.addIssue({
|
|
3916
|
+
code: z.ZodIssueCode.custom,
|
|
3917
|
+
path: issue.path,
|
|
3918
|
+
message: issue.message
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
});
|
|
3630
3923
|
const RulerWidgetSchema = z.object({
|
|
3631
3924
|
type: z.literal(JSON_TYPES.RULER),
|
|
3632
3925
|
size: z.number().optional()
|
|
@@ -3649,6 +3942,7 @@ const WidgetJSONSchema = z.union([
|
|
|
3649
3942
|
MatcherWidgetJSONSchema,
|
|
3650
3943
|
PairMatcherWidgetJSONSchema,
|
|
3651
3944
|
VisualFractionJSONSchema,
|
|
3945
|
+
GridSymmetryJSONSchema,
|
|
3652
3946
|
RulerWidgetSchema,
|
|
3653
3947
|
ProtractorWidgetSchema,
|
|
3654
3948
|
SetSquareWidgetSchema
|
|
@@ -3664,7 +3958,8 @@ const ValidationJSONSchema = z.object({
|
|
|
3664
3958
|
z.string(),
|
|
3665
3959
|
z.number(),
|
|
3666
3960
|
z.array(z.array(z.string().min(1)).min(1)).min(1),
|
|
3667
|
-
z.array(z.number().int().nonnegative())
|
|
3961
|
+
z.array(z.number().int().nonnegative()),
|
|
3962
|
+
z.array(GridCellSchema)
|
|
3668
3963
|
])
|
|
3669
3964
|
).optional(),
|
|
3670
3965
|
expression: z.string().optional(),
|
|
@@ -3762,6 +4057,9 @@ function isNumericKeypadJSON(widget) {
|
|
|
3762
4057
|
function isFractionJSON(widget) {
|
|
3763
4058
|
return widget.type === JSON_TYPES.FRACTION;
|
|
3764
4059
|
}
|
|
4060
|
+
function isGridSymmetryJSON(widget) {
|
|
4061
|
+
return widget.type === JSON_TYPES.GRID_SYMMETRY;
|
|
4062
|
+
}
|
|
3765
4063
|
function isRulerWidget(widget) {
|
|
3766
4064
|
return widget.type === JSON_TYPES.RULER;
|
|
3767
4065
|
}
|
|
@@ -9269,6 +9567,7 @@ function toJSON(ast) {
|
|
|
9269
9567
|
const idGenerator = new LayoutIdGenerator();
|
|
9270
9568
|
const widgets = {};
|
|
9271
9569
|
const answers = {};
|
|
9570
|
+
let gridSymmetryCounter = 0;
|
|
9272
9571
|
const allInlines = collectAllInlines(ast.lines);
|
|
9273
9572
|
const inlineDragIds = /* @__PURE__ */ new Map();
|
|
9274
9573
|
{
|
|
@@ -9323,6 +9622,34 @@ function toJSON(ast) {
|
|
|
9323
9622
|
});
|
|
9324
9623
|
} else if (block.type === AST_TYPES.VERTICAL_OPERATION_BLOCK) {
|
|
9325
9624
|
layout.push(transformVerticalOperation(block, idGenerator));
|
|
9625
|
+
} else if (block.type === AST_TYPES.GRID_SYMMETRY_BLOCK) {
|
|
9626
|
+
const grid = block;
|
|
9627
|
+
const widgetId = `${GRID_SYMMETRY_WIDGET_PREFIX}${gridSymmetryCounter++}`;
|
|
9628
|
+
widgets[widgetId] = {
|
|
9629
|
+
type: JSON_TYPES.GRID_SYMMETRY,
|
|
9630
|
+
rows: grid.rows,
|
|
9631
|
+
cols: grid.cols,
|
|
9632
|
+
axis: grid.axis,
|
|
9633
|
+
interactiveArea: grid.interactiveArea,
|
|
9634
|
+
cellSize: grid.cellSize,
|
|
9635
|
+
cells: grid.cells
|
|
9636
|
+
};
|
|
9637
|
+
const prefilled = grid.cells.filter((cell) => isCellInInteractiveArea(
|
|
9638
|
+
cell,
|
|
9639
|
+
grid.rows,
|
|
9640
|
+
grid.cols,
|
|
9641
|
+
grid.interactiveArea
|
|
9642
|
+
));
|
|
9643
|
+
answers[widgetId] = [...prefilled, ...computeGridSymmetryCellsToFill(grid)].sort(([ar, ac], [br, bc]) => ar - br || ac - bc);
|
|
9644
|
+
layout.push({
|
|
9645
|
+
id: idGenerator.generate(JSON_TYPES.WIDGET_REF),
|
|
9646
|
+
type: JSON_TYPES.WIDGET_REF,
|
|
9647
|
+
widgetId
|
|
9648
|
+
});
|
|
9649
|
+
layout.push({
|
|
9650
|
+
id: idGenerator.generate(JSON_TYPES.LINEBREAK),
|
|
9651
|
+
type: JSON_TYPES.LINEBREAK
|
|
9652
|
+
});
|
|
9326
9653
|
} else if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
9327
9654
|
const radioBlock = block;
|
|
9328
9655
|
layout.push({
|
|
@@ -9787,7 +10114,8 @@ function transformInline(inline, idGenerator, inlineDragIds) {
|
|
|
9787
10114
|
src: inline.src,
|
|
9788
10115
|
alt: inline.alt,
|
|
9789
10116
|
...inline.width !== void 0 && { width: inline.width },
|
|
9790
|
-
...inline.height !== void 0 && { height: inline.height }
|
|
10117
|
+
...inline.height !== void 0 && { height: inline.height },
|
|
10118
|
+
...inline.fullscreen !== void 0 && { fullscreen: inline.fullscreen }
|
|
9791
10119
|
};
|
|
9792
10120
|
case AST_TYPES.VISUAL_FRACTION:
|
|
9793
10121
|
return {
|
|
@@ -9919,6 +10247,18 @@ function areVisualFractionCountsEqual(expected, result) {
|
|
|
9919
10247
|
if (expected.length !== result.length) return false;
|
|
9920
10248
|
return lodashExports.isEqual(lodashExports.sortBy(expected), lodashExports.sortBy(result));
|
|
9921
10249
|
}
|
|
10250
|
+
function isValidGridSymmetryCell(value, rows, cols) {
|
|
10251
|
+
if (!Array.isArray(value) || value.length !== 2) return false;
|
|
10252
|
+
const [row, col] = value;
|
|
10253
|
+
return Number.isInteger(row) && Number.isInteger(col) && row >= 1 && row <= rows && col >= 1 && col <= cols;
|
|
10254
|
+
}
|
|
10255
|
+
function areGridSymmetryCellsEqual(expected, actual, rows, cols) {
|
|
10256
|
+
if (!Array.isArray(actual) || actual.length !== expected.length) return false;
|
|
10257
|
+
if (!actual.every((cell) => isValidGridSymmetryCell(cell, rows, cols))) return false;
|
|
10258
|
+
const actualKeys = new Set(actual.map(gridCellKey));
|
|
10259
|
+
if (actualKeys.size !== actual.length) return false;
|
|
10260
|
+
return expected.every((cell) => actualKeys.has(gridCellKey(cell)));
|
|
10261
|
+
}
|
|
9922
10262
|
const math$1 = create(all);
|
|
9923
10263
|
function collectNumberLines(items) {
|
|
9924
10264
|
const result = [];
|
|
@@ -9992,6 +10332,20 @@ function validateSimple(exercise, userInputs) {
|
|
|
9992
10332
|
else allCorrect = false;
|
|
9993
10333
|
continue;
|
|
9994
10334
|
}
|
|
10335
|
+
if (widget?.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
10336
|
+
const userValue2 = userInputs[widgetId];
|
|
10337
|
+
const isCorrect2 = areGridSymmetryCellsEqual(
|
|
10338
|
+
expected,
|
|
10339
|
+
userValue2,
|
|
10340
|
+
widget.rows,
|
|
10341
|
+
widget.cols
|
|
10342
|
+
);
|
|
10343
|
+
details[widgetId] = { widgetId, userValue: userValue2, isCorrect: isCorrect2 };
|
|
10344
|
+
totalCount++;
|
|
10345
|
+
if (isCorrect2) correctCount++;
|
|
10346
|
+
else allCorrect = false;
|
|
10347
|
+
continue;
|
|
10348
|
+
}
|
|
9995
10349
|
totalCount++;
|
|
9996
10350
|
const userValue = userInputs[widgetId];
|
|
9997
10351
|
const isFractionLenient = widget?.type === JSON_TYPES.FRACTION && widget.simplified === false;
|
|
@@ -10422,9 +10776,19 @@ function getMissingInputs(exercise, userInputs) {
|
|
|
10422
10776
|
}
|
|
10423
10777
|
const inputs = preprocessFractionValues(exercise, userInputs);
|
|
10424
10778
|
const isEmpty = (v) => v === void 0 || v === null || v === "" || Array.isArray(v) && v.length === 0;
|
|
10779
|
+
const isMissingAnswer = (widgetId) => {
|
|
10780
|
+
const value = inputs[widgetId];
|
|
10781
|
+
const widget = exercise.widgets[widgetId];
|
|
10782
|
+
if (widget?.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
10783
|
+
const expected = exercise.validation?.answers?.[widgetId];
|
|
10784
|
+
const expectedCount = Array.isArray(expected) ? expected.length : 0;
|
|
10785
|
+
return !Array.isArray(value) || value.length < expectedCount;
|
|
10786
|
+
}
|
|
10787
|
+
return !(widgetId in inputs) || isEmpty(value);
|
|
10788
|
+
};
|
|
10425
10789
|
if (exercise.validation.mode === VALIDATION_MODES.SIMPLE && exercise.validation.answers) {
|
|
10426
10790
|
for (const widgetId of Object.keys(exercise.validation.answers)) {
|
|
10427
|
-
if (
|
|
10791
|
+
if (isMissingAnswer(widgetId)) {
|
|
10428
10792
|
missing.push(widgetId);
|
|
10429
10793
|
}
|
|
10430
10794
|
}
|
|
@@ -10440,7 +10804,7 @@ function getMissingInputs(exercise, userInputs) {
|
|
|
10440
10804
|
...exercise.validation.variables ?? []
|
|
10441
10805
|
]);
|
|
10442
10806
|
for (const k of keys) {
|
|
10443
|
-
if (
|
|
10807
|
+
if (isMissingAnswer(k)) missing.push(k);
|
|
10444
10808
|
}
|
|
10445
10809
|
}
|
|
10446
10810
|
return missing;
|
|
@@ -15863,6 +16227,94 @@ function getSizeStyle(size, _shape) {
|
|
|
15863
16227
|
"--visual-fraction-size": `${size * VISUAL_FRACTION_SVG_SIZE_SCALE}rem`
|
|
15864
16228
|
};
|
|
15865
16229
|
}
|
|
16230
|
+
const GridSymmetry = ({
|
|
16231
|
+
widgetId,
|
|
16232
|
+
widget,
|
|
16233
|
+
value,
|
|
16234
|
+
validationStatus = null,
|
|
16235
|
+
onChange
|
|
16236
|
+
}) => {
|
|
16237
|
+
const initialKeys = useMemo(() => new Set(widget.cells.map(gridCellKey)), [widget.cells]);
|
|
16238
|
+
const initialInteractiveCells = useMemo(
|
|
16239
|
+
() => widget.cells.filter(
|
|
16240
|
+
(cell) => isCellInInteractiveArea(cell, widget.rows, widget.cols, widget.interactiveArea)
|
|
16241
|
+
),
|
|
16242
|
+
[widget.cells, widget.rows, widget.cols, widget.interactiveArea]
|
|
16243
|
+
);
|
|
16244
|
+
const selectedCells = value ?? initialInteractiveCells;
|
|
16245
|
+
const selectedKeys = useMemo(() => new Set(selectedCells.map(gridCellKey)), [selectedCells]);
|
|
16246
|
+
const toggleCell = (cell) => {
|
|
16247
|
+
const key2 = gridCellKey(cell);
|
|
16248
|
+
if (initialKeys.has(key2)) return;
|
|
16249
|
+
const next = selectedKeys.has(key2) ? selectedCells.filter((candidate) => gridCellKey(candidate) !== key2) : [...selectedCells, cell];
|
|
16250
|
+
onChange(widgetId, next);
|
|
16251
|
+
};
|
|
16252
|
+
const statusClass = validationStatus ? ` grid-symmetry--${validationStatus}` : "";
|
|
16253
|
+
const cells = [];
|
|
16254
|
+
for (let row = 1; row <= widget.rows; row++) {
|
|
16255
|
+
for (let col = 1; col <= widget.cols; col++) {
|
|
16256
|
+
const cell = [row, col];
|
|
16257
|
+
const key2 = gridCellKey(cell);
|
|
16258
|
+
const interactive = isCellInInteractiveArea(
|
|
16259
|
+
cell,
|
|
16260
|
+
widget.rows,
|
|
16261
|
+
widget.cols,
|
|
16262
|
+
widget.interactiveArea
|
|
16263
|
+
);
|
|
16264
|
+
const initial = initialKeys.has(key2);
|
|
16265
|
+
const selected = selectedKeys.has(key2);
|
|
16266
|
+
const locked = initial || !interactive;
|
|
16267
|
+
const roleClass = initial ? interactive ? " grid-symmetry__cell--prefilled" : " grid-symmetry__cell--reference" : selected ? " grid-symmetry__cell--answer" : "";
|
|
16268
|
+
const className = `grid-symmetry__cell${interactive ? " grid-symmetry__cell--interactive" : ""}${locked ? "" : " grid-symmetry__cell--editable"}${selected || initial ? " grid-symmetry__cell--selected" : ""}${roleClass}`;
|
|
16269
|
+
const label = `Row ${row}, column ${col}`;
|
|
16270
|
+
cells.push(
|
|
16271
|
+
locked ? /* @__PURE__ */ jsx(
|
|
16272
|
+
"span",
|
|
16273
|
+
{
|
|
16274
|
+
className,
|
|
16275
|
+
role: initial ? "img" : void 0,
|
|
16276
|
+
"aria-label": initial ? `${label}, filled` : void 0,
|
|
16277
|
+
"aria-hidden": initial ? void 0 : true
|
|
16278
|
+
},
|
|
16279
|
+
key2
|
|
16280
|
+
) : /* @__PURE__ */ jsx(
|
|
16281
|
+
"button",
|
|
16282
|
+
{
|
|
16283
|
+
type: "button",
|
|
16284
|
+
className,
|
|
16285
|
+
"aria-label": label,
|
|
16286
|
+
"aria-pressed": selected,
|
|
16287
|
+
onClick: () => toggleCell(cell)
|
|
16288
|
+
},
|
|
16289
|
+
key2
|
|
16290
|
+
)
|
|
16291
|
+
);
|
|
16292
|
+
}
|
|
16293
|
+
}
|
|
16294
|
+
return /* @__PURE__ */ jsx("div", { className: `grid-symmetry${statusClass}`, "data-testid": "grid-symmetry", children: /* @__PURE__ */ jsxs(
|
|
16295
|
+
"div",
|
|
16296
|
+
{
|
|
16297
|
+
className: "grid-symmetry__grid",
|
|
16298
|
+
style: {
|
|
16299
|
+
"--grid-symmetry-cell-size": `calc(48px * ${widget.cellSize})`,
|
|
16300
|
+
gridTemplateColumns: `repeat(${widget.cols}, var(--grid-symmetry-cell-size))`,
|
|
16301
|
+
gridTemplateRows: `repeat(${widget.rows}, var(--grid-symmetry-cell-size))`
|
|
16302
|
+
},
|
|
16303
|
+
role: "group",
|
|
16304
|
+
"aria-label": `Symmetry grid, ${widget.rows} rows by ${widget.cols} columns`,
|
|
16305
|
+
children: [
|
|
16306
|
+
cells,
|
|
16307
|
+
/* @__PURE__ */ jsx(
|
|
16308
|
+
"span",
|
|
16309
|
+
{
|
|
16310
|
+
className: `grid-symmetry__axis grid-symmetry__axis--${widget.axis}`,
|
|
16311
|
+
"aria-hidden": "true"
|
|
16312
|
+
}
|
|
16313
|
+
)
|
|
16314
|
+
]
|
|
16315
|
+
}
|
|
16316
|
+
) });
|
|
16317
|
+
};
|
|
15866
16318
|
const RadioOptions = ({
|
|
15867
16319
|
name,
|
|
15868
16320
|
options,
|
|
@@ -16511,6 +16963,9 @@ function isPairMatcherWidget(widget) {
|
|
|
16511
16963
|
function isVisualFractionWidget(widget) {
|
|
16512
16964
|
return widget.type === "visual-fraction";
|
|
16513
16965
|
}
|
|
16966
|
+
function isGridSymmetryWidget(widget) {
|
|
16967
|
+
return widget.type === "grid-symmetry";
|
|
16968
|
+
}
|
|
16514
16969
|
const WidgetRenderer = ({
|
|
16515
16970
|
widget,
|
|
16516
16971
|
widgetId,
|
|
@@ -16655,8 +17110,243 @@ const WidgetRenderer = ({
|
|
|
16655
17110
|
}
|
|
16656
17111
|
);
|
|
16657
17112
|
}
|
|
17113
|
+
if (isGridSymmetryWidget(widget)) {
|
|
17114
|
+
const cells = Array.isArray(value) && value.every(
|
|
17115
|
+
(cell) => Array.isArray(cell) && cell.length === 2 && cell.every(Number.isInteger)
|
|
17116
|
+
) ? value : void 0;
|
|
17117
|
+
return /* @__PURE__ */ jsx(
|
|
17118
|
+
GridSymmetry,
|
|
17119
|
+
{
|
|
17120
|
+
widgetId,
|
|
17121
|
+
widget,
|
|
17122
|
+
value: cells,
|
|
17123
|
+
validationStatus,
|
|
17124
|
+
onChange: (id, nextValue) => onChange(id, nextValue)
|
|
17125
|
+
}
|
|
17126
|
+
);
|
|
17127
|
+
}
|
|
16658
17128
|
return null;
|
|
16659
17129
|
};
|
|
17130
|
+
const ExpandDiamondIcon = () => {
|
|
17131
|
+
const gradientId = useId();
|
|
17132
|
+
return /* @__PURE__ */ jsxs(
|
|
17133
|
+
"svg",
|
|
17134
|
+
{
|
|
17135
|
+
className: "image-zoom-icon",
|
|
17136
|
+
viewBox: "0 0 60 60",
|
|
17137
|
+
fill: "none",
|
|
17138
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
17139
|
+
"aria-hidden": "true",
|
|
17140
|
+
focusable: "false",
|
|
17141
|
+
children: [
|
|
17142
|
+
/* @__PURE__ */ jsx(
|
|
17143
|
+
"rect",
|
|
17144
|
+
{
|
|
17145
|
+
y: "-1.41531",
|
|
17146
|
+
width: "31.6754",
|
|
17147
|
+
height: "31.6754",
|
|
17148
|
+
rx: "7",
|
|
17149
|
+
transform: "matrix(0.706556 0.707657 -0.706556 0.707657 29.4526 10.0862)",
|
|
17150
|
+
fill: "white"
|
|
17151
|
+
}
|
|
17152
|
+
),
|
|
17153
|
+
/* @__PURE__ */ jsx(
|
|
17154
|
+
"rect",
|
|
17155
|
+
{
|
|
17156
|
+
y: "-1.41531",
|
|
17157
|
+
width: "31.6754",
|
|
17158
|
+
height: "31.6754",
|
|
17159
|
+
rx: "7",
|
|
17160
|
+
transform: "matrix(0.706556 0.707657 -0.706556 0.707657 29.4526 10.0862)",
|
|
17161
|
+
stroke: "#BDAFFE",
|
|
17162
|
+
strokeWidth: "2"
|
|
17163
|
+
}
|
|
17164
|
+
),
|
|
17165
|
+
/* @__PURE__ */ jsx(
|
|
17166
|
+
"rect",
|
|
17167
|
+
{
|
|
17168
|
+
y: "-1.41531",
|
|
17169
|
+
width: "31.6754",
|
|
17170
|
+
height: "31.6754",
|
|
17171
|
+
rx: "7",
|
|
17172
|
+
transform: "matrix(0.706556 0.707657 -0.706556 0.707657 29.4526 10.0862)",
|
|
17173
|
+
stroke: `url(#${gradientId})`,
|
|
17174
|
+
strokeOpacity: "0.3",
|
|
17175
|
+
strokeWidth: "2"
|
|
17176
|
+
}
|
|
17177
|
+
),
|
|
17178
|
+
/* @__PURE__ */ jsxs(
|
|
17179
|
+
"g",
|
|
17180
|
+
{
|
|
17181
|
+
stroke: "currentColor",
|
|
17182
|
+
strokeWidth: "2.5",
|
|
17183
|
+
strokeLinecap: "round",
|
|
17184
|
+
strokeLinejoin: "round",
|
|
17185
|
+
children: [
|
|
17186
|
+
/* @__PURE__ */ jsx("path", { d: "M22.45 27 L22.45 23.5 L25.95 23.5" }),
|
|
17187
|
+
/* @__PURE__ */ jsx("path", { d: "M34.95 23.5 L38.45 23.5 L38.45 27" }),
|
|
17188
|
+
/* @__PURE__ */ jsx("path", { d: "M38.45 36 L38.45 39.5 L34.95 39.5" }),
|
|
17189
|
+
/* @__PURE__ */ jsx("path", { d: "M25.95 39.5 L22.45 39.5 L22.45 36" })
|
|
17190
|
+
]
|
|
17191
|
+
}
|
|
17192
|
+
),
|
|
17193
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs(
|
|
17194
|
+
"linearGradient",
|
|
17195
|
+
{
|
|
17196
|
+
id: gradientId,
|
|
17197
|
+
x1: "3.84406",
|
|
17198
|
+
y1: "3.97407",
|
|
17199
|
+
x2: "24.7079",
|
|
17200
|
+
y2: "24.8379",
|
|
17201
|
+
gradientUnits: "userSpaceOnUse",
|
|
17202
|
+
children: [
|
|
17203
|
+
/* @__PURE__ */ jsx("stop", { stopColor: "#FAFAFA" }),
|
|
17204
|
+
/* @__PURE__ */ jsx("stop", { offset: "1", stopColor: "#8A4899" })
|
|
17205
|
+
]
|
|
17206
|
+
}
|
|
17207
|
+
) })
|
|
17208
|
+
]
|
|
17209
|
+
}
|
|
17210
|
+
);
|
|
17211
|
+
};
|
|
17212
|
+
const CloseIcon = () => /* @__PURE__ */ jsxs(
|
|
17213
|
+
"svg",
|
|
17214
|
+
{
|
|
17215
|
+
className: "image-lightbox-close-icon",
|
|
17216
|
+
viewBox: "0 0 24 24",
|
|
17217
|
+
fill: "none",
|
|
17218
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
17219
|
+
"aria-hidden": "true",
|
|
17220
|
+
focusable: "false",
|
|
17221
|
+
children: [
|
|
17222
|
+
/* @__PURE__ */ jsx(
|
|
17223
|
+
"path",
|
|
17224
|
+
{
|
|
17225
|
+
d: "M18 6L6 18",
|
|
17226
|
+
stroke: "currentColor",
|
|
17227
|
+
strokeWidth: "2",
|
|
17228
|
+
strokeLinecap: "round",
|
|
17229
|
+
strokeLinejoin: "round"
|
|
17230
|
+
}
|
|
17231
|
+
),
|
|
17232
|
+
/* @__PURE__ */ jsx(
|
|
17233
|
+
"path",
|
|
17234
|
+
{
|
|
17235
|
+
d: "M6 6L18 18",
|
|
17236
|
+
stroke: "currentColor",
|
|
17237
|
+
strokeWidth: "2",
|
|
17238
|
+
strokeLinecap: "round",
|
|
17239
|
+
strokeLinejoin: "round"
|
|
17240
|
+
}
|
|
17241
|
+
)
|
|
17242
|
+
]
|
|
17243
|
+
}
|
|
17244
|
+
);
|
|
17245
|
+
const ImageItem = ({
|
|
17246
|
+
src,
|
|
17247
|
+
alt,
|
|
17248
|
+
width,
|
|
17249
|
+
height,
|
|
17250
|
+
fullscreen = false
|
|
17251
|
+
}) => {
|
|
17252
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
17253
|
+
const triggerRef = useRef(null);
|
|
17254
|
+
const closeRef = useRef(null);
|
|
17255
|
+
const open = useCallback(() => setIsOpen(true), []);
|
|
17256
|
+
const close = useCallback(() => setIsOpen(false), []);
|
|
17257
|
+
const isLightboxOpen = isOpen && fullscreen;
|
|
17258
|
+
useEffect(() => {
|
|
17259
|
+
if (!isLightboxOpen) return;
|
|
17260
|
+
const onKeyDown = (e) => {
|
|
17261
|
+
if (e.key === "Escape") {
|
|
17262
|
+
e.stopPropagation();
|
|
17263
|
+
close();
|
|
17264
|
+
}
|
|
17265
|
+
};
|
|
17266
|
+
document.addEventListener("keydown", onKeyDown);
|
|
17267
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
17268
|
+
}, [isLightboxOpen, close]);
|
|
17269
|
+
useEffect(() => {
|
|
17270
|
+
if (!isLightboxOpen) return;
|
|
17271
|
+
const previous = document.body.style.overflow;
|
|
17272
|
+
document.body.style.overflow = "hidden";
|
|
17273
|
+
return () => {
|
|
17274
|
+
document.body.style.overflow = previous;
|
|
17275
|
+
};
|
|
17276
|
+
}, [isLightboxOpen]);
|
|
17277
|
+
const wasOpen = useRef(false);
|
|
17278
|
+
useEffect(() => {
|
|
17279
|
+
if (isLightboxOpen) {
|
|
17280
|
+
closeRef.current?.focus();
|
|
17281
|
+
} else if (wasOpen.current) {
|
|
17282
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
17283
|
+
}
|
|
17284
|
+
wasOpen.current = isLightboxOpen;
|
|
17285
|
+
}, [isLightboxOpen]);
|
|
17286
|
+
const overlay = isLightboxOpen && typeof document !== "undefined" ? createPortal(
|
|
17287
|
+
/* @__PURE__ */ jsxs(
|
|
17288
|
+
"div",
|
|
17289
|
+
{
|
|
17290
|
+
className: "image-lightbox",
|
|
17291
|
+
role: "dialog",
|
|
17292
|
+
"aria-modal": "true",
|
|
17293
|
+
"aria-label": alt || "Image",
|
|
17294
|
+
onClick: close,
|
|
17295
|
+
children: [
|
|
17296
|
+
/* @__PURE__ */ jsx(
|
|
17297
|
+
"button",
|
|
17298
|
+
{
|
|
17299
|
+
ref: closeRef,
|
|
17300
|
+
type: "button",
|
|
17301
|
+
className: "image-lightbox-close",
|
|
17302
|
+
"aria-label": "Close fullscreen image",
|
|
17303
|
+
onClick: close,
|
|
17304
|
+
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
17305
|
+
}
|
|
17306
|
+
),
|
|
17307
|
+
/* @__PURE__ */ jsx(
|
|
17308
|
+
"img",
|
|
17309
|
+
{
|
|
17310
|
+
className: "image-lightbox-img",
|
|
17311
|
+
src,
|
|
17312
|
+
alt,
|
|
17313
|
+
onClick: (e) => e.stopPropagation()
|
|
17314
|
+
}
|
|
17315
|
+
)
|
|
17316
|
+
]
|
|
17317
|
+
}
|
|
17318
|
+
),
|
|
17319
|
+
document.body
|
|
17320
|
+
) : null;
|
|
17321
|
+
const img = /* @__PURE__ */ jsx(
|
|
17322
|
+
"img",
|
|
17323
|
+
{
|
|
17324
|
+
className: `image-item text-item ${fullscreen ? "image-item-zoomable" : ""}`,
|
|
17325
|
+
src,
|
|
17326
|
+
alt,
|
|
17327
|
+
...width !== void 0 && { width },
|
|
17328
|
+
...height !== void 0 && { height },
|
|
17329
|
+
loading: "lazy",
|
|
17330
|
+
...fullscreen && { onClick: open }
|
|
17331
|
+
}
|
|
17332
|
+
);
|
|
17333
|
+
if (!fullscreen) return img;
|
|
17334
|
+
return /* @__PURE__ */ jsxs("span", { className: "image-item-wrapper", children: [
|
|
17335
|
+
img,
|
|
17336
|
+
/* @__PURE__ */ jsx(
|
|
17337
|
+
"button",
|
|
17338
|
+
{
|
|
17339
|
+
ref: triggerRef,
|
|
17340
|
+
type: "button",
|
|
17341
|
+
className: "image-zoom-btn",
|
|
17342
|
+
"aria-label": alt ? `View "${alt}" fullscreen` : "View image fullscreen",
|
|
17343
|
+
onClick: open,
|
|
17344
|
+
children: /* @__PURE__ */ jsx(ExpandDiamondIcon, {})
|
|
17345
|
+
}
|
|
17346
|
+
),
|
|
17347
|
+
overlay
|
|
17348
|
+
] });
|
|
17349
|
+
};
|
|
16660
17350
|
const EPSILON = 10 ** -INTERNAL_PRECISION;
|
|
16661
17351
|
const cleanFloat = (n) => parseFloat(n.toFixed(INTERNAL_PRECISION));
|
|
16662
17352
|
const roundTick = (n) => parseFloat(n.toFixed(DISPLAY_PRECISION));
|
|
@@ -16710,7 +17400,7 @@ const getPositionWidgetId = (content) => {
|
|
|
16710
17400
|
}
|
|
16711
17401
|
return null;
|
|
16712
17402
|
};
|
|
16713
|
-
const toHighlightColor = (color) => `var(--nl-highlight-${color})`;
|
|
17403
|
+
const toHighlightColor = (color) => color.startsWith("#") ? color : `var(--nl-highlight-${color})`;
|
|
16714
17404
|
const getBandGeometry = (h, min, max) => {
|
|
16715
17405
|
const leftPct = toPercent(Math.min(h.from, h.to), min, max);
|
|
16716
17406
|
const rightPct = toPercent(Math.max(h.from, h.to), min, max);
|
|
@@ -17407,24 +18097,27 @@ function getArrowArcLabelTop(widthPct) {
|
|
|
17407
18097
|
const arcTop = svgTop + ARROW_ARC_BASE_Y - getArrowArcLift(widthPct);
|
|
17408
18098
|
return Math.max(0, arcTop - ARROW_ARC_LABEL_OFFSET);
|
|
17409
18099
|
}
|
|
17410
|
-
function getArrowArcPaths(widthPct, svgWidthPx = ARROW_ARC_VIEWBOX_WIDTH) {
|
|
17411
|
-
const
|
|
18100
|
+
function getArrowArcPaths(widthPct, svgWidthPx = ARROW_ARC_VIEWBOX_WIDTH, reversed = false) {
|
|
18101
|
+
const width = getArrowArcSvgWidth(svgWidthPx);
|
|
18102
|
+
const startX = reversed ? width : 0;
|
|
18103
|
+
const tipX = reversed ? 0 : width;
|
|
18104
|
+
const sign = reversed ? -1 : 1;
|
|
17412
18105
|
const controlInset = Math.min(svgWidthPx * 0.18, 48);
|
|
17413
18106
|
const lift = getArrowArcLift(widthPct);
|
|
17414
18107
|
const controlY = fmt(ARROW_ARC_BASE_Y - lift);
|
|
17415
|
-
const tangentAngle = Math.atan2(ARROW_ARC_BASE_Y - controlY,
|
|
17416
|
-
const tx = Math.cos(tangentAngle);
|
|
18108
|
+
const tangentAngle = Math.atan2(ARROW_ARC_BASE_Y - controlY, controlInset);
|
|
18109
|
+
const tx = Math.cos(tangentAngle) * sign;
|
|
17417
18110
|
const ty = Math.sin(tangentAngle);
|
|
17418
|
-
const shaftEndX = fmt(
|
|
18111
|
+
const shaftEndX = fmt(tipX - tx * ARROW_ARC_HEAD_LENGTH);
|
|
17419
18112
|
const shaftEndY = fmt(ARROW_ARC_BASE_Y - ty * ARROW_ARC_HEAD_LENGTH);
|
|
17420
18113
|
const endControlX = fmt(shaftEndX - tx * controlInset);
|
|
17421
18114
|
const endControlY = fmt(shaftEndY - ty * controlInset);
|
|
17422
|
-
const arcPath = `M
|
|
18115
|
+
const arcPath = `M ${fmt(startX)} ${ARROW_ARC_BASE_Y} C ${fmt(startX + sign * controlInset)} ${controlY} ${endControlX} ${endControlY} ${shaftEndX} ${shaftEndY}`;
|
|
17423
18116
|
const nx = -ty;
|
|
17424
18117
|
const ny = tx;
|
|
17425
|
-
const baseX =
|
|
18118
|
+
const baseX = tipX - tx * ARROW_ARC_HEAD_LENGTH;
|
|
17426
18119
|
const baseY = ARROW_ARC_BASE_Y - ty * ARROW_ARC_HEAD_LENGTH;
|
|
17427
|
-
const headPath = `M ${fmt(
|
|
18120
|
+
const headPath = `M ${fmt(tipX)} ${ARROW_ARC_BASE_Y} L ${fmt(baseX + nx * ARROW_ARC_HEAD_HALF_WIDTH)} ${fmt(baseY + ny * ARROW_ARC_HEAD_HALF_WIDTH)} L ${fmt(baseX - nx * ARROW_ARC_HEAD_HALF_WIDTH)} ${fmt(baseY - ny * ARROW_ARC_HEAD_HALF_WIDTH)} Z`;
|
|
17428
18121
|
return { arcPath, headPath };
|
|
17429
18122
|
}
|
|
17430
18123
|
function getArrowArcLayouts(arrows, min, max) {
|
|
@@ -17435,9 +18128,10 @@ function getArrowArcLayouts(arrows, min, max) {
|
|
|
17435
18128
|
return {
|
|
17436
18129
|
arrow,
|
|
17437
18130
|
index,
|
|
17438
|
-
leftPct: fromPct,
|
|
17439
|
-
rightPct: toPct,
|
|
17440
|
-
widthPct: toPct - fromPct,
|
|
18131
|
+
leftPct: Math.min(fromPct, toPct),
|
|
18132
|
+
rightPct: Math.max(fromPct, toPct),
|
|
18133
|
+
widthPct: Math.abs(toPct - fromPct),
|
|
18134
|
+
reversed: toPct < fromPct,
|
|
17441
18135
|
level: 0
|
|
17442
18136
|
};
|
|
17443
18137
|
}).filter((layout) => layout.widthPct > 0).sort((a, b) => a.leftPct - b.leftPct || a.rightPct - b.rightPct || a.index - b.index);
|
|
@@ -17474,7 +18168,7 @@ function useElementWidth() {
|
|
|
17474
18168
|
return [ref, width];
|
|
17475
18169
|
}
|
|
17476
18170
|
const ArrowArc = ({
|
|
17477
|
-
layout: { arrow, leftPct, widthPct, level },
|
|
18171
|
+
layout: { arrow, leftPct, widthPct, reversed, level },
|
|
17478
18172
|
widgets,
|
|
17479
18173
|
values,
|
|
17480
18174
|
droppedInputs,
|
|
@@ -17486,8 +18180,8 @@ const ArrowArc = ({
|
|
|
17486
18180
|
const hasLabel = arrow.content.length > 0;
|
|
17487
18181
|
const svgWidthPx = getArrowArcSvgWidth(renderedWidthPx);
|
|
17488
18182
|
const { arcPath, headPath } = useMemo(
|
|
17489
|
-
() => getArrowArcPaths(widthPct, svgWidthPx),
|
|
17490
|
-
[widthPct, svgWidthPx]
|
|
18183
|
+
() => getArrowArcPaths(widthPct, svgWidthPx, reversed),
|
|
18184
|
+
[widthPct, svgWidthPx, reversed]
|
|
17491
18185
|
);
|
|
17492
18186
|
return /* @__PURE__ */ jsxs(
|
|
17493
18187
|
"div",
|
|
@@ -18243,14 +18937,13 @@ const LayoutRenderer = ({
|
|
|
18243
18937
|
}
|
|
18244
18938
|
case JSON_TYPES.IMAGE:
|
|
18245
18939
|
return /* @__PURE__ */ jsx(
|
|
18246
|
-
|
|
18940
|
+
ImageItem,
|
|
18247
18941
|
{
|
|
18248
|
-
className: "image-item text-item",
|
|
18249
18942
|
src: item.src,
|
|
18250
18943
|
alt: item.alt,
|
|
18251
|
-
|
|
18252
|
-
|
|
18253
|
-
|
|
18944
|
+
width: item.width,
|
|
18945
|
+
height: item.height,
|
|
18946
|
+
fullscreen: item.fullscreen
|
|
18254
18947
|
}
|
|
18255
18948
|
);
|
|
18256
18949
|
case JSON_TYPES.WIDGET_REF: {
|
|
@@ -18496,6 +19189,14 @@ function buildInitialValues(json) {
|
|
|
18496
19189
|
const initial = {};
|
|
18497
19190
|
for (const [id, widget] of Object.entries(json.widgets)) {
|
|
18498
19191
|
if (widget.type === JSON_TYPES.MATCHER || widget.type === JSON_TYPES.PAIR_MATCHER) initial[id] = [];
|
|
19192
|
+
if (widget.type === JSON_TYPES.GRID_SYMMETRY) {
|
|
19193
|
+
initial[id] = widget.cells.filter((cell) => isCellInInteractiveArea(
|
|
19194
|
+
cell,
|
|
19195
|
+
widget.rows,
|
|
19196
|
+
widget.cols,
|
|
19197
|
+
widget.interactiveArea
|
|
19198
|
+
));
|
|
19199
|
+
}
|
|
18499
19200
|
}
|
|
18500
19201
|
return initial;
|
|
18501
19202
|
}
|
|
@@ -18925,6 +19626,15 @@ export {
|
|
|
18925
19626
|
FractionInlineSchema,
|
|
18926
19627
|
FractionJSONSchema,
|
|
18927
19628
|
FreeTextInlineSchema,
|
|
19629
|
+
GRID_SYMMETRY_AXES,
|
|
19630
|
+
GRID_SYMMETRY_INTERACTIVE_AREAS,
|
|
19631
|
+
GRID_SYMMETRY_MAX_CELL_SIZE,
|
|
19632
|
+
GRID_SYMMETRY_MAX_COLS,
|
|
19633
|
+
GRID_SYMMETRY_MAX_ROWS,
|
|
19634
|
+
GRID_SYMMETRY_MIN_CELL_SIZE,
|
|
19635
|
+
GridCellSchema,
|
|
19636
|
+
GridSymmetryBlockSchema,
|
|
19637
|
+
GridSymmetryJSONSchema,
|
|
18928
19638
|
GroupItemSchema,
|
|
18929
19639
|
HighlightedTextInlineSchema,
|
|
18930
19640
|
HighlightedTextSchema,
|
|
@@ -19011,6 +19721,8 @@ export {
|
|
|
19011
19721
|
WidgetRenderer,
|
|
19012
19722
|
compile,
|
|
19013
19723
|
compileWithAST,
|
|
19724
|
+
computeGridSymmetryCellsToFill,
|
|
19725
|
+
computeGridSymmetryExpectedCells,
|
|
19014
19726
|
createBoldText,
|
|
19015
19727
|
createColorText,
|
|
19016
19728
|
createDisplayClock,
|
|
@@ -19019,6 +19731,7 @@ export {
|
|
|
19019
19731
|
createExpressionValidation,
|
|
19020
19732
|
createFraction,
|
|
19021
19733
|
createFreeText,
|
|
19734
|
+
createGridSymmetryBlock,
|
|
19022
19735
|
createHighlightedText,
|
|
19023
19736
|
createImage,
|
|
19024
19737
|
createInlineDragOption,
|
|
@@ -19057,6 +19770,7 @@ export {
|
|
|
19057
19770
|
getFractionKey,
|
|
19058
19771
|
getMissingInputs,
|
|
19059
19772
|
isBoldText,
|
|
19773
|
+
isCellInInteractiveArea,
|
|
19060
19774
|
isClock,
|
|
19061
19775
|
isClockItem,
|
|
19062
19776
|
isColorText,
|
|
@@ -19068,6 +19782,8 @@ export {
|
|
|
19068
19782
|
isFractionJSON,
|
|
19069
19783
|
isFractionKey,
|
|
19070
19784
|
isFreeText,
|
|
19785
|
+
isGridSymmetryBlock,
|
|
19786
|
+
isGridSymmetryJSON,
|
|
19071
19787
|
isGroupItem,
|
|
19072
19788
|
isHighlightedText,
|
|
19073
19789
|
isImage,
|
|
@@ -19120,6 +19836,7 @@ export {
|
|
|
19120
19836
|
parse,
|
|
19121
19837
|
parseFractionKey,
|
|
19122
19838
|
parseSafe,
|
|
19839
|
+
reflectGridSymmetryCell,
|
|
19123
19840
|
renderExercise,
|
|
19124
19841
|
toAST,
|
|
19125
19842
|
toJSON,
|