@khanacademy/perseus-editor 3.0.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/components/widget-editor.d.ts +1 -1
- package/dist/es/index.js +131 -901
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +129 -894
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/i18n.test.ts +1 -85
- package/src/all-editors.ts +0 -2
- package/src/i18n.ts +0 -2
- package/src/index.ts +2 -3
- package/src/util/register-all-widgets-and-editors-for-testing.test.ts +1 -3
- package/src/util/register-all-widgets-and-editors-for-testing.ts +2 -3
- package/tsconfig-build.tsbuildinfo +1 -1
- package/dist/widgets/transformer-editor.d.ts +0 -11
- package/src/widgets/transformer-editor.tsx +0 -1007
|
@@ -1,1007 +0,0 @@
|
|
|
1
|
-
/* eslint-disable camelcase, react/sort-comp */
|
|
2
|
-
import {
|
|
3
|
-
line as kline,
|
|
4
|
-
number as knumber,
|
|
5
|
-
point as kpoint,
|
|
6
|
-
ray as kray,
|
|
7
|
-
vector as kvector,
|
|
8
|
-
} from "@khanacademy/kmath";
|
|
9
|
-
import {
|
|
10
|
-
components,
|
|
11
|
-
ApiOptions,
|
|
12
|
-
Errors,
|
|
13
|
-
KhanColors,
|
|
14
|
-
PerseusError,
|
|
15
|
-
TransformerWidget,
|
|
16
|
-
Util,
|
|
17
|
-
} from "@khanacademy/perseus";
|
|
18
|
-
import * as React from "react";
|
|
19
|
-
import _ from "underscore";
|
|
20
|
-
|
|
21
|
-
import GraphSettings from "../components/graph-settings";
|
|
22
|
-
|
|
23
|
-
const {Graph, InfoTip, PropCheckBox} = components;
|
|
24
|
-
const Transformer = TransformerWidget.widget;
|
|
25
|
-
const {deepEq, getGridStep} = Util;
|
|
26
|
-
|
|
27
|
-
function arraySum(array) {
|
|
28
|
-
return _.reduce(
|
|
29
|
-
array,
|
|
30
|
-
function (memo, arg) {
|
|
31
|
-
return memo + arg;
|
|
32
|
-
},
|
|
33
|
-
0,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const defaultBackgroundImage = {
|
|
38
|
-
url: null,
|
|
39
|
-
} as const;
|
|
40
|
-
|
|
41
|
-
/* Scales a distance from the default range of
|
|
42
|
-
* [-10, 10] to a given props.range pair
|
|
43
|
-
*
|
|
44
|
-
* Used for sizing various transformation tools
|
|
45
|
-
* (rotation handle, dilation circle)
|
|
46
|
-
*/
|
|
47
|
-
function scaleToRange(dist: number, range: any) {
|
|
48
|
-
const spreadX = range[0][1] - range[0][0];
|
|
49
|
-
const spreadY = range[1][1] - range[1][0];
|
|
50
|
-
|
|
51
|
-
return (dist * Math.max(spreadX, spreadY)) / 20;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function orderInsensitiveCoordsEqual(coords1: any, coords2: any) {
|
|
55
|
-
coords1 = _.clone(coords1).sort(kpoint.compare);
|
|
56
|
-
coords2 = _.clone(coords2).sort(kpoint.compare);
|
|
57
|
-
return _.all(
|
|
58
|
-
_.map(coords1, function (coord1, i) {
|
|
59
|
-
const coord2 = coords2[i];
|
|
60
|
-
return kpoint.equal(coord1, coord2);
|
|
61
|
-
}),
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const defaultGraphProps = function (setProps: any, boxSize: number) {
|
|
66
|
-
setProps = setProps || {};
|
|
67
|
-
const labels = setProps.labels || ["x", "y"];
|
|
68
|
-
const range = setProps.range || [
|
|
69
|
-
[-10, 10],
|
|
70
|
-
[-10, 10],
|
|
71
|
-
];
|
|
72
|
-
const step = setProps.step || [1, 1];
|
|
73
|
-
const gridStep = setProps.gridStep || getGridStep(range, step, boxSize);
|
|
74
|
-
return {
|
|
75
|
-
box: [boxSize, boxSize],
|
|
76
|
-
labels: labels,
|
|
77
|
-
range: range,
|
|
78
|
-
step: step,
|
|
79
|
-
gridStep: gridStep,
|
|
80
|
-
valid: true,
|
|
81
|
-
backgroundImage: defaultBackgroundImage,
|
|
82
|
-
markings: "grid",
|
|
83
|
-
showProtractor: false,
|
|
84
|
-
};
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const defaultTransformerProps = {
|
|
88
|
-
apiOptions: ApiOptions.defaults,
|
|
89
|
-
gradeEmpty: false,
|
|
90
|
-
graphMode: "interactive",
|
|
91
|
-
listMode: "dynamic",
|
|
92
|
-
graph: {},
|
|
93
|
-
tools: {
|
|
94
|
-
translation: {
|
|
95
|
-
enabled: true,
|
|
96
|
-
required: false,
|
|
97
|
-
constraints: {},
|
|
98
|
-
},
|
|
99
|
-
rotation: {
|
|
100
|
-
enabled: true,
|
|
101
|
-
required: false,
|
|
102
|
-
constraints: {
|
|
103
|
-
fixed: false,
|
|
104
|
-
},
|
|
105
|
-
coord: [1, 6],
|
|
106
|
-
},
|
|
107
|
-
reflection: {
|
|
108
|
-
enabled: true,
|
|
109
|
-
required: false,
|
|
110
|
-
constraints: {
|
|
111
|
-
fixed: false,
|
|
112
|
-
},
|
|
113
|
-
coords: [
|
|
114
|
-
[2, -4],
|
|
115
|
-
[2, 2],
|
|
116
|
-
],
|
|
117
|
-
},
|
|
118
|
-
dilation: {
|
|
119
|
-
enabled: true,
|
|
120
|
-
required: false,
|
|
121
|
-
constraints: {
|
|
122
|
-
fixed: false,
|
|
123
|
-
},
|
|
124
|
-
coord: [6, 6],
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
drawSolutionShape: true,
|
|
128
|
-
starting: {
|
|
129
|
-
shape: {
|
|
130
|
-
type: "polygon-3",
|
|
131
|
-
coords: [
|
|
132
|
-
[2, 2],
|
|
133
|
-
[2, 6],
|
|
134
|
-
[7, 2],
|
|
135
|
-
],
|
|
136
|
-
},
|
|
137
|
-
transformations: [],
|
|
138
|
-
},
|
|
139
|
-
correct: {
|
|
140
|
-
shape: {
|
|
141
|
-
type: "polygon-3",
|
|
142
|
-
coords: [
|
|
143
|
-
[2, 2],
|
|
144
|
-
[2, 6],
|
|
145
|
-
[7, 2],
|
|
146
|
-
],
|
|
147
|
-
},
|
|
148
|
-
transformations: [],
|
|
149
|
-
},
|
|
150
|
-
} as const;
|
|
151
|
-
|
|
152
|
-
class ToolSettings extends React.Component<any> {
|
|
153
|
-
static defaultProps = {
|
|
154
|
-
allowFixed: true,
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
render(): React.ReactNode {
|
|
158
|
-
return (
|
|
159
|
-
<div>
|
|
160
|
-
{this.props.name}:{" "}
|
|
161
|
-
<PropCheckBox
|
|
162
|
-
label="enabled:"
|
|
163
|
-
enabled={this.props.settings.enabled}
|
|
164
|
-
onChange={this.props.onChange}
|
|
165
|
-
/>{" "}
|
|
166
|
-
{this.props.settings.enabled && (
|
|
167
|
-
<PropCheckBox
|
|
168
|
-
label="required:"
|
|
169
|
-
required={this.props.settings.required}
|
|
170
|
-
onChange={this.props.onChange}
|
|
171
|
-
/>
|
|
172
|
-
)}
|
|
173
|
-
{this.props.settings.enabled && (
|
|
174
|
-
<InfoTip>
|
|
175
|
-
'Required' will only grade the answer as correct if the
|
|
176
|
-
student has used at least one such transformation.
|
|
177
|
-
</InfoTip>
|
|
178
|
-
)}{" "}
|
|
179
|
-
{this.props.allowFixed && this.props.settings.enabled && (
|
|
180
|
-
<PropCheckBox
|
|
181
|
-
label="fixed:"
|
|
182
|
-
fixed={this.props.settings.constraints.fixed}
|
|
183
|
-
onChange={this.changeConstraints}
|
|
184
|
-
/>
|
|
185
|
-
)}
|
|
186
|
-
{this.props.allowFixed && this.props.settings.enabled && (
|
|
187
|
-
<InfoTip>
|
|
188
|
-
Enable 'fixed' to prevent the student from repositioning
|
|
189
|
-
the tool. The tool will appear in the position at which
|
|
190
|
-
it is placed in the editor below.
|
|
191
|
-
</InfoTip>
|
|
192
|
-
)}
|
|
193
|
-
</div>
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
changeConstraints = (changed) => {
|
|
198
|
-
const newConstraints = _.extend({}, this.props.constraints, changed);
|
|
199
|
-
this.props.onChange({
|
|
200
|
-
constraints: newConstraints,
|
|
201
|
-
});
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
class TransformationExplorerSettings extends React.Component<any> {
|
|
206
|
-
render(): React.ReactNode {
|
|
207
|
-
return (
|
|
208
|
-
<div className="transformer-settings">
|
|
209
|
-
<div>
|
|
210
|
-
{" "}
|
|
211
|
-
Mode:{" "}
|
|
212
|
-
<select value={this.getMode()} onChange={this.changeMode}>
|
|
213
|
-
<option value="interactive,dynamic">
|
|
214
|
-
{" "}
|
|
215
|
-
Exploration with text{" "}
|
|
216
|
-
</option>
|
|
217
|
-
<option value="interactive,static">
|
|
218
|
-
{" "}
|
|
219
|
-
Exploration without text{" "}
|
|
220
|
-
</option>
|
|
221
|
-
<option value="dynamic,interactive">
|
|
222
|
-
{" "}
|
|
223
|
-
Formal with movement{" "}
|
|
224
|
-
</option>
|
|
225
|
-
<option value="static,interactive">
|
|
226
|
-
{" "}
|
|
227
|
-
Formal without movement{" "}
|
|
228
|
-
</option>
|
|
229
|
-
</select>
|
|
230
|
-
<InfoTip>
|
|
231
|
-
<ul>
|
|
232
|
-
<li>
|
|
233
|
-
<b>Exploration:</b> Students create
|
|
234
|
-
transformations with tools on the graph.{" "}
|
|
235
|
-
</li>
|
|
236
|
-
<li>
|
|
237
|
-
<b>Formal with movement:</b> Students specify
|
|
238
|
-
transformations mathematically in the
|
|
239
|
-
transformation list. Graph shows the results of
|
|
240
|
-
these transformations.{" "}
|
|
241
|
-
</li>
|
|
242
|
-
<li>
|
|
243
|
-
<b>Formal without movement:</b> Students specify
|
|
244
|
-
transformations mathematically in the
|
|
245
|
-
transformation list. Graph does not update.{" "}
|
|
246
|
-
</li>
|
|
247
|
-
</ul>
|
|
248
|
-
</InfoTip>
|
|
249
|
-
</div>
|
|
250
|
-
<ToolSettings
|
|
251
|
-
name="Translations"
|
|
252
|
-
settings={this.props.tools.translation}
|
|
253
|
-
allowFixed={false}
|
|
254
|
-
onChange={this.changeHandlerFor("translation")}
|
|
255
|
-
/>
|
|
256
|
-
<ToolSettings
|
|
257
|
-
name="Rotations"
|
|
258
|
-
settings={this.props.tools.rotation}
|
|
259
|
-
onChange={this.changeHandlerFor("rotation")}
|
|
260
|
-
/>
|
|
261
|
-
<ToolSettings
|
|
262
|
-
name="Reflections"
|
|
263
|
-
settings={this.props.tools.reflection}
|
|
264
|
-
onChange={this.changeHandlerFor("reflection")}
|
|
265
|
-
/>
|
|
266
|
-
<ToolSettings
|
|
267
|
-
name="Dilations"
|
|
268
|
-
settings={this.props.tools.dilation}
|
|
269
|
-
onChange={this.changeHandlerFor("dilation")}
|
|
270
|
-
/>
|
|
271
|
-
<PropCheckBox
|
|
272
|
-
label="Draw Solution:"
|
|
273
|
-
drawSolutionShape={this.props.drawSolutionShape}
|
|
274
|
-
onChange={this.props.onChange}
|
|
275
|
-
/>
|
|
276
|
-
</div>
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
getMode = () => {
|
|
281
|
-
return this.props.graphMode + "," + this.props.listMode;
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
changeMode = (e) => {
|
|
285
|
-
const selected = e.target.value;
|
|
286
|
-
const modes = selected.split(",");
|
|
287
|
-
|
|
288
|
-
this.props.onChange({
|
|
289
|
-
graphMode: modes[0],
|
|
290
|
-
listMode: modes[1],
|
|
291
|
-
});
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
changeHandlerFor = (toolName: string) => {
|
|
295
|
-
return (change: any) => {
|
|
296
|
-
const newTools = _.clone(this.props.tools);
|
|
297
|
-
newTools[toolName] = _.extend(
|
|
298
|
-
{},
|
|
299
|
-
this.props.tools[toolName],
|
|
300
|
-
change,
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
this.props.onChange({
|
|
304
|
-
tools: newTools,
|
|
305
|
-
});
|
|
306
|
-
};
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const ShapeTypes: any = {
|
|
311
|
-
getPointCountForType: function (type) {
|
|
312
|
-
const splitType = type.split("-");
|
|
313
|
-
if (splitType[0] === "polygon") {
|
|
314
|
-
return splitType[1] || 3;
|
|
315
|
-
}
|
|
316
|
-
if (splitType[0] === "line" || splitType[0] === "lineSegment") {
|
|
317
|
-
return 2;
|
|
318
|
-
}
|
|
319
|
-
if (splitType[0] === "angle") {
|
|
320
|
-
return 3;
|
|
321
|
-
}
|
|
322
|
-
if (splitType[0] === "circle") {
|
|
323
|
-
return 2;
|
|
324
|
-
}
|
|
325
|
-
if (splitType[0] === "point") {
|
|
326
|
-
return 1;
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
|
|
330
|
-
addMovableShape: function (graphie, options) {
|
|
331
|
-
if (options.editable && options.translatable) {
|
|
332
|
-
throw new PerseusError(
|
|
333
|
-
"It doesn't make sense to have a movable shape " +
|
|
334
|
-
"where you can stretch the points and translate them " +
|
|
335
|
-
"simultaneously.",
|
|
336
|
-
Errors.InvalidInput,
|
|
337
|
-
{metadata: {options}},
|
|
338
|
-
);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const points = _.map(options.shape.coords, function (coord) {
|
|
342
|
-
let isMoving = false;
|
|
343
|
-
let previousCoord = coord;
|
|
344
|
-
|
|
345
|
-
const onMove = function (x: any, y) {
|
|
346
|
-
if (!isMoving) {
|
|
347
|
-
previousCoord = currentPoint.coord;
|
|
348
|
-
isMoving = true;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
let moveVector = kvector.subtract([x, y], currentPoint.coord);
|
|
352
|
-
|
|
353
|
-
// Translate from (x, y) semantics to (dX, dY) semantics
|
|
354
|
-
// This is more useful for translations on multiple points,
|
|
355
|
-
// where we care about how the points moved, not where any
|
|
356
|
-
// individual point ended up
|
|
357
|
-
if (options.onMove) {
|
|
358
|
-
moveVector = options.onMove(moveVector[0], moveVector[1]);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Perform a translation on all points in this shape when
|
|
362
|
-
// any point moves
|
|
363
|
-
if (options.translatable) {
|
|
364
|
-
_.each(points, function (point) {
|
|
365
|
-
// The point itself will be updated by the
|
|
366
|
-
// movablePoint class, so only translate the other
|
|
367
|
-
// points
|
|
368
|
-
if (point !== currentPoint) {
|
|
369
|
-
point.setCoord(
|
|
370
|
-
kvector.add(point.coord, moveVector),
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Update our shape and our currentPoint
|
|
377
|
-
// Without this, some shapes (circles, angles) appear
|
|
378
|
-
// "bouncy" as they are updated with currentPoint at the
|
|
379
|
-
// current mouse coordinate (oldCoord), rather than newCoord
|
|
380
|
-
const oldCoord = currentPoint.coord;
|
|
381
|
-
const newCoord = kvector.add(currentPoint.coord, moveVector);
|
|
382
|
-
// Temporarily change our coordinate so that
|
|
383
|
-
// shape.update() sees the new coordinate
|
|
384
|
-
currentPoint.coord = newCoord;
|
|
385
|
-
shape.update();
|
|
386
|
-
// ...But don't break onMove, which assumes it
|
|
387
|
-
// is the only thing changing our coord
|
|
388
|
-
currentPoint.coord = oldCoord;
|
|
389
|
-
return newCoord;
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
const onMoveEnd = function () {
|
|
393
|
-
// onMove isn't guaranteed to be called before onMoveEnd, so
|
|
394
|
-
// we have to take into account that we may not have moved and
|
|
395
|
-
// set previousCoord.
|
|
396
|
-
if (options.onMoveEnd && isMoving) {
|
|
397
|
-
isMoving = false;
|
|
398
|
-
// We don't use the supplied x and y parameters here
|
|
399
|
-
// because MovablePoint's onMoveEnd semantics suck.
|
|
400
|
-
// It returns the mouseX, mouseY without processing them
|
|
401
|
-
// through onMove, leaving us with weird fractional moves
|
|
402
|
-
const change = kvector.subtract(
|
|
403
|
-
currentPoint.coord,
|
|
404
|
-
previousCoord,
|
|
405
|
-
);
|
|
406
|
-
options.onMoveEnd(change[0], change[1]);
|
|
407
|
-
}
|
|
408
|
-
shape.update();
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
const currentPoint = graphie.addMovablePoint({
|
|
412
|
-
coord: coord,
|
|
413
|
-
normalStyle: options.normalPointStyle,
|
|
414
|
-
highlightStyle: options.highlightPointStyle,
|
|
415
|
-
constraints: {
|
|
416
|
-
fixed: !options.translatable && !options.editable,
|
|
417
|
-
},
|
|
418
|
-
visible: options.showPoints,
|
|
419
|
-
snapX: (options.snap && options.snap[0]) || 0,
|
|
420
|
-
snapY: (options.snap && options.snap[1]) || 0,
|
|
421
|
-
bounded: false, // Don't bound it when placing it on the graph
|
|
422
|
-
onMove: onMove,
|
|
423
|
-
onMoveEnd: onMoveEnd,
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
// Bound it when moving
|
|
427
|
-
// We can't set this earlier, because doing so would mean any
|
|
428
|
-
// points outside of the graph would be moved into a moved into
|
|
429
|
-
// a position that doesn't preserve the shape
|
|
430
|
-
currentPoint.bounded = true;
|
|
431
|
-
|
|
432
|
-
return currentPoint;
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
const shape = ShapeTypes.addShape(graphie, options, points);
|
|
436
|
-
const removeShapeWithoutPoints = shape.remove;
|
|
437
|
-
shape.remove = function () {
|
|
438
|
-
removeShapeWithoutPoints.apply(shape);
|
|
439
|
-
_.invoke(points, "remove");
|
|
440
|
-
};
|
|
441
|
-
return shape;
|
|
442
|
-
},
|
|
443
|
-
|
|
444
|
-
addShape: function (graphie, options, points) {
|
|
445
|
-
points = points || options.shape.coords;
|
|
446
|
-
|
|
447
|
-
const types = ShapeTypes._typesOf(options.shape);
|
|
448
|
-
const typeOptions =
|
|
449
|
-
options.shape.options || ShapeTypes.defaultOptions(types);
|
|
450
|
-
|
|
451
|
-
const shapes = ShapeTypes._mapTypes(
|
|
452
|
-
types,
|
|
453
|
-
points,
|
|
454
|
-
function (type, points, i) {
|
|
455
|
-
const shapeOptions = _.extend({}, options, typeOptions[i]);
|
|
456
|
-
return ShapeTypes._addType(graphie, type, points, shapeOptions);
|
|
457
|
-
},
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
const updateFuncs = _.filter(_.pluck(shapes, "update"), _.identity);
|
|
461
|
-
const update = function () {
|
|
462
|
-
_.invoke(updateFuncs, "call");
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
const removeFuncs = _.filter(_.pluck(shapes, "remove"), _.identity);
|
|
466
|
-
const remove = function () {
|
|
467
|
-
_.invoke(removeFuncs, "call");
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
const getOptions = function () {
|
|
471
|
-
return _.map(shapes, function (shape) {
|
|
472
|
-
if (shape.getOptions) {
|
|
473
|
-
return shape.getOptions();
|
|
474
|
-
}
|
|
475
|
-
return {};
|
|
476
|
-
});
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
const toJSON = function () {
|
|
480
|
-
const coords = _.map(points, function (pt) {
|
|
481
|
-
if (_.isArray(pt)) {
|
|
482
|
-
return pt;
|
|
483
|
-
}
|
|
484
|
-
return pt.coord;
|
|
485
|
-
});
|
|
486
|
-
return {
|
|
487
|
-
type: types,
|
|
488
|
-
coords: coords,
|
|
489
|
-
options: getOptions(),
|
|
490
|
-
};
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
return {
|
|
494
|
-
type: types,
|
|
495
|
-
points: points,
|
|
496
|
-
update: update,
|
|
497
|
-
remove: remove,
|
|
498
|
-
toJSON: toJSON,
|
|
499
|
-
getOptions: getOptions,
|
|
500
|
-
};
|
|
501
|
-
},
|
|
502
|
-
|
|
503
|
-
equal: function (shape1, shape2) {
|
|
504
|
-
const types1 = ShapeTypes._typesOf(shape1);
|
|
505
|
-
const types2 = ShapeTypes._typesOf(shape2);
|
|
506
|
-
if (types1.length !== types2.length) {
|
|
507
|
-
return false;
|
|
508
|
-
}
|
|
509
|
-
const shapes1 = ShapeTypes._mapTypes(
|
|
510
|
-
types1,
|
|
511
|
-
shape1.coords,
|
|
512
|
-
ShapeTypes._combine,
|
|
513
|
-
);
|
|
514
|
-
const shapes2 = ShapeTypes._mapTypes(
|
|
515
|
-
types2,
|
|
516
|
-
shape2.coords,
|
|
517
|
-
ShapeTypes._combine,
|
|
518
|
-
);
|
|
519
|
-
return _.all(
|
|
520
|
-
_.map(shapes1, function (partialShape1, i) {
|
|
521
|
-
const partialShape2 = shapes2[i];
|
|
522
|
-
if (partialShape1.type !== partialShape2.type) {
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
525
|
-
return ShapeTypes._forType(partialShape1.type).equal(
|
|
526
|
-
partialShape1.coords,
|
|
527
|
-
partialShape2.coords,
|
|
528
|
-
);
|
|
529
|
-
}),
|
|
530
|
-
);
|
|
531
|
-
},
|
|
532
|
-
|
|
533
|
-
_typesOf: function (shape) {
|
|
534
|
-
let types = shape.type;
|
|
535
|
-
if (!_.isArray(types)) {
|
|
536
|
-
types = [types];
|
|
537
|
-
}
|
|
538
|
-
return _.map(types, function (type) {
|
|
539
|
-
if (type === "polygon") {
|
|
540
|
-
return "polygon-3";
|
|
541
|
-
}
|
|
542
|
-
return type;
|
|
543
|
-
});
|
|
544
|
-
},
|
|
545
|
-
|
|
546
|
-
defaultOptions: function (types) {
|
|
547
|
-
return _.map(types, function (type) {
|
|
548
|
-
const typeDefaultOptions = ShapeTypes._forType(type).defaultOptions;
|
|
549
|
-
return _.extend({}, typeDefaultOptions);
|
|
550
|
-
});
|
|
551
|
-
},
|
|
552
|
-
|
|
553
|
-
_forType: function (type) {
|
|
554
|
-
const baseType = type.split("-")[0];
|
|
555
|
-
return ShapeTypes[baseType];
|
|
556
|
-
},
|
|
557
|
-
|
|
558
|
-
_mapTypes: function (types, points, func, context) {
|
|
559
|
-
return _.map(types, function (type, i) {
|
|
560
|
-
const pointCount = ShapeTypes.getPointCountForType(type);
|
|
561
|
-
const currentPoints = _.first(points, pointCount);
|
|
562
|
-
points = _.rest(points, pointCount);
|
|
563
|
-
return func.call(context, type, currentPoints, i);
|
|
564
|
-
});
|
|
565
|
-
},
|
|
566
|
-
|
|
567
|
-
_addType: function (graphie, type, points, options) {
|
|
568
|
-
const lineCoords = _.isArray(points[0])
|
|
569
|
-
? {
|
|
570
|
-
coordA: points[0],
|
|
571
|
-
coordZ: points[1],
|
|
572
|
-
}
|
|
573
|
-
: {
|
|
574
|
-
pointA: points[0],
|
|
575
|
-
pointZ: points[1],
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
type = type.split("-")[0];
|
|
579
|
-
if (type === "polygon") {
|
|
580
|
-
const polygon = graphie.addMovablePolygon(
|
|
581
|
-
_.extend({}, options, {
|
|
582
|
-
fixed: !options.editable,
|
|
583
|
-
snapX: (options.snap && options.snap[0]) || 0,
|
|
584
|
-
snapY: (options.snap && options.snap[1]) || 0,
|
|
585
|
-
points: points,
|
|
586
|
-
constrainToGraph: false,
|
|
587
|
-
}),
|
|
588
|
-
);
|
|
589
|
-
return {
|
|
590
|
-
update: polygon.transform.bind(polygon),
|
|
591
|
-
remove: polygon.remove.bind(polygon),
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
if (type === "line" || type === "lineSegment") {
|
|
595
|
-
const line = graphie.addMovableLineSegment(
|
|
596
|
-
_.extend({}, options, lineCoords, {
|
|
597
|
-
movePointsWithLine: true,
|
|
598
|
-
fixed: true,
|
|
599
|
-
constraints: {
|
|
600
|
-
fixed: true,
|
|
601
|
-
},
|
|
602
|
-
extendLine: type === "line",
|
|
603
|
-
}),
|
|
604
|
-
);
|
|
605
|
-
|
|
606
|
-
// TODO(jack): Hide points on uneditable lines when translation
|
|
607
|
-
// is a vector.
|
|
608
|
-
// We can't just remove the points yet, because they are the
|
|
609
|
-
// translation handle for the line.
|
|
610
|
-
return {
|
|
611
|
-
update: line.transform.bind(line, true),
|
|
612
|
-
remove: line.remove.bind(line),
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
if (type === "angle") {
|
|
616
|
-
// If this angle is editable, we want to be able to make angles
|
|
617
|
-
// both larger and smaller than 180 degrees.
|
|
618
|
-
// If this angle is not editable, it should always maintain
|
|
619
|
-
// it's angle measure, even if it is reflected (causing the
|
|
620
|
-
// clockwise-ness of the points to change)
|
|
621
|
-
const shouldChangeReflexivity = options.editable ? null : false;
|
|
622
|
-
|
|
623
|
-
const angle = graphie.addMovableAngle({
|
|
624
|
-
angleLabel: "$deg0",
|
|
625
|
-
fixed: true,
|
|
626
|
-
points: points,
|
|
627
|
-
normalStyle: options.normalStyle,
|
|
628
|
-
reflex: options.reflex,
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
// Hide non-vertex points on uneditable angles
|
|
632
|
-
if (!_.isArray(points[0]) && !options.editable) {
|
|
633
|
-
points[0].remove();
|
|
634
|
-
points[2].remove();
|
|
635
|
-
}
|
|
636
|
-
return {
|
|
637
|
-
update: angle.update.bind(angle, shouldChangeReflexivity),
|
|
638
|
-
remove: angle.remove.bind(angle),
|
|
639
|
-
getOptions: function () {
|
|
640
|
-
return {
|
|
641
|
-
reflex: angle.isReflex(),
|
|
642
|
-
};
|
|
643
|
-
},
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
if (type === "circle") {
|
|
647
|
-
let perimeter = {
|
|
648
|
-
// temporary object for the first removal
|
|
649
|
-
remove: _.identity,
|
|
650
|
-
};
|
|
651
|
-
const redrawPerim = function () {
|
|
652
|
-
const coord0 = points[0].coord || points[0];
|
|
653
|
-
const coord1 = points[1].coord || points[1];
|
|
654
|
-
const radius = kpoint.distanceToPoint(coord0, coord1);
|
|
655
|
-
// @ts-expect-error - TS2554 - Expected 1 arguments, but got 0.
|
|
656
|
-
perimeter.remove();
|
|
657
|
-
perimeter = graphie.circle(
|
|
658
|
-
coord0,
|
|
659
|
-
radius,
|
|
660
|
-
_.extend(
|
|
661
|
-
{
|
|
662
|
-
stroke: KhanColors.DYNAMIC,
|
|
663
|
-
"stroke-width": 2,
|
|
664
|
-
},
|
|
665
|
-
options.normalStyle,
|
|
666
|
-
),
|
|
667
|
-
);
|
|
668
|
-
};
|
|
669
|
-
|
|
670
|
-
redrawPerim();
|
|
671
|
-
if (points[1].remove && !options.editable) {
|
|
672
|
-
points[1].remove();
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
return {
|
|
676
|
-
update: redrawPerim,
|
|
677
|
-
remove: function () {
|
|
678
|
-
// Not _.bind because the remove function changes
|
|
679
|
-
// when the perimeter is redrawn
|
|
680
|
-
// @ts-expect-error - TS2554 - Expected 1 arguments, but got 0.
|
|
681
|
-
perimeter.remove();
|
|
682
|
-
},
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
if (type === "point") {
|
|
686
|
-
// do nothing
|
|
687
|
-
return {
|
|
688
|
-
update: null,
|
|
689
|
-
remove: null,
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
throw new PerseusError(
|
|
693
|
-
"Invalid shape type " + type,
|
|
694
|
-
Errors.InvalidInput,
|
|
695
|
-
);
|
|
696
|
-
},
|
|
697
|
-
|
|
698
|
-
_combine: function (type, coords) {
|
|
699
|
-
return {
|
|
700
|
-
type: type,
|
|
701
|
-
coords: coords,
|
|
702
|
-
};
|
|
703
|
-
},
|
|
704
|
-
|
|
705
|
-
polygon: {
|
|
706
|
-
equal: orderInsensitiveCoordsEqual,
|
|
707
|
-
},
|
|
708
|
-
|
|
709
|
-
line: {
|
|
710
|
-
equal: kline.equal,
|
|
711
|
-
},
|
|
712
|
-
|
|
713
|
-
lineSegment: {
|
|
714
|
-
equal: orderInsensitiveCoordsEqual,
|
|
715
|
-
},
|
|
716
|
-
|
|
717
|
-
angle: {
|
|
718
|
-
equal: function (points1, points2) {
|
|
719
|
-
if (!kpoint.equal(points1[1], points2[1])) {
|
|
720
|
-
return false;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
const line1_0 = [points1[1], points1[0]];
|
|
724
|
-
const line1_2 = [points1[1], points1[2]];
|
|
725
|
-
const line2_0 = [points2[1], points2[0]];
|
|
726
|
-
const line2_2 = [points2[1], points2[2]];
|
|
727
|
-
|
|
728
|
-
const equalUnflipped =
|
|
729
|
-
// @ts-expect-error - TS2345 - Argument of type 'any[]' is not assignable to parameter of type 'Ray'. | TS2345 - Argument of type 'any[]' is not assignable to parameter of type 'Ray'.
|
|
730
|
-
kray.equal(line1_0, line2_0) && kray.equal(line1_2, line2_2);
|
|
731
|
-
const equalFlipped =
|
|
732
|
-
// @ts-expect-error - TS2345 - Argument of type 'any[]' is not assignable to parameter of type 'Ray'. | TS2345 - Argument of type 'any[]' is not assignable to parameter of type 'Ray'.
|
|
733
|
-
kray.equal(line1_0, line2_2) && kray.equal(line1_2, line2_0);
|
|
734
|
-
|
|
735
|
-
return equalUnflipped || equalFlipped;
|
|
736
|
-
},
|
|
737
|
-
|
|
738
|
-
defaultOptions: {
|
|
739
|
-
reflex: false,
|
|
740
|
-
},
|
|
741
|
-
},
|
|
742
|
-
|
|
743
|
-
circle: {
|
|
744
|
-
equal: function (points1, points2) {
|
|
745
|
-
const radius1 = kpoint.distanceToPoint(points1[0], points1[1]);
|
|
746
|
-
const radius2 = kpoint.distanceToPoint(points2[0], points2[1]);
|
|
747
|
-
return (
|
|
748
|
-
kpoint.equal(points1[0], points2[0]) &&
|
|
749
|
-
knumber.equal(radius1, radius2)
|
|
750
|
-
);
|
|
751
|
-
},
|
|
752
|
-
},
|
|
753
|
-
|
|
754
|
-
point: {
|
|
755
|
-
equal: kpoint.equal,
|
|
756
|
-
},
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
class TransformationsShapeEditor extends React.Component<any> {
|
|
760
|
-
render(): React.ReactNode {
|
|
761
|
-
return (
|
|
762
|
-
<div>
|
|
763
|
-
<Graph
|
|
764
|
-
// eslint-disable-next-line react/no-string-refs
|
|
765
|
-
ref="graph"
|
|
766
|
-
box={this.props.graph.box}
|
|
767
|
-
range={this.props.graph.range}
|
|
768
|
-
labels={this.props.graph.labels}
|
|
769
|
-
step={this.props.graph.step}
|
|
770
|
-
gridStep={this.props.graph.gridStep}
|
|
771
|
-
markings={this.props.graph.markings}
|
|
772
|
-
backgroundImage={this.props.graph.backgroundImage}
|
|
773
|
-
onGraphieUpdated={this.setupGraphie}
|
|
774
|
-
/>
|
|
775
|
-
<select
|
|
776
|
-
key="type-select"
|
|
777
|
-
value={this.getTypeString(this.props.shape.type)}
|
|
778
|
-
onChange={this.changeType}
|
|
779
|
-
>
|
|
780
|
-
<option value="polygon-3">Triangle</option>
|
|
781
|
-
<option value="polygon-4">Quadrilateral</option>
|
|
782
|
-
<option value="polygon-5">Pentagon</option>
|
|
783
|
-
<option value="polygon-6">Hexagon</option>
|
|
784
|
-
<option value="line">Line</option>
|
|
785
|
-
<option value="line,line">2 lines</option>
|
|
786
|
-
<option value="lineSegment">Line segment</option>
|
|
787
|
-
<option value="lineSegment,lineSegment">
|
|
788
|
-
{" "}
|
|
789
|
-
2 line segments{" "}
|
|
790
|
-
</option>
|
|
791
|
-
<option value="angle">Angle</option>
|
|
792
|
-
<option value="circle">Circle</option>
|
|
793
|
-
</select>
|
|
794
|
-
</div>
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
/* Return the option string for a given type */
|
|
799
|
-
getTypeString = (type: any) => {
|
|
800
|
-
if (_.isArray(type)) {
|
|
801
|
-
return _.map(type, this.getTypeString).join(",");
|
|
802
|
-
}
|
|
803
|
-
if (type === "polygon") {
|
|
804
|
-
return "polygon-" + this.props.shape.coords.length;
|
|
805
|
-
}
|
|
806
|
-
return type;
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
/* Change the type on the window event e
|
|
810
|
-
*
|
|
811
|
-
* e.target.value is the new type string
|
|
812
|
-
*/
|
|
813
|
-
changeType = (e: any) => {
|
|
814
|
-
const types = String(e.target.value).split(",");
|
|
815
|
-
const pointCount = arraySum(
|
|
816
|
-
_.map(types, ShapeTypes.getPointCountForType),
|
|
817
|
-
);
|
|
818
|
-
|
|
819
|
-
// eslint-disable-next-line react/no-string-refs
|
|
820
|
-
// @ts-expect-error - TS2339 - Property 'props' does not exist on type 'ReactInstance'.
|
|
821
|
-
const radius = scaleToRange(4, this.refs.graph.props.range);
|
|
822
|
-
const offset = (1 / 2 - 1 / pointCount) * 180;
|
|
823
|
-
const coords = _.times(pointCount, function (i) {
|
|
824
|
-
return kpoint.rotateDeg(
|
|
825
|
-
[radius, 0],
|
|
826
|
-
(360 * i) / pointCount + offset,
|
|
827
|
-
);
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
this.props.onChange({
|
|
831
|
-
shape: {
|
|
832
|
-
type: types,
|
|
833
|
-
coords: coords,
|
|
834
|
-
options: ShapeTypes.defaultOptions(types),
|
|
835
|
-
},
|
|
836
|
-
});
|
|
837
|
-
};
|
|
838
|
-
|
|
839
|
-
componentDidMount() {
|
|
840
|
-
// eslint-disable-next-line react/no-string-refs
|
|
841
|
-
// @ts-expect-error - TS2339 - Property 'graphie' does not exist on type 'ReactInstance'.
|
|
842
|
-
this.setupGraphie(this.refs.graph.graphie());
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
componentDidUpdate(prevProps) {
|
|
846
|
-
if (!deepEq(prevProps.shape, this.props.shape)) {
|
|
847
|
-
// eslint-disable-next-line react/no-string-refs
|
|
848
|
-
// @ts-expect-error - TS2339 - Property 'reset' does not exist on type 'ReactInstance'.
|
|
849
|
-
this.refs.graph.reset();
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
updateCoords = () => {
|
|
854
|
-
this.props.onChange({
|
|
855
|
-
// @ts-expect-error - TS2339 - Property 'shape' does not exist on type 'TransformationsShapeEditor'.
|
|
856
|
-
shape: this.shape.toJSON(),
|
|
857
|
-
});
|
|
858
|
-
};
|
|
859
|
-
|
|
860
|
-
setupGraphie = (graphie: any) => {
|
|
861
|
-
// @ts-expect-error - TS2339 - Property 'shape' does not exist on type 'TransformationsShapeEditor'.
|
|
862
|
-
this.shape = ShapeTypes.addMovableShape(graphie, {
|
|
863
|
-
editable: true,
|
|
864
|
-
snap: graphie.snap,
|
|
865
|
-
shape: this.props.shape,
|
|
866
|
-
onMoveEnd: this.updateCoords,
|
|
867
|
-
});
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
class TransformerEditor extends React.Component<any> {
|
|
872
|
-
// TODO (jack): These should be refactored into a nice object at the top
|
|
873
|
-
// so that we don't have all this duplication
|
|
874
|
-
static defaultProps: any = defaultTransformerProps;
|
|
875
|
-
|
|
876
|
-
static widgetName = "transformer" as const;
|
|
877
|
-
|
|
878
|
-
render(): React.ReactNode {
|
|
879
|
-
// Fill in any missing value in this.props.graph
|
|
880
|
-
// this can happen because the graph json doesn't include
|
|
881
|
-
// box, for example
|
|
882
|
-
const graph = _.extend(
|
|
883
|
-
defaultGraphProps(this.props.graph, 340),
|
|
884
|
-
this.props.graph,
|
|
885
|
-
);
|
|
886
|
-
|
|
887
|
-
return (
|
|
888
|
-
<div>
|
|
889
|
-
<div>
|
|
890
|
-
<PropCheckBox
|
|
891
|
-
label="Grade empty answers as wrong:"
|
|
892
|
-
gradeEmpty={this.props.gradeEmpty}
|
|
893
|
-
onChange={this.props.onChange}
|
|
894
|
-
/>
|
|
895
|
-
<InfoTip>
|
|
896
|
-
<p>
|
|
897
|
-
We generally do not grade empty answers. This
|
|
898
|
-
usually works well, but sometimes can result in
|
|
899
|
-
giving away part of an answer in a multi-part
|
|
900
|
-
question.
|
|
901
|
-
</p>
|
|
902
|
-
<p>
|
|
903
|
-
If this is a multi-part question (there is another
|
|
904
|
-
widget), you probably want to enable this option.
|
|
905
|
-
Otherwise, you should leave it disabled.
|
|
906
|
-
</p>
|
|
907
|
-
<p>Confused? Talk to Elizabeth.</p>
|
|
908
|
-
</InfoTip>
|
|
909
|
-
</div>
|
|
910
|
-
<div>Graph settings:</div>
|
|
911
|
-
<GraphSettings
|
|
912
|
-
box={graph.box}
|
|
913
|
-
labels={graph.labels}
|
|
914
|
-
range={graph.range}
|
|
915
|
-
step={graph.step}
|
|
916
|
-
gridStep={graph.gridStep}
|
|
917
|
-
valid={graph.valid}
|
|
918
|
-
backgroundImage={graph.backgroundImage}
|
|
919
|
-
markings={graph.markings}
|
|
920
|
-
showProtractor={graph.showProtractor}
|
|
921
|
-
onChange={this.changeGraph}
|
|
922
|
-
/>
|
|
923
|
-
<div>Transformation settings:</div>
|
|
924
|
-
<TransformationExplorerSettings
|
|
925
|
-
// eslint-disable-next-line react/no-string-refs
|
|
926
|
-
ref="transformationSettings"
|
|
927
|
-
graphMode={this.props.graphMode}
|
|
928
|
-
listMode={this.props.listMode}
|
|
929
|
-
tools={this.props.tools}
|
|
930
|
-
drawSolutionShape={this.props.drawSolutionShape}
|
|
931
|
-
onChange={this.props.onChange}
|
|
932
|
-
/>
|
|
933
|
-
<div>Starting location:</div>
|
|
934
|
-
<TransformationsShapeEditor
|
|
935
|
-
// eslint-disable-next-line react/no-string-refs
|
|
936
|
-
ref="shapeEditor"
|
|
937
|
-
graph={graph}
|
|
938
|
-
shape={this.props.starting.shape}
|
|
939
|
-
onChange={this.changeStarting}
|
|
940
|
-
setDrawingAreaAvailable={
|
|
941
|
-
this.props.apiOptions.setDrawingAreaAvailable
|
|
942
|
-
}
|
|
943
|
-
/>
|
|
944
|
-
<div>Solution transformations:</div>
|
|
945
|
-
<Transformer
|
|
946
|
-
// eslint-disable-next-line react/no-string-refs
|
|
947
|
-
ref="explorer"
|
|
948
|
-
graph={graph}
|
|
949
|
-
graphMode={this.props.graphMode}
|
|
950
|
-
listMode={this.props.listMode}
|
|
951
|
-
gradeEmpty={this.props.gradeEmpty}
|
|
952
|
-
tools={this.props.tools}
|
|
953
|
-
drawSolutionShape={this.props.drawSolutionShape}
|
|
954
|
-
starting={this.props.starting}
|
|
955
|
-
correct={this.props.starting}
|
|
956
|
-
transformations={this.props.correct.transformations}
|
|
957
|
-
onChange={this.changeTransformer}
|
|
958
|
-
trackInteraction={() => {}}
|
|
959
|
-
/>
|
|
960
|
-
</div>
|
|
961
|
-
);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// propagate a props change on our graph settings to
|
|
965
|
-
// this.props.graph
|
|
966
|
-
changeGraph: (arg1: any, arg2: any) => void = (graphChanges, callback) => {
|
|
967
|
-
const newGraph = _.extend({}, this.props.graph, graphChanges);
|
|
968
|
-
this.props.onChange(
|
|
969
|
-
{
|
|
970
|
-
graph: newGraph,
|
|
971
|
-
},
|
|
972
|
-
callback,
|
|
973
|
-
);
|
|
974
|
-
};
|
|
975
|
-
|
|
976
|
-
// propagate a props change on our starting graph to
|
|
977
|
-
// this.props.starting
|
|
978
|
-
changeStarting: (arg1: any) => void = (startingChanges) => {
|
|
979
|
-
const newStarting = _.extend({}, this.props.starting, startingChanges);
|
|
980
|
-
this.props.onChange({
|
|
981
|
-
starting: newStarting,
|
|
982
|
-
});
|
|
983
|
-
};
|
|
984
|
-
|
|
985
|
-
// propagate a transformations change onto correct.transformations
|
|
986
|
-
changeTransformer: (arg1: any, arg2: any) => void = (changes, callback) => {
|
|
987
|
-
if (changes.transformations) {
|
|
988
|
-
changes.correct = {
|
|
989
|
-
...this.props.correct,
|
|
990
|
-
transformations: changes.transformations,
|
|
991
|
-
};
|
|
992
|
-
delete changes.transformations;
|
|
993
|
-
}
|
|
994
|
-
this.props.onChange(changes, callback);
|
|
995
|
-
};
|
|
996
|
-
|
|
997
|
-
serialize: () => any = () => {
|
|
998
|
-
// eslint-disable-next-line react/no-string-refs
|
|
999
|
-
// @ts-expect-error - TS2339 - Property 'getEditorJSON' does not exist on type 'ReactInstance'.
|
|
1000
|
-
const json = this.refs.explorer.getEditorJSON();
|
|
1001
|
-
json.correct = json.answer;
|
|
1002
|
-
delete json.answer;
|
|
1003
|
-
return json;
|
|
1004
|
-
};
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
export default TransformerEditor;
|