@mathscan/math-exercise-engine 1.2.7 → 1.2.9

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/README.md CHANGED
@@ -22,6 +22,19 @@ pnpm add @mathscan/math-exercise-engine
22
22
  yarn add @mathscan/math-exercise-engine
23
23
  ```
24
24
 
25
+ ### Upgrading to 2.0
26
+
27
+ - **React 19.2+** is required for the renderer (peer dependency was `^18 || ^19`).
28
+ - **`konva` and `react-konva`** are new optional peers, needed only to render GEOMETRY widgets.
29
+ npm 7+ and pnpm install them automatically; with Yarn 1 add them yourself.
30
+ - **Node** `^20.19.0 || >=22.12.0`.
31
+ - **Geometry engine on the main entry.** `validate()` grades GEOMETRY widgets, so importing the
32
+ package now loads `@mathscan/geometry-engine` (and its `jsxgraph` kernel) even for exercises
33
+ without geometry. It runs headless — no DOM, no React — but adds to install and import cost.
34
+ The geometry canvas UI stays lazy and loads only when a GEOMETRY widget renders.
35
+ - **`ValidationResult.scoreContribution`** (new, optional) carries raw `{ correct, earned, possible }`
36
+ scoring units. `validate()` always sets it; its return type is `ScoredValidationResult`.
37
+
25
38
  ## 🚀 Quick Start
26
39
 
27
40
  ```typescript
@@ -0,0 +1,50 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useMemo, useEffect } from "react";
3
+ import { StudentGeometryExercise } from "@mathscan/geometry-engine/react";
4
+ import { serializeGeometryState } from "@mathscan/geometry-engine/validation";
5
+ import { u as useExerciseLocked, r as readGeometryAttempt, l as logRejectedGeometryAttempt } from "./index-XAInSHXa.mjs";
6
+ const GeometryWidget = ({
7
+ widgetId,
8
+ widget,
9
+ value,
10
+ validationDetail,
11
+ onChange
12
+ }) => {
13
+ const exerciseLocked = useExerciseLocked();
14
+ const { attempt, issues } = useMemo(() => readGeometryAttempt(value), [value]);
15
+ useEffect(() => {
16
+ if (issues) logRejectedGeometryAttempt(widgetId, issues);
17
+ }, [issues, widgetId]);
18
+ return /* @__PURE__ */ jsx("div", { className: "geometry-widget", "data-widget-id": widgetId, children: /* @__PURE__ */ jsx(
19
+ StudentGeometryExercise,
20
+ {
21
+ showCompanion: false,
22
+ exercise: widget.json,
23
+ value: attempt,
24
+ readOnly: exerciseLocked,
25
+ validationFeedback: validationDetail?.geometryFeedback ?? null,
26
+ controls: {
27
+ toolbar: false,
28
+ history: false,
29
+ delete: false,
30
+ reset: false,
31
+ instruments: false
32
+ },
33
+ onChange: (next) => onChange(widgetId, serializeGeometryState(next)),
34
+ onError: (issues2) => {
35
+ console.error(
36
+ `Geometry exercise rejected (widget "${widgetId}"):`,
37
+ issues2.map(({ source, path, message }) => ({
38
+ source,
39
+ path: path.join("."),
40
+ message
41
+ }))
42
+ );
43
+ }
44
+ }
45
+ ) });
46
+ };
47
+ export {
48
+ GeometryWidget
49
+ };
50
+ //# sourceMappingURL=GeometryWidget-BRV6LLcW.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeometryWidget-BRV6LLcW.mjs","sources":["../src/renderer/widgets/geometry/GeometryWidget.tsx"],"sourcesContent":["import React, { useEffect, useMemo } from 'react';\nimport { StudentGeometryExercise } from '@mathscan/geometry-engine/react';\nimport { serializeGeometryState } from '@mathscan/geometry-engine/validation';\nimport type { GeometryJSON } from '../../../transformers/zod-schemas';\nimport type { WidgetValidation } from '../../../validators';\nimport { logRejectedGeometryAttempt, readGeometryAttempt } from '../../../validators';\nimport { useExerciseLocked } from '../../lock';\n\nexport interface GeometryWidgetProps {\n widgetId: string;\n widget: GeometryJSON;\n /** Serialized `GeometryState` — the value this package stores and validates. */\n value?: string;\n validationDetail?: WidgetValidation;\n onChange: (widgetId: string, value: string) => void;\n}\n\n/**\n * Adapter between this package's widget contract and the geometry engine's\n * student component. It owns no geometry logic: the attempt is passed through as\n * data, the engine's verdict is passed back untouched, and the canvas itself\n * decides what to draw and when a result has gone stale.\n */\nexport const GeometryWidget: React.FC<GeometryWidgetProps> = ({\n widgetId,\n widget,\n value,\n validationDetail,\n onChange,\n}) => {\n const exerciseLocked = useExerciseLocked();\n // Parsed per value change: the component is controlled, and handing it the\n // same attempt as a fresh object on every render would look like a new value.\n const { attempt, issues } = useMemo(() => readGeometryAttempt(value), [value]);\n\n // A stored value the engine refuses is shown as an empty canvas, so the\n // student's saved work silently vanishes unless this says so — once per value.\n useEffect(() => {\n if (issues) logRejectedGeometryAttempt(widgetId, issues);\n }, [issues, widgetId]);\n\n return (\n <div className=\"geometry-widget\" data-widget-id={widgetId}>\n <StudentGeometryExercise\n showCompanion={false}\n exercise={widget.json}\n value={attempt}\n readOnly={exerciseLocked}\n validationFeedback={validationDetail?.geometryFeedback ?? null}\n // Reset belongs to the exercise, which clears every widget at once.\n controls={{\n toolbar: false,\n history: false,\n delete: false,\n reset: false,\n instruments: false,\n }}\n onChange={(next) => onChange(widgetId, serializeGeometryState(next))}\n // The engine renders its own error box for a rejected exercise or\n // value; this is the only place the reasons surface for an author.\n onError={(issues) => {\n console.error(\n `Geometry exercise rejected (widget \"${widgetId}\"):`,\n issues.map(({ source, path, message }) => ({\n source,\n path: path.join('.'),\n message,\n }))\n );\n }}\n />\n </div>\n );\n};\n"],"names":["issues"],"mappings":";;;;;AAuBO,MAAM,iBAAgD,CAAC;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,iBAAiB,kBAAA;AAGvB,QAAM,EAAE,SAAS,OAAA,IAAW,QAAQ,MAAM,oBAAoB,KAAK,GAAG,CAAC,KAAK,CAAC;AAI7E,YAAU,MAAM;AACd,QAAI,OAAQ,4BAA2B,UAAU,MAAM;AAAA,EACzD,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAErB,SACE,oBAAC,OAAA,EAAI,WAAU,mBAAkB,kBAAgB,UAC/C,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAe;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,oBAAoB,kBAAkB,oBAAoB;AAAA,MAE1D,UAAU;AAAA,QACR,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,aAAa;AAAA,MAAA;AAAA,MAEf,UAAU,CAAC,SAAS,SAAS,UAAU,uBAAuB,IAAI,CAAC;AAAA,MAGnE,SAAS,CAACA,YAAW;AACnB,gBAAQ;AAAA,UACN,uCAAuC,QAAQ;AAAA,UAC/CA,QAAO,IAAI,CAAC,EAAE,QAAQ,MAAM,eAAe;AAAA,YACzC;AAAA,YACA,MAAM,KAAK,KAAK,GAAG;AAAA,YACnB;AAAA,UAAA,EACA;AAAA,QAAA;AAAA,MAEN;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;"}
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const React = require("react");
5
+ const react = require("@mathscan/geometry-engine/react");
6
+ const validation = require("@mathscan/geometry-engine/validation");
7
+ const index = require("./index-D-QbiBn5.js");
8
+ const GeometryWidget = ({
9
+ widgetId,
10
+ widget,
11
+ value,
12
+ validationDetail,
13
+ onChange
14
+ }) => {
15
+ const exerciseLocked = index.useExerciseLocked();
16
+ const { attempt, issues } = React.useMemo(() => index.readGeometryAttempt(value), [value]);
17
+ React.useEffect(() => {
18
+ if (issues) index.logRejectedGeometryAttempt(widgetId, issues);
19
+ }, [issues, widgetId]);
20
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "geometry-widget", "data-widget-id": widgetId, children: /* @__PURE__ */ jsxRuntime.jsx(
21
+ react.StudentGeometryExercise,
22
+ {
23
+ showCompanion: false,
24
+ exercise: widget.json,
25
+ value: attempt,
26
+ readOnly: exerciseLocked,
27
+ validationFeedback: validationDetail?.geometryFeedback ?? null,
28
+ controls: {
29
+ toolbar: false,
30
+ history: false,
31
+ delete: false,
32
+ reset: false,
33
+ instruments: false
34
+ },
35
+ onChange: (next) => onChange(widgetId, validation.serializeGeometryState(next)),
36
+ onError: (issues2) => {
37
+ console.error(
38
+ `Geometry exercise rejected (widget "${widgetId}"):`,
39
+ issues2.map(({ source, path, message }) => ({
40
+ source,
41
+ path: path.join("."),
42
+ message
43
+ }))
44
+ );
45
+ }
46
+ }
47
+ ) });
48
+ };
49
+ exports.GeometryWidget = GeometryWidget;
50
+ //# sourceMappingURL=GeometryWidget-D2QyGzW-.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeometryWidget-D2QyGzW-.js","sources":["../src/renderer/widgets/geometry/GeometryWidget.tsx"],"sourcesContent":["import React, { useEffect, useMemo } from 'react';\nimport { StudentGeometryExercise } from '@mathscan/geometry-engine/react';\nimport { serializeGeometryState } from '@mathscan/geometry-engine/validation';\nimport type { GeometryJSON } from '../../../transformers/zod-schemas';\nimport type { WidgetValidation } from '../../../validators';\nimport { logRejectedGeometryAttempt, readGeometryAttempt } from '../../../validators';\nimport { useExerciseLocked } from '../../lock';\n\nexport interface GeometryWidgetProps {\n widgetId: string;\n widget: GeometryJSON;\n /** Serialized `GeometryState` — the value this package stores and validates. */\n value?: string;\n validationDetail?: WidgetValidation;\n onChange: (widgetId: string, value: string) => void;\n}\n\n/**\n * Adapter between this package's widget contract and the geometry engine's\n * student component. It owns no geometry logic: the attempt is passed through as\n * data, the engine's verdict is passed back untouched, and the canvas itself\n * decides what to draw and when a result has gone stale.\n */\nexport const GeometryWidget: React.FC<GeometryWidgetProps> = ({\n widgetId,\n widget,\n value,\n validationDetail,\n onChange,\n}) => {\n const exerciseLocked = useExerciseLocked();\n // Parsed per value change: the component is controlled, and handing it the\n // same attempt as a fresh object on every render would look like a new value.\n const { attempt, issues } = useMemo(() => readGeometryAttempt(value), [value]);\n\n // A stored value the engine refuses is shown as an empty canvas, so the\n // student's saved work silently vanishes unless this says so — once per value.\n useEffect(() => {\n if (issues) logRejectedGeometryAttempt(widgetId, issues);\n }, [issues, widgetId]);\n\n return (\n <div className=\"geometry-widget\" data-widget-id={widgetId}>\n <StudentGeometryExercise\n showCompanion={false}\n exercise={widget.json}\n value={attempt}\n readOnly={exerciseLocked}\n validationFeedback={validationDetail?.geometryFeedback ?? null}\n // Reset belongs to the exercise, which clears every widget at once.\n controls={{\n toolbar: false,\n history: false,\n delete: false,\n reset: false,\n instruments: false,\n }}\n onChange={(next) => onChange(widgetId, serializeGeometryState(next))}\n // The engine renders its own error box for a rejected exercise or\n // value; this is the only place the reasons surface for an author.\n onError={(issues) => {\n console.error(\n `Geometry exercise rejected (widget \"${widgetId}\"):`,\n issues.map(({ source, path, message }) => ({\n source,\n path: path.join('.'),\n message,\n }))\n );\n }}\n />\n </div>\n );\n};\n"],"names":["useExerciseLocked","useMemo","readGeometryAttempt","useEffect","logRejectedGeometryAttempt","jsx","StudentGeometryExercise","serializeGeometryState","issues"],"mappings":";;;;;;;AAuBO,MAAM,iBAAgD,CAAC;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,iBAAiBA,MAAAA,kBAAA;AAGvB,QAAM,EAAE,SAAS,OAAA,IAAWC,MAAAA,QAAQ,MAAMC,MAAAA,oBAAoB,KAAK,GAAG,CAAC,KAAK,CAAC;AAI7EC,QAAAA,UAAU,MAAM;AACd,QAAI,OAAQC,OAAAA,2BAA2B,UAAU,MAAM;AAAA,EACzD,GAAG,CAAC,QAAQ,QAAQ,CAAC;AAErB,SACEC,2BAAAA,IAAC,OAAA,EAAI,WAAU,mBAAkB,kBAAgB,UAC/C,UAAAA,2BAAAA;AAAAA,IAACC,MAAAA;AAAAA,IAAA;AAAA,MACC,eAAe;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,OAAO;AAAA,MACP,UAAU;AAAA,MACV,oBAAoB,kBAAkB,oBAAoB;AAAA,MAE1D,UAAU;AAAA,QACR,SAAS;AAAA,QACT,SAAS;AAAA,QACT,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,aAAa;AAAA,MAAA;AAAA,MAEf,UAAU,CAAC,SAAS,SAAS,UAAUC,WAAAA,uBAAuB,IAAI,CAAC;AAAA,MAGnE,SAAS,CAACC,YAAW;AACnB,gBAAQ;AAAA,UACN,uCAAuC,QAAQ;AAAA,UAC/CA,QAAO,IAAI,CAAC,EAAE,QAAQ,MAAM,eAAe;AAAA,YACzC;AAAA,YACA,MAAM,KAAK,KAAK,GAAG;AAAA,YACnB;AAAA,UAAA,EACA;AAAA,QAAA;AAAA,MAEN;AAAA,IAAA;AAAA,EAAA,GAEJ;AAEJ;;"}