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