@khanacademy/perseus 0.0.1 → 0.2.1
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/.babelrc.js +1 -1
- package/CHANGELOG.md +29 -0
- package/dist/es/index.css +4361 -0
- package/dist/es/index.js +7 -1
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +4361 -0
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/strings.js +148 -46
- package/package.json +50 -4
- package/src/README.md +35 -0
- package/src/__stories__/item-renderer.stories.jsx +27 -0
- package/src/__stories__/server-item-renderer.stories.jsx +27 -0
- package/src/__testdata__/item-renderer_testdata.js +161 -0
- package/src/__testdata__/renderer_testdata.js +123 -0
- package/src/__tests__/__snapshots__/item-renderer_test.jsx.snap +233 -0
- package/src/__tests__/__snapshots__/perseus-markdown_test.jsx.snap +516 -0
- package/src/__tests__/__snapshots__/pure-markdown_test.js.snap +113 -0
- package/src/__tests__/__snapshots__/renderer_test.jsx.snap +576 -0
- package/src/__tests__/__snapshots__/server-item-renderer_test.jsx.snap +306 -0
- package/src/__tests__/a11y_test.jsx +304 -0
- package/src/__tests__/item-renderer_test.jsx +892 -0
- package/src/__tests__/mock-asset-loading-widget.jsx +57 -0
- package/src/__tests__/mock-widget.jsx +35 -0
- package/src/__tests__/perseus-markdown_test.jsx +747 -0
- package/src/__tests__/pure-markdown_test.js +552 -0
- package/src/__tests__/renderability_test.jsx +481 -0
- package/src/__tests__/renderer-api_test.jsx +333 -0
- package/src/__tests__/renderer_test.jsx +1792 -0
- package/src/__tests__/server-item-renderer_test.jsx +516 -0
- package/src/__tests__/test-items/image-item.js +25 -0
- package/src/__tests__/test-items/input-number-1-item.js +27 -0
- package/src/__tests__/test-items/input-number-2-item.js +39 -0
- package/src/__tests__/test-items/table-item.js +30 -0
- package/src/__tests__/tex-wrangler_test.js +83 -0
- package/src/__tests__/util_test.js +41 -0
- package/src/a11y.js +46 -0
- package/src/all-widgets.js +9 -0
- package/src/article-renderer.jsx +215 -0
- package/src/asset-context.js +30 -0
- package/src/basic-widgets.js +13 -0
- package/src/components/__stories__/button-group.stories.jsx +61 -0
- package/src/components/__stories__/fixed-to-responsive.stories.jsx +132 -0
- package/src/components/__stories__/graph.stories.jsx +27 -0
- package/src/components/__stories__/graphie.stories.jsx +28 -0
- package/src/components/__stories__/hud.stories.jsx +36 -0
- package/src/components/__stories__/icon.stories.jsx +56 -0
- package/src/components/__stories__/image-loader.stories.jsx +60 -0
- package/src/components/__stories__/info-tip.stories.jsx +33 -0
- package/src/components/__stories__/inline-icon.stories.jsx +39 -0
- package/src/components/__stories__/input-with-examples.stories.jsx +51 -0
- package/src/components/__stories__/lint.stories.jsx +40 -0
- package/src/components/__stories__/math-input.stories.jsx +29 -0
- package/src/components/__stories__/math-output.stories.jsx +24 -0
- package/src/components/__stories__/multi-button-group.stories.jsx +69 -0
- package/src/components/__stories__/number-input.stories.jsx +42 -0
- package/src/components/__stories__/possible-answers.stories.jsx +22 -0
- package/src/components/__stories__/prop-check-box.stories.jsx +62 -0
- package/src/components/__stories__/range-input.stories.jsx +28 -0
- package/src/components/__stories__/simple-keypad-input.stories.jsx +28 -0
- package/src/components/__stories__/sortable.stories.jsx +73 -0
- package/src/components/__stories__/stub-tag-editor.stories.jsx +44 -0
- package/src/components/__stories__/svg-image.stories.jsx +60 -0
- package/src/components/__stories__/tex.stories.jsx +18 -0
- package/src/components/__stories__/text-buttons.stories.jsx +51 -0
- package/src/components/__stories__/text-input.stories.jsx +34 -0
- package/src/components/__stories__/text-list-editor.stories.jsx +33 -0
- package/src/components/__stories__/zoomable-tex.stories.jsx +39 -0
- package/src/components/__stories__/zoomable.stories.jsx +40 -0
- package/src/components/__testdata__/sorter_testdata.js +20 -0
- package/src/components/__tests__/__snapshots__/sortable_test.jsx.snap +79 -0
- package/src/components/__tests__/__snapshots__/sorter_test.jsx.snap +297 -0
- package/src/components/__tests__/number-input_test.jsx +112 -0
- package/src/components/__tests__/sortable_test.jsx +234 -0
- package/src/components/__tests__/sorter_test.jsx +111 -0
- package/src/components/__tests__/zoomable_test.jsx +388 -0
- package/src/components/button-group.jsx +131 -0
- package/src/components/constants.js +15 -0
- package/src/components/fixed-to-responsive.jsx +164 -0
- package/src/components/graph.jsx +415 -0
- package/src/components/graphie-classes.jsx +148 -0
- package/src/components/graphie-movables.jsx +342 -0
- package/src/components/graphie.jsx +353 -0
- package/src/components/highlighting/highlightable-content.jsx +245 -0
- package/src/components/highlighting/highlights.js +208 -0
- package/src/components/highlighting/ranges.js +299 -0
- package/src/components/highlighting/types.js +73 -0
- package/src/components/highlighting/ui/highlight-renderer.jsx +179 -0
- package/src/components/highlighting/ui/highlight-set-renderer.jsx +190 -0
- package/src/components/highlighting/ui/highlight-tooltip.jsx +153 -0
- package/src/components/highlighting/ui/highlighting-ui.jsx +163 -0
- package/src/components/highlighting/ui/selection-tracker.jsx +195 -0
- package/src/components/highlighting/ui/types.js +21 -0
- package/src/components/highlighting/ui/util.js +166 -0
- package/src/components/highlighting/word-indexer.jsx +155 -0
- package/src/components/hud.jsx +179 -0
- package/src/components/icon.jsx +149 -0
- package/src/components/image-loader.jsx +175 -0
- package/src/components/info-tip/info-tip.jsx +146 -0
- package/src/components/info-tip.jsx +38 -0
- package/src/components/inline-icon.jsx +81 -0
- package/src/components/input-with-examples.jsx +231 -0
- package/src/components/lint.jsx +454 -0
- package/src/components/math-input.jsx +282 -0
- package/src/components/math-output.jsx +128 -0
- package/src/components/multi-button-group.jsx +137 -0
- package/src/components/new-tooltip/README.md +110 -0
- package/src/components/new-tooltip/__stories__/flexible-position.stories.jsx +437 -0
- package/src/components/new-tooltip/__stories__/new-tooltip.stories.jsx +665 -0
- package/src/components/new-tooltip/__stories__/tooltip-arrow.stories.jsx +70 -0
- package/src/components/new-tooltip/__stories__/tooltip-bubble.stories.jsx +98 -0
- package/src/components/new-tooltip/__tests__/style-constants_test.js +8 -0
- package/src/components/new-tooltip/__tests__/util_test.js +47 -0
- package/src/components/new-tooltip/flexible-position.jsx +182 -0
- package/src/components/new-tooltip/new-tooltip.jsx +399 -0
- package/src/components/new-tooltip/style-constants.js +13 -0
- package/src/components/new-tooltip/tooltip-arrow.jsx +184 -0
- package/src/components/new-tooltip/tooltip-bubble.jsx +223 -0
- package/src/components/new-tooltip/tooltip-portal.jsx +101 -0
- package/src/components/new-tooltip/tooltip-positioner.jsx +529 -0
- package/src/components/new-tooltip/types.js +183 -0
- package/src/components/new-tooltip/util.js +27 -0
- package/src/components/number-input.jsx +253 -0
- package/src/components/possible-answers.jsx +36 -0
- package/src/components/prop-check-box.jsx +80 -0
- package/src/components/range-input.jsx +61 -0
- package/src/components/simple-keypad-input.jsx +91 -0
- package/src/components/sortable.jsx +873 -0
- package/src/components/stub-tag-editor.jsx +54 -0
- package/src/components/svg-image.jsx +762 -0
- package/src/components/tex-buttons.jsx +322 -0
- package/src/components/tex.jsx +70 -0
- package/src/components/text-input.jsx +82 -0
- package/src/components/text-list-editor.jsx +157 -0
- package/src/components/tooltip.jsx +395 -0
- package/src/components/visibility-observer/__stories__/visibility-observer.stories.jsx +140 -0
- package/src/components/visibility-observer/get-scroll-parent.js +45 -0
- package/src/components/visibility-observer/visibility-observer.js +109 -0
- package/src/components/visibility-observer/visibility-observer.jsx +123 -0
- package/src/components/zoomable-tex.jsx +70 -0
- package/src/components/zoomable.jsx +304 -0
- package/src/components.js +17 -0
- package/src/dependencies.js +22 -0
- package/src/error-boundary.jsx +48 -0
- package/src/extra-widgets.js +75 -0
- package/src/gorgon/README +40 -0
- package/src/gorgon/__tests__/matcher_test.js +497 -0
- package/src/gorgon/__tests__/rule_test.js +101 -0
- package/src/gorgon/__tests__/rules_test.js +487 -0
- package/src/gorgon/__tests__/selector-parser_test.js +52 -0
- package/src/gorgon/__tests__/tree-transformer_test.js +446 -0
- package/src/gorgon/gorgon.js +290 -0
- package/src/gorgon/proptypes.js +29 -0
- package/src/gorgon/rule.js +413 -0
- package/src/gorgon/rules/absolute-url.js +24 -0
- package/src/gorgon/rules/all-rules.js +72 -0
- package/src/gorgon/rules/blockquoted-math.js +10 -0
- package/src/gorgon/rules/blockquoted-widget.js +10 -0
- package/src/gorgon/rules/double-spacing-after-terminal.js +12 -0
- package/src/gorgon/rules/extra-content-spacing.js +12 -0
- package/src/gorgon/rules/heading-level-1.js +14 -0
- package/src/gorgon/rules/heading-level-skip.js +20 -0
- package/src/gorgon/rules/heading-sentence-case.js +11 -0
- package/src/gorgon/rules/heading-title-case.js +63 -0
- package/src/gorgon/rules/image-alt-text.js +21 -0
- package/src/gorgon/rules/image-in-table.js +10 -0
- package/src/gorgon/rules/image-spaces-around-urls.js +35 -0
- package/src/gorgon/rules/image-widget.js +50 -0
- package/src/gorgon/rules/link-click-here.js +11 -0
- package/src/gorgon/rules/lint-utils.js +48 -0
- package/src/gorgon/rules/long-paragraph.js +14 -0
- package/src/gorgon/rules/math-adjacent.js +10 -0
- package/src/gorgon/rules/math-align-extra-break.js +11 -0
- package/src/gorgon/rules/math-align-linebreaks.js +43 -0
- package/src/gorgon/rules/math-empty.js +10 -0
- package/src/gorgon/rules/math-font-size.js +12 -0
- package/src/gorgon/rules/math-frac.js +10 -0
- package/src/gorgon/rules/math-nested.js +11 -0
- package/src/gorgon/rules/math-starts-with-space.js +12 -0
- package/src/gorgon/rules/math-text-empty.js +10 -0
- package/src/gorgon/rules/math-without-dollars.js +14 -0
- package/src/gorgon/rules/nested-lists.js +11 -0
- package/src/gorgon/rules/profanity.js +10 -0
- package/src/gorgon/rules/table-missing-cells.js +20 -0
- package/src/gorgon/rules/unbalanced-code-delimiters.js +14 -0
- package/src/gorgon/rules/unescaped-dollar.js +10 -0
- package/src/gorgon/rules/widget-in-table.js +10 -0
- package/src/gorgon/selector.js +506 -0
- package/src/gorgon/tree-transformer.js +588 -0
- package/src/hint-renderer.jsx +159 -0
- package/src/hints-renderer.jsx +359 -0
- package/src/icon-paths.js +120 -0
- package/src/index.js +149 -2
- package/src/init.js +49 -0
- package/src/interaction-tracker.js +60 -0
- package/src/interactive2/__tests__/movable-mock.js +48 -0
- package/src/interactive2/__tests__/movable-point_test.js +146 -0
- package/src/interactive2/interactive-util.js +152 -0
- package/src/interactive2/movable-helper-methods.js +160 -0
- package/src/interactive2/movable-line-options.js +392 -0
- package/src/interactive2/movable-line.js +322 -0
- package/src/interactive2/movable-point-options.js +190 -0
- package/src/interactive2/movable-point.jsx +603 -0
- package/src/interactive2/movable-polygon-options.js +351 -0
- package/src/interactive2/movable-polygon.js +368 -0
- package/src/interactive2/movable.js +273 -0
- package/src/interactive2/objective_.js +79 -0
- package/src/interactive2/types.js +3 -0
- package/src/interactive2/wrapped-defaults.js +86 -0
- package/src/interactive2/wrapped-ellipse.js +87 -0
- package/src/interactive2/wrapped-line.js +109 -0
- package/src/interactive2/wrapped-path.js +47 -0
- package/src/interactive2.js +91 -0
- package/src/item-renderer.jsx +518 -0
- package/src/item-version.js +12 -0
- package/src/jipt-paragraphs.js +44 -0
- package/src/jquery.mobile.vmouse.js +545 -0
- package/src/keypad-context.js +33 -0
- package/src/loading-context.js +20 -0
- package/src/logging/log.js +107 -0
- package/src/logging/log_test.js +41 -0
- package/src/mixins/changeable.jsx +115 -0
- package/src/mixins/editor-jsonify.jsx +13 -0
- package/src/mixins/provide-keypad.jsx +129 -0
- package/src/mixins/widget-jsonify-deprecated.jsx +23 -0
- package/src/mixins/widget-prop-blacklist.js +36 -0
- package/src/multi-items/__stories__/multi-renderer.stories.js +88 -0
- package/src/multi-items/__testdata__/multi-renderer_testdata.js +174 -0
- package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +994 -0
- package/src/multi-items/__tests__/items_test.js +245 -0
- package/src/multi-items/__tests__/multi-renderer_test.js +903 -0
- package/src/multi-items/__tests__/prop-type-builders_test.js +134 -0
- package/src/multi-items/__tests__/shapes_test.js +44 -0
- package/src/multi-items/__tests__/trees_test.js +316 -0
- package/src/multi-items/item-types.js +55 -0
- package/src/multi-items/items.js +176 -0
- package/src/multi-items/multi-renderer.jsx +543 -0
- package/src/multi-items/prop-type-builders.js +105 -0
- package/src/multi-items/shape-types.js +47 -0
- package/src/multi-items/shapes.js +53 -0
- package/src/multi-items/tree-types.js +42 -0
- package/src/multi-items/trees.js +345 -0
- package/src/multi-items.js +76 -0
- package/src/not-gorgon.js +129 -0
- package/src/perseus-api.jsx +270 -0
- package/src/perseus-env.js +26 -0
- package/src/perseus-error.js +19 -0
- package/src/perseus-markdown.jsx +433 -0
- package/src/perseus-types.js +1597 -0
- package/src/pure-markdown.js +347 -0
- package/src/question-paragraph.jsx +30 -0
- package/src/renderability.jsx +108 -0
- package/src/renderer.jsx +1932 -0
- package/src/server-item-renderer.jsx +467 -0
- package/src/sigfigs.jsx +492 -0
- package/src/styles/articles.less +748 -0
- package/src/styles/constants.js +119 -0
- package/src/styles/global-constants.js +350 -0
- package/src/styles/global-styles.js +307 -0
- package/src/styles/khan-exercise.css +1304 -0
- package/src/styles/media-queries.js +48 -0
- package/src/styles/perseus-renderer.less +1007 -0
- package/src/styles/shared.js +132 -0
- package/src/styles/util.less +47 -0
- package/src/styles/variables.boostrap.less +301 -0
- package/src/styles/variables.less +68 -0
- package/src/styles/variables.shared.less +356 -0
- package/src/styles/widgets/categorizer.less +39 -0
- package/src/styles/widgets/definition.less +86 -0
- package/src/styles/widgets/dropdown.less +3 -0
- package/src/styles/widgets/expression.less +147 -0
- package/src/styles/widgets/graded-group.less +36 -0
- package/src/styles/widgets/grapher.less +11 -0
- package/src/styles/widgets/group.less +19 -0
- package/src/styles/widgets/image.less +29 -0
- package/src/styles/widgets/interactive-graph.less +61 -0
- package/src/styles/widgets/label-image.less +41 -0
- package/src/styles/widgets/matcher.less +8 -0
- package/src/styles/widgets/matrix.less +120 -0
- package/src/styles/widgets/measurer.less +20 -0
- package/src/styles/widgets/orderer.less +97 -0
- package/src/styles/widgets/passage.less +151 -0
- package/src/styles/widgets/plotter.less +33 -0
- package/src/styles/widgets/radio.less +52 -0
- package/src/styles/widgets/reaction-diagram.less +35 -0
- package/src/styles/widgets/sequence.less +5 -0
- package/src/styles/widgets/sortable.less +127 -0
- package/src/styles/widgets/table.less +56 -0
- package/src/styles/widgets/transformer.less +28 -0
- package/src/styles/widgets/unit.less +14 -0
- package/src/tex-wrangler.js +176 -0
- package/src/traversal.jsx +141 -0
- package/src/types.js +538 -0
- package/src/util/a11y.js +24 -0
- package/src/util/answer-types.js +862 -0
- package/src/util/answer-types_test.js +115 -0
- package/src/util/colors.js +94 -0
- package/src/util/fix-passage-refs.jsx +122 -0
- package/src/util/graph-utils.js +16 -0
- package/src/util/graphie.js +1586 -0
- package/src/util/interactive.js +4078 -0
- package/src/util/katex-preprocess.js +16 -0
- package/src/util/math.js +296 -0
- package/src/util/mobile-native-utils.js +14 -0
- package/src/util/mobile-native-utils_test.js +24 -0
- package/src/util/register-all-widgets-for-testing.js +12 -0
- package/src/util/scroll-utils.js +162 -0
- package/src/util/sizing-utils.js +46 -0
- package/src/util/tex.js +203 -0
- package/src/util/url-utils.js +36 -0
- package/src/util.js +990 -0
- package/src/version.js +11 -0
- package/src/widget-container.jsx +174 -0
- package/src/widgets/__stories__/categorizer.stories.jsx +15 -0
- package/src/widgets/__stories__/cs-program.stories.jsx +15 -0
- package/src/widgets/__stories__/definition.stories.jsx +38 -0
- package/src/widgets/__stories__/dropdown.stories.jsx +15 -0
- package/src/widgets/__stories__/explanation.stories.jsx +19 -0
- package/src/widgets/__stories__/expression.stories.jsx +26 -0
- package/src/widgets/__stories__/graded-group-set.stories.jsx +32 -0
- package/src/widgets/__stories__/graded-group.stories.jsx +32 -0
- package/src/widgets/__stories__/grapher.stories.jsx +47 -0
- package/src/widgets/__stories__/group.stories.jsx +15 -0
- package/src/widgets/__stories__/iframe.stories.jsx +15 -0
- package/src/widgets/__stories__/image.stories.jsx +62 -0
- package/src/widgets/__stories__/input-number.stories.jsx +90 -0
- package/src/widgets/__stories__/interaction.stories.jsx +15 -0
- package/src/widgets/__stories__/interactive-graph.stories.jsx +61 -0
- package/src/widgets/__stories__/matcher.stories.jsx +15 -0
- package/src/widgets/__stories__/matrix.stories.jsx +15 -0
- package/src/widgets/__stories__/number-line.stories.jsx +15 -0
- package/src/widgets/__stories__/numeric-input.stories.jsx +19 -0
- package/src/widgets/__stories__/orderer.stories.jsx +15 -0
- package/src/widgets/__stories__/passage-ref.stories.jsx +19 -0
- package/src/widgets/__stories__/passage.stories.jsx +19 -0
- package/src/widgets/__stories__/radio.stories.jsx +82 -0
- package/src/widgets/__stories__/sorter.stories.jsx +15 -0
- package/src/widgets/__stories__/transformer.stories.jsx +15 -0
- package/src/widgets/__stories__/unit.stories.jsx +45 -0
- package/src/widgets/__stories__/video-transcript-link.stories.jsx +24 -0
- package/src/widgets/__stories__/video.stories.jsx +15 -0
- package/src/widgets/__testdata__/categorizer_testdata.js +50 -0
- package/src/widgets/__testdata__/cs-program_testdata.js +28 -0
- package/src/widgets/__testdata__/dropdown_testdata.js +34 -0
- package/src/widgets/__testdata__/explanation_testdata.js +56 -0
- package/src/widgets/__testdata__/expression_testdata.js +95 -0
- package/src/widgets/__testdata__/graded-group-set_testdata.js +358 -0
- package/src/widgets/__testdata__/graded-group_testdata.js +79 -0
- package/src/widgets/__testdata__/grapher_testdata.js +376 -0
- package/src/widgets/__testdata__/group_testdata.js +156 -0
- package/src/widgets/__testdata__/iframe_testdata.js +36 -0
- package/src/widgets/__testdata__/image_testdata.js +38 -0
- package/src/widgets/__testdata__/input-number_testdata.js +73 -0
- package/src/widgets/__testdata__/interaction_testdata.js +264 -0
- package/src/widgets/__testdata__/interactive-graph_testdata.js +646 -0
- package/src/widgets/__testdata__/matcher_testdata.js +34 -0
- package/src/widgets/__testdata__/matrix_testdata.js +29 -0
- package/src/widgets/__testdata__/number-line_testdata.js +31 -0
- package/src/widgets/__testdata__/numeric-input_testdata.js +266 -0
- package/src/widgets/__testdata__/orderer_testdata.js +58 -0
- package/src/widgets/__testdata__/passage-ref_testdata.js +41 -0
- package/src/widgets/__testdata__/passage_testdata.js +71 -0
- package/src/widgets/__testdata__/radio_testdata.js +210 -0
- package/src/widgets/__testdata__/sorter_testdata.js +20 -0
- package/src/widgets/__testdata__/transformer_testdata.js +109 -0
- package/src/widgets/__testdata__/video_testdata.js +22 -0
- package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +928 -0
- package/src/widgets/__tests__/__snapshots__/cs-program_test.jsx.snap +75 -0
- package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +99 -0
- package/src/widgets/__tests__/__snapshots__/dropdown_test.jsx.snap +143 -0
- package/src/widgets/__tests__/__snapshots__/explanation_test.js.snap +473 -0
- package/src/widgets/__tests__/__snapshots__/expression_test.jsx.snap +38 -0
- package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +754 -0
- package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +310 -0
- package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +863 -0
- package/src/widgets/__tests__/__snapshots__/grapher_test.jsx.snap +2880 -0
- package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +1154 -0
- package/src/widgets/__tests__/__snapshots__/iframe_test.jsx.snap +69 -0
- package/src/widgets/__tests__/__snapshots__/image_test.jsx.snap +388 -0
- package/src/widgets/__tests__/__snapshots__/input-number_test.jsx.snap +978 -0
- package/src/widgets/__tests__/__snapshots__/interaction_test.jsx.snap +311 -0
- package/src/widgets/__tests__/__snapshots__/interactive-graph_test.jsx.snap +2915 -0
- package/src/widgets/__tests__/__snapshots__/matcher_test.jsx.snap +1039 -0
- package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +450 -0
- package/src/widgets/__tests__/__snapshots__/number-line_test.jsx.snap +888 -0
- package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +930 -0
- package/src/widgets/__tests__/__snapshots__/orderer_test.jsx.snap +275 -0
- package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +446 -0
- package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +1988 -0
- package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +7107 -0
- package/src/widgets/__tests__/__snapshots__/video_test.jsx.snap +163 -0
- package/src/widgets/__tests__/categorizer_test.jsx +205 -0
- package/src/widgets/__tests__/cs-program_test.jsx +45 -0
- package/src/widgets/__tests__/definition_test.js +152 -0
- package/src/widgets/__tests__/dropdown_test.jsx +106 -0
- package/src/widgets/__tests__/explanation_test.js +199 -0
- package/src/widgets/__tests__/expression_test.jsx +439 -0
- package/src/widgets/__tests__/graded-group-set-jipt_test.jsx +33 -0
- package/src/widgets/__tests__/graded-group-set_test.jsx +270 -0
- package/src/widgets/__tests__/graded-group_test.jsx +233 -0
- package/src/widgets/__tests__/grapher.cypress.jsx +486 -0
- package/src/widgets/__tests__/grapher_test.jsx +35 -0
- package/src/widgets/__tests__/group_test.jsx +512 -0
- package/src/widgets/__tests__/iframe_test.jsx +47 -0
- package/src/widgets/__tests__/image_test.jsx +65 -0
- package/src/widgets/__tests__/input-number_test.jsx +360 -0
- package/src/widgets/__tests__/interaction_test.jsx +33 -0
- package/src/widgets/__tests__/interactive-graph.cypress.jsx +697 -0
- package/src/widgets/__tests__/interactive-graph_test.jsx +105 -0
- package/src/widgets/__tests__/matcher_test.jsx +157 -0
- package/src/widgets/__tests__/matrix_test.jsx +84 -0
- package/src/widgets/__tests__/number-line_test.jsx +81 -0
- package/src/widgets/__tests__/numeric-input_test.jsx +574 -0
- package/src/widgets/__tests__/orderer_test.jsx +75 -0
- package/src/widgets/__tests__/passage-ref_test.jsx +166 -0
- package/src/widgets/__tests__/passage_test.jsx +133 -0
- package/src/widgets/__tests__/radio/base-radio_test.jsx +82 -0
- package/src/widgets/__tests__/radio_test.jsx +651 -0
- package/src/widgets/__tests__/renderQuestion.jsx +66 -0
- package/src/widgets/__tests__/unit_test.jsx +144 -0
- package/src/widgets/__tests__/video_test.jsx +55 -0
- package/src/widgets/categorizer.jsx +338 -0
- package/src/widgets/cs-program.jsx +227 -0
- package/src/widgets/definition.jsx +141 -0
- package/src/widgets/dropdown.jsx +143 -0
- package/src/widgets/explanation.jsx +315 -0
- package/src/widgets/expression.jsx +652 -0
- package/src/widgets/graded-group-answer-bar.jsx +152 -0
- package/src/widgets/graded-group-set.jsx +285 -0
- package/src/widgets/graded-group.jsx +448 -0
- package/src/widgets/grapher/util.jsx +806 -0
- package/src/widgets/grapher.jsx +663 -0
- package/src/widgets/group.jsx +213 -0
- package/src/widgets/iframe.jsx +198 -0
- package/src/widgets/image.jsx +310 -0
- package/src/widgets/input-number.jsx +411 -0
- package/src/widgets/interaction/arrow-picker.jsx +39 -0
- package/src/widgets/interaction/color-picker.jsx +69 -0
- package/src/widgets/interaction/dash-picker.jsx +46 -0
- package/src/widgets/interaction/element-container.jsx +97 -0
- package/src/widgets/interaction.jsx +742 -0
- package/src/widgets/interactive-graph.jsx +2820 -0
- package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +93 -0
- package/src/widgets/label-image/__stories__/marker.stories.jsx +172 -0
- package/src/widgets/label-image/answer-choices.jsx +311 -0
- package/src/widgets/label-image/marker.jsx +365 -0
- package/src/widgets/label-image/types.js +33 -0
- package/src/widgets/label-image.jsx +841 -0
- package/src/widgets/label-image_test.js +840 -0
- package/src/widgets/lights-puzzle.jsx +356 -0
- package/src/widgets/matcher.jsx +271 -0
- package/src/widgets/matrix.jsx +616 -0
- package/src/widgets/measurer.jsx +215 -0
- package/src/widgets/molecule/__tests__/layout_test.jsx +159 -0
- package/src/widgets/molecule/__tests__/parser_test.jsx +142 -0
- package/src/widgets/molecule/molecule-drawing.jsx +175 -0
- package/src/widgets/molecule/molecule-layout.jsx +367 -0
- package/src/widgets/molecule/smiles-parser.jsx +300 -0
- package/src/widgets/molecule.jsx +172 -0
- package/src/widgets/number-line.jsx +858 -0
- package/src/widgets/numeric-input.jsx +622 -0
- package/src/widgets/orderer.jsx +753 -0
- package/src/widgets/passage/__tests__/passage-markdown_test.jsx +382 -0
- package/src/widgets/passage/get-line-height-for-node.js +10 -0
- package/src/widgets/passage/passage-markdown.jsx +416 -0
- package/src/widgets/passage-ref-target.jsx +70 -0
- package/src/widgets/passage-ref.jsx +198 -0
- package/src/widgets/passage.jsx +601 -0
- package/src/widgets/plotter.jsx +1198 -0
- package/src/widgets/radio/__stories__/base-radio.stories.jsx +40 -0
- package/src/widgets/radio/__stories__/choice-icon.stories.jsx +157 -0
- package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +26 -0
- package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +64 -0
- package/src/widgets/radio/__stories__/cross-out-menu.stories.jsx +59 -0
- package/src/widgets/radio/__stories__/cross-out-tooltip.stories.jsx +59 -0
- package/src/widgets/radio/__stories__/focus-ring.stories.jsx +38 -0
- package/src/widgets/radio/__stories__/option-status.stories.jsx +118 -0
- package/src/widgets/radio/__stories__/toggleable-radio-button.stories.jsx +28 -0
- package/src/widgets/radio/base-radio.jsx +642 -0
- package/src/widgets/radio/choice-icon.jsx +468 -0
- package/src/widgets/radio/choice-none-above.jsx +54 -0
- package/src/widgets/radio/choice.jsx +692 -0
- package/src/widgets/radio/choice.jsx.fixture.js +64 -0
- package/src/widgets/radio/cross-out-button.jsx +136 -0
- package/src/widgets/radio/cross-out-menu-wrapper.jsx +492 -0
- package/src/widgets/radio/cross-out-tooltip.jsx +129 -0
- package/src/widgets/radio/focus-ring.jsx +51 -0
- package/src/widgets/radio/option-status.jsx +89 -0
- package/src/widgets/radio/toggleable-radio-button.jsx +133 -0
- package/src/widgets/radio/util.js +32 -0
- package/src/widgets/radio/util_test.js +19 -0
- package/src/widgets/radio/widget.jsx +470 -0
- package/src/widgets/radio.jsx +156 -0
- package/src/widgets/reaction-diagram.jsx +137 -0
- package/src/widgets/sequence.jsx +140 -0
- package/src/widgets/simulator.jsx +808 -0
- package/src/widgets/sorter.jsx +146 -0
- package/src/widgets/table.jsx +394 -0
- package/src/widgets/transformer.jsx +2876 -0
- package/src/widgets/unit.jsx +372 -0
- package/src/widgets/video-transcript-link.jsx +84 -0
- package/src/widgets/video-transcript-link_test.js +211 -0
- package/src/widgets/video.jsx +126 -0
- package/src/widgets.js +431 -0
- package/src/zoom.js +539 -0
- package/README.md +0 -3
- package/src/__tests__/index.test.js +0 -14
|
@@ -0,0 +1,2876 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
/* eslint-disable @babel/no-invalid-this */
|
|
3
|
+
/* eslint-disable camelcase */
|
|
4
|
+
/* eslint-disable react/no-did-update-set-state */
|
|
5
|
+
/* eslint-disable react/sort-comp */
|
|
6
|
+
/*
|
|
7
|
+
NOTE(Pereus stabilize project): This widget is no-longer-used as of January
|
|
8
|
+
2022. It's not present in US content but is still present in some
|
|
9
|
+
international content so it can't be removed quite yet. As soon as it's
|
|
10
|
+
verified to have been removed from international content this all needs
|
|
11
|
+
to get deleted. In the meantime- do not maintain this code.
|
|
12
|
+
|
|
13
|
+
ref: https://khanacademy.slack.com/archives/CJDRXTGQ7/p1641857400011600
|
|
14
|
+
ref: https://khanacademy.slack.com/archives/C2RFQGYKU/p1596160975061500?thread_ts=1596129758.040000&cid=C2RFQGYKU
|
|
15
|
+
*/
|
|
16
|
+
// @flow
|
|
17
|
+
import {
|
|
18
|
+
number as knumber,
|
|
19
|
+
vector as kvector,
|
|
20
|
+
point as kpoint,
|
|
21
|
+
ray as kray,
|
|
22
|
+
line as kline,
|
|
23
|
+
} from "@khanacademy/kmath";
|
|
24
|
+
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
25
|
+
import createReactClass from "create-react-class";
|
|
26
|
+
import $ from "jquery";
|
|
27
|
+
import * as React from "react";
|
|
28
|
+
import ReactDOM from "react-dom";
|
|
29
|
+
import _ from "underscore";
|
|
30
|
+
|
|
31
|
+
import Graph from "../components/graph.jsx";
|
|
32
|
+
import InlineIcon from "../components/inline-icon.jsx";
|
|
33
|
+
import MathOutput from "../components/math-output.jsx";
|
|
34
|
+
import NumberInput from "../components/number-input.jsx";
|
|
35
|
+
import SimpleKeypadInput from "../components/simple-keypad-input.jsx";
|
|
36
|
+
import {getDependencies} from "../dependencies.js";
|
|
37
|
+
import {iconPlus, iconUndo} from "../icon-paths.js";
|
|
38
|
+
import InteractiveUtil from "../interactive2/interactive-util.js";
|
|
39
|
+
import {Errors} from "../logging/log.js";
|
|
40
|
+
import {ApiOptions} from "../perseus-api.jsx";
|
|
41
|
+
import {PerseusError} from "../perseus-error.js";
|
|
42
|
+
import Util from "../util.js";
|
|
43
|
+
import KhanColors from "../util/colors.js";
|
|
44
|
+
import KhanMath from "../util/math.js";
|
|
45
|
+
|
|
46
|
+
import type {Coord} from "../interactive2/types.js";
|
|
47
|
+
import type {
|
|
48
|
+
PerseusTransformerWidgetOptions,
|
|
49
|
+
PerseusTransformerTransformation as TransformerTransformation,
|
|
50
|
+
DilationTransformation,
|
|
51
|
+
ReflectionTransformation,
|
|
52
|
+
RotationTransformation,
|
|
53
|
+
TranslationTransformation,
|
|
54
|
+
} from "../perseus-types.js";
|
|
55
|
+
import type {Path, PerseusScore, WidgetExports, WidgetProps} from "../types.js";
|
|
56
|
+
|
|
57
|
+
const {deepEq, getGridStep, captureScratchpadTouchStart} = Util;
|
|
58
|
+
const {assert} = InteractiveUtil;
|
|
59
|
+
|
|
60
|
+
const ROTATE_SNAP_DEGREES = 15;
|
|
61
|
+
|
|
62
|
+
const DEGREE_SIGN = "\u00B0";
|
|
63
|
+
const RENDER_TRANSFORM_DELAY_IN_MS = 300;
|
|
64
|
+
const ROTATE_HANDLE_DIST = 1.5;
|
|
65
|
+
const REFLECT_ROTATE_HANDLE_DIST = 2;
|
|
66
|
+
const REFLECT_BUTTON_SIZE = 1;
|
|
67
|
+
const defaultBoxSize = 400;
|
|
68
|
+
const defaultBackgroundImage = {
|
|
69
|
+
url: null,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/* Does a pluck on keys inside objects in an object
|
|
73
|
+
*
|
|
74
|
+
* Ex:
|
|
75
|
+
* tools = {
|
|
76
|
+
* translation: {
|
|
77
|
+
* enabled: true
|
|
78
|
+
* },
|
|
79
|
+
* rotation: {
|
|
80
|
+
* enabled: false
|
|
81
|
+
* }
|
|
82
|
+
* };
|
|
83
|
+
* pluckObject(tools, "enabled") returns {
|
|
84
|
+
* translation: true
|
|
85
|
+
* rotation: false
|
|
86
|
+
* }
|
|
87
|
+
*/
|
|
88
|
+
function pluckObject(object, subKey) {
|
|
89
|
+
return _.object(
|
|
90
|
+
_.map(object, function (value, key) {
|
|
91
|
+
return [key, value[subKey]];
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const defaultGraphProps = function (setProps, boxSize) {
|
|
97
|
+
setProps = setProps || {};
|
|
98
|
+
const labels = setProps.labels || ["x", "y"];
|
|
99
|
+
const range = setProps.range || [
|
|
100
|
+
[-10, 10],
|
|
101
|
+
[-10, 10],
|
|
102
|
+
];
|
|
103
|
+
const step = setProps.step || [1, 1];
|
|
104
|
+
const gridStep = setProps.gridStep || getGridStep(range, step, boxSize);
|
|
105
|
+
return {
|
|
106
|
+
box: [boxSize, boxSize],
|
|
107
|
+
labels: labels,
|
|
108
|
+
range: range,
|
|
109
|
+
step: step,
|
|
110
|
+
gridStep: gridStep,
|
|
111
|
+
valid: true,
|
|
112
|
+
backgroundImage: defaultBackgroundImage,
|
|
113
|
+
markings: "grid",
|
|
114
|
+
showProtractor: false,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const defaultTransformerProps = {
|
|
119
|
+
apiOptions: ApiOptions.defaults,
|
|
120
|
+
gradeEmpty: false,
|
|
121
|
+
graphMode: "interactive",
|
|
122
|
+
listMode: "dynamic",
|
|
123
|
+
graph: {},
|
|
124
|
+
tools: {
|
|
125
|
+
translation: {
|
|
126
|
+
enabled: true,
|
|
127
|
+
required: false,
|
|
128
|
+
constraints: {},
|
|
129
|
+
},
|
|
130
|
+
rotation: {
|
|
131
|
+
enabled: true,
|
|
132
|
+
required: false,
|
|
133
|
+
constraints: {
|
|
134
|
+
fixed: false,
|
|
135
|
+
},
|
|
136
|
+
coord: [1, 6],
|
|
137
|
+
},
|
|
138
|
+
reflection: {
|
|
139
|
+
enabled: true,
|
|
140
|
+
required: false,
|
|
141
|
+
constraints: {
|
|
142
|
+
fixed: false,
|
|
143
|
+
},
|
|
144
|
+
coords: [
|
|
145
|
+
[2, -4],
|
|
146
|
+
[2, 2],
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
dilation: {
|
|
150
|
+
enabled: true,
|
|
151
|
+
required: false,
|
|
152
|
+
constraints: {
|
|
153
|
+
fixed: false,
|
|
154
|
+
},
|
|
155
|
+
coord: [6, 6],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
drawSolutionShape: true,
|
|
159
|
+
starting: {
|
|
160
|
+
shape: {
|
|
161
|
+
type: "polygon-3",
|
|
162
|
+
coords: [
|
|
163
|
+
[2, 2],
|
|
164
|
+
[2, 6],
|
|
165
|
+
[7, 2],
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
transformations: [],
|
|
169
|
+
},
|
|
170
|
+
correct: {
|
|
171
|
+
shape: {
|
|
172
|
+
type: "polygon-3",
|
|
173
|
+
coords: [
|
|
174
|
+
[2, 2],
|
|
175
|
+
[2, 6],
|
|
176
|
+
[7, 2],
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
transformations: [],
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
function colorForTool(tool) {
|
|
184
|
+
return tool.constraints.fixed ? KhanColors.DYNAMIC : KhanColors.INTERACTIVE;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Scales a distance from the default range of
|
|
188
|
+
* [-10, 10] to a given props.range pair
|
|
189
|
+
*
|
|
190
|
+
* Used for sizing various transformation tools
|
|
191
|
+
* (rotation handle, dilation circle)
|
|
192
|
+
*/
|
|
193
|
+
function scaleToRange(dist, range) {
|
|
194
|
+
const spreadX = range[0][1] - range[0][0];
|
|
195
|
+
const spreadY = range[1][1] - range[1][0];
|
|
196
|
+
|
|
197
|
+
return (dist * Math.max(spreadX, spreadY)) / 20;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function dilatePointFromCenter(point, dilationCenter, scale) {
|
|
201
|
+
const pv = kvector.subtract(point, dilationCenter);
|
|
202
|
+
const pvScaled = kvector.scale(pv, scale);
|
|
203
|
+
const transformedPoint = kvector.add(dilationCenter, pvScaled);
|
|
204
|
+
return transformedPoint;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// TODO(jack): i18nize this
|
|
208
|
+
function stringFromDecimal(number) {
|
|
209
|
+
return String(KhanMath.roundTo(9, number));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function stringFromFraction(number) {
|
|
213
|
+
const frac = KhanMath.toFraction(number, knumber.DEFAULT_TOLERANCE);
|
|
214
|
+
if (frac[1] === 1) {
|
|
215
|
+
return stringFromDecimal(number);
|
|
216
|
+
}
|
|
217
|
+
return stringFromDecimal(frac[0]) + "/" + stringFromDecimal(frac[1]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function texFromPoint(point) {
|
|
221
|
+
const {TeX} = getDependencies();
|
|
222
|
+
|
|
223
|
+
return [
|
|
224
|
+
<TeX key="(">(</TeX>,
|
|
225
|
+
stringFromDecimal(point[0]),
|
|
226
|
+
<TeX key=",">{", {}"}</TeX>,
|
|
227
|
+
stringFromDecimal(point[1]),
|
|
228
|
+
<TeX key=")">)</TeX>,
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function texFromVector(vector) {
|
|
233
|
+
const {TeX} = getDependencies();
|
|
234
|
+
|
|
235
|
+
return [
|
|
236
|
+
<TeX key="<">{i18n.doNotTranslate("\\langle")}</TeX>,
|
|
237
|
+
stringFromDecimal(vector[0]),
|
|
238
|
+
<TeX key=",">{", {}"}</TeX>,
|
|
239
|
+
stringFromDecimal(vector[1]),
|
|
240
|
+
<TeX key=">">{i18n.doNotTranslate("\\rangle")}</TeX>,
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function texFromAngleDeg(angleDeg) {
|
|
245
|
+
return stringFromDecimal(angleDeg) + DEGREE_SIGN;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function orderInsensitiveCoordsEqual(coords1, coords2) {
|
|
249
|
+
coords1 = _.clone(coords1).sort(kpoint.compare);
|
|
250
|
+
coords2 = _.clone(coords2).sort(kpoint.compare);
|
|
251
|
+
return _.all(
|
|
252
|
+
_.map(coords1, function (coord1, i) {
|
|
253
|
+
const coord2 = coords2[i];
|
|
254
|
+
return kpoint.equal(coord1, coord2);
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const inputComponentForApiOptions = (apiOptions) => {
|
|
260
|
+
if (apiOptions.customKeypad) {
|
|
261
|
+
return SimpleKeypadInput;
|
|
262
|
+
}
|
|
263
|
+
if (apiOptions.staticRender) {
|
|
264
|
+
return MathOutput;
|
|
265
|
+
}
|
|
266
|
+
return NumberInput;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/* Perform operations on raw transform objects */
|
|
270
|
+
const TransformOps = {
|
|
271
|
+
apply: function (transform: TransformerTransformation) {
|
|
272
|
+
// Any transformation with empty text boxes is a no-op until
|
|
273
|
+
// filled out (these show up as nulls in transform.vector/line/etc).
|
|
274
|
+
// TODO (jack): Merge this just into reflections now that other
|
|
275
|
+
// transforms are always valid (after merging transformation
|
|
276
|
+
// collapsing, which may use isValid)
|
|
277
|
+
// $FlowFixMe[prop-missing]
|
|
278
|
+
// $FlowFixMe[incompatible-call]
|
|
279
|
+
if (!Transformations[transform.type].isValid(transform)) {
|
|
280
|
+
return _.identity; // do not transform the coord
|
|
281
|
+
}
|
|
282
|
+
// $FlowFixMe[prop-missing]
|
|
283
|
+
// $FlowFixMe[incompatible-call]
|
|
284
|
+
return Transformations[transform.type].apply(transform);
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
append: function (transformList, newTransform) {
|
|
288
|
+
// Append newTransform to transformList, and collapse the last
|
|
289
|
+
// two transforms if they are collapsable
|
|
290
|
+
const results = TransformOps._appendAndCollapseLastTwo(
|
|
291
|
+
transformList,
|
|
292
|
+
newTransform,
|
|
293
|
+
);
|
|
294
|
+
// Collapse any no-ops at the end of the transformation list
|
|
295
|
+
return TransformOps._collapseFinalNoOps(results);
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
_collapseFinalNoOps: function (transforms) {
|
|
299
|
+
// Collapse no-op transformations at the end of the list
|
|
300
|
+
if (transforms.length && TransformOps.isNoOp(_.last(transforms))) {
|
|
301
|
+
return _.initial(transforms);
|
|
302
|
+
}
|
|
303
|
+
return transforms;
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
_appendAndCollapseLastTwo: function (transformList, newTransform) {
|
|
307
|
+
if (!transformList.length) {
|
|
308
|
+
return [newTransform];
|
|
309
|
+
}
|
|
310
|
+
const collapsed = TransformOps.collapse(
|
|
311
|
+
_.last(transformList),
|
|
312
|
+
newTransform,
|
|
313
|
+
);
|
|
314
|
+
return _.initial(transformList).concat(collapsed);
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
isNoOp: function (transform) {
|
|
318
|
+
return Transformations[transform.type].isNoOp(transform);
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
collapse: function (transform1, transform2) {
|
|
322
|
+
// We can only collapse transforms that have the same type
|
|
323
|
+
if (transform1.type !== transform2.type) {
|
|
324
|
+
return [transform1, transform2];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Clicking the button again removes empty transformations
|
|
328
|
+
if (
|
|
329
|
+
TransformOps.isEmpty(transform1) &&
|
|
330
|
+
TransformOps.isEmpty(transform2)
|
|
331
|
+
) {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Don't collapse invalid transformations otherwise
|
|
336
|
+
if (
|
|
337
|
+
!TransformOps.isValid(transform1) ||
|
|
338
|
+
!TransformOps.isValid(transform2)
|
|
339
|
+
) {
|
|
340
|
+
return [transform1, transform2];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return TransformOps._collapseValidMonotypedTransforms(
|
|
344
|
+
transform1,
|
|
345
|
+
transform2,
|
|
346
|
+
);
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
isValid: function (transform: TransformerTransformation): boolean {
|
|
350
|
+
// $FlowFixMe[prop-missing]
|
|
351
|
+
// $FlowFixMe[incompatible-call]
|
|
352
|
+
return Transformations[transform.type].isValid(transform);
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
isEmpty: function (transform: TransformerTransformation): boolean {
|
|
356
|
+
// $FlowFixMe[prop-missing]
|
|
357
|
+
// $FlowFixMe[incompatible-call]
|
|
358
|
+
return Transformations[transform.type].isEmpty(transform);
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
_collapseValidMonotypedTransforms: function (transform1, transform2) {
|
|
362
|
+
let collapsed = Transformations[transform1.type].collapse(
|
|
363
|
+
transform1,
|
|
364
|
+
transform2,
|
|
365
|
+
);
|
|
366
|
+
if (collapsed) {
|
|
367
|
+
// Force all answers into an array
|
|
368
|
+
if (!_.isArray(collapsed)) {
|
|
369
|
+
collapsed = [collapsed];
|
|
370
|
+
}
|
|
371
|
+
// Add types to all transforms in the answer
|
|
372
|
+
_.each(collapsed, function (transform) {
|
|
373
|
+
transform.type = transform1.type;
|
|
374
|
+
});
|
|
375
|
+
return collapsed;
|
|
376
|
+
}
|
|
377
|
+
// These transforms can't be collapsed together
|
|
378
|
+
return [transform1, transform2];
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
toTeX: function (transform) {
|
|
382
|
+
return Transformations[transform.type].toTeX(transform);
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
/* A react representation of this transform object */
|
|
386
|
+
ListItem: createReactClass({
|
|
387
|
+
displayName: "ListItem",
|
|
388
|
+
|
|
389
|
+
render: function () {
|
|
390
|
+
if (this.props.mode === "dynamic") {
|
|
391
|
+
return <div>{TransformOps.toTeX(this.props.transform)}</div>;
|
|
392
|
+
}
|
|
393
|
+
if (this.props.mode === "interactive") {
|
|
394
|
+
const TransformClass =
|
|
395
|
+
Transformations[this.props.transform.type].Input;
|
|
396
|
+
return (
|
|
397
|
+
<TransformClass
|
|
398
|
+
// eslint-disable-next-line react/no-string-refs
|
|
399
|
+
ref="transform"
|
|
400
|
+
onChange={this.handleChange}
|
|
401
|
+
onFocus={this.props.onFocus}
|
|
402
|
+
onBlur={this.props.onBlur}
|
|
403
|
+
keypadElement={this.props.keypadElement}
|
|
404
|
+
apiOptions={this.props.apiOptions}
|
|
405
|
+
{...this.props.transform}
|
|
406
|
+
/>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
throw new PerseusError(
|
|
410
|
+
"Invalid mode: " + this.props.mode,
|
|
411
|
+
Errors.InvalidInput,
|
|
412
|
+
);
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
value: function () {
|
|
416
|
+
if (this.props.mode === "interactive") {
|
|
417
|
+
return _.extend(
|
|
418
|
+
{
|
|
419
|
+
type: this.props.transform.type,
|
|
420
|
+
},
|
|
421
|
+
// eslint-disable-next-line react/no-string-refs
|
|
422
|
+
this.refs.transform.value(),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
return this.props.transform;
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
handleChange: _.debounce(function (callback) {
|
|
429
|
+
this.props.onChange(this.value(), callback);
|
|
430
|
+
}, RENDER_TRANSFORM_DELAY_IN_MS),
|
|
431
|
+
|
|
432
|
+
/* InputPath API: depending on the API call, this could involve simply
|
|
433
|
+
* navigating to the right ref and calling the function on that
|
|
434
|
+
* component, or threading the call down and returning the result. */
|
|
435
|
+
_getComponentAtPath: function (path) {
|
|
436
|
+
// eslint-disable-next-line react/no-string-refs
|
|
437
|
+
const transform = this.refs.transform;
|
|
438
|
+
const ref = _.head(path);
|
|
439
|
+
return transform.refs[ref];
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
focus: function () {
|
|
443
|
+
// eslint-disable-next-line react/no-string-refs
|
|
444
|
+
const transform = this.refs.transform;
|
|
445
|
+
const path = _.head(transform.getInputPaths());
|
|
446
|
+
if (path) {
|
|
447
|
+
this.focusInputPath(path);
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
|
|
451
|
+
focusInputPath: function (path) {
|
|
452
|
+
this._getComponentAtPath(path).focus();
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
blurInputPath: function (path) {
|
|
456
|
+
this._getComponentAtPath(path).blur();
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
getDOMNodeForPath: function (path) {
|
|
460
|
+
return ReactDOM.findDOMNode(this._getComponentAtPath(path));
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
getGrammarTypeForPath: function (path) {
|
|
464
|
+
return "number";
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
setInputValue: function (path, value: string | number, cb) {
|
|
468
|
+
// `value` comes in as a string on mobile, but we need a number
|
|
469
|
+
// We let through the empty string so that "Clear" works -- in
|
|
470
|
+
// that case, the transformer widget will just act as if there is
|
|
471
|
+
// no input, which is what we want.
|
|
472
|
+
if (value.length) {
|
|
473
|
+
value = parseFloat(value);
|
|
474
|
+
if (isNaN(value)) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// eslint-disable-next-line react/no-string-refs
|
|
479
|
+
this.refs.transform.setInputValue(path, value, cb);
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
getInputPaths: function () {
|
|
483
|
+
// If we're in dynamic mode, then the list items are made up of
|
|
484
|
+
// static text.
|
|
485
|
+
if (this.props.mode === "dynamic") {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
// eslint-disable-next-line react/no-string-refs
|
|
489
|
+
return this.refs.transform.getInputPaths();
|
|
490
|
+
},
|
|
491
|
+
}),
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const Transformations = {
|
|
495
|
+
translation: {
|
|
496
|
+
// I18N: As in the command, "Translate the polygon"
|
|
497
|
+
verbName: i18n._("Translate"),
|
|
498
|
+
nounName: i18n._("Translation"),
|
|
499
|
+
lowerNounName: i18n._("translation"),
|
|
500
|
+
apply: function (transform: TranslationTransformation) {
|
|
501
|
+
return function (coord) {
|
|
502
|
+
return kvector.add(coord, transform.vector);
|
|
503
|
+
};
|
|
504
|
+
},
|
|
505
|
+
isValid: function (transform: TranslationTransformation) {
|
|
506
|
+
return (
|
|
507
|
+
_.isFinite(transform.vector[0]) &&
|
|
508
|
+
_.isFinite(transform.vector[1])
|
|
509
|
+
);
|
|
510
|
+
},
|
|
511
|
+
isEmpty: function (transform: TranslationTransformation) {
|
|
512
|
+
return transform.vector[0] === null && transform.vector[1] === null;
|
|
513
|
+
},
|
|
514
|
+
isNoOp: function (transform) {
|
|
515
|
+
return kvector.equal(transform.vector, [0, 0]);
|
|
516
|
+
},
|
|
517
|
+
collapse: function (
|
|
518
|
+
transform1: TranslationTransformation,
|
|
519
|
+
transform2: TranslationTransformation,
|
|
520
|
+
) {
|
|
521
|
+
return {
|
|
522
|
+
vector: kvector.add(transform1.vector, transform2.vector),
|
|
523
|
+
};
|
|
524
|
+
},
|
|
525
|
+
toTeX: function (transform: TranslationTransformation) {
|
|
526
|
+
// I18N: As in the command, "Translation by <3, 1>"
|
|
527
|
+
return i18n.$_("Translation by %(vector)s", {
|
|
528
|
+
vector: texFromVector(transform.vector),
|
|
529
|
+
});
|
|
530
|
+
},
|
|
531
|
+
Input: class extends React.Component<$FlowFixMe, $FlowFixMe> {
|
|
532
|
+
state = {
|
|
533
|
+
vector: this.props.vector || [null, null],
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
componentDidUpdate(prevProps) {
|
|
537
|
+
if (!deepEq(this.props, prevProps)) {
|
|
538
|
+
this.setState({vector: this.props.vector});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
render(): React.Node {
|
|
543
|
+
const InputComponent = inputComponentForApiOptions(
|
|
544
|
+
this.props.apiOptions,
|
|
545
|
+
);
|
|
546
|
+
const {TeX} = getDependencies();
|
|
547
|
+
|
|
548
|
+
const vector = [
|
|
549
|
+
<TeX key="<">{i18n.doNotTranslate("\\langle")}</TeX>,
|
|
550
|
+
<InputComponent
|
|
551
|
+
key="vec_x"
|
|
552
|
+
// eslint-disable-next-line react/no-string-refs
|
|
553
|
+
ref="x"
|
|
554
|
+
placeholder={0}
|
|
555
|
+
value={this.state.vector[0]}
|
|
556
|
+
useArrowKeys={true}
|
|
557
|
+
onChange={(val0) => {
|
|
558
|
+
const val1 = this.state.vector[1];
|
|
559
|
+
this.setState({vector: [val0, val1]}, () => {
|
|
560
|
+
this.props.onChange();
|
|
561
|
+
});
|
|
562
|
+
}}
|
|
563
|
+
onFocus={_.partial(this.props.onFocus, "x")}
|
|
564
|
+
onBlur={_.partial(this.props.onBlur, "x")}
|
|
565
|
+
keypadElement={this.props.keypadElement}
|
|
566
|
+
/>,
|
|
567
|
+
<TeX key=",">{", {}"}</TeX>,
|
|
568
|
+
<InputComponent
|
|
569
|
+
key="vec_y"
|
|
570
|
+
// eslint-disable-next-line react/no-string-refs
|
|
571
|
+
ref="y"
|
|
572
|
+
placeholder={0}
|
|
573
|
+
value={this.state.vector[1]}
|
|
574
|
+
useArrowKeys={true}
|
|
575
|
+
onChange={(val1) => {
|
|
576
|
+
const val0 = this.state.vector[0];
|
|
577
|
+
this.setState({vector: [val0, val1]}, () => {
|
|
578
|
+
this.props.onChange();
|
|
579
|
+
});
|
|
580
|
+
}}
|
|
581
|
+
onFocus={_.partial(this.props.onFocus, "y")}
|
|
582
|
+
onBlur={_.partial(this.props.onBlur, "y")}
|
|
583
|
+
keypadElement={this.props.keypadElement}
|
|
584
|
+
/>,
|
|
585
|
+
<TeX key=">">{i18n.doNotTranslate("\\rangle")}</TeX>,
|
|
586
|
+
];
|
|
587
|
+
return (
|
|
588
|
+
<div>
|
|
589
|
+
{i18n.$_("Translation by %(vector)s", {vector: vector})}
|
|
590
|
+
</div>
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
value = () => {
|
|
595
|
+
// eslint-disable-next-line react/no-string-refs
|
|
596
|
+
const x = this.refs.x.getValue();
|
|
597
|
+
// eslint-disable-next-line react/no-string-refs
|
|
598
|
+
const y = this.refs.y.getValue();
|
|
599
|
+
return {
|
|
600
|
+
vector: [x, y],
|
|
601
|
+
};
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
/* InputPath API */
|
|
605
|
+
setInputValue = (path, value, cb) => {
|
|
606
|
+
const id = _.first(path);
|
|
607
|
+
const vector = _.clone(this.state.vector);
|
|
608
|
+
if (id === "x") {
|
|
609
|
+
vector[0] = value;
|
|
610
|
+
} else if (id === "y") {
|
|
611
|
+
vector[1] = value;
|
|
612
|
+
}
|
|
613
|
+
this.setState({vector: vector}, () => {
|
|
614
|
+
this.props.onChange(cb);
|
|
615
|
+
});
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
getInputPaths = () => {
|
|
619
|
+
return [["x"], ["y"]];
|
|
620
|
+
};
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
|
|
624
|
+
rotation: {
|
|
625
|
+
// I18N: As in the command, "Rotate the polygon"
|
|
626
|
+
verbName: i18n._("Rotate"),
|
|
627
|
+
nounName: i18n._("Rotation"),
|
|
628
|
+
lowerNounName: i18n._("rotation"),
|
|
629
|
+
apply: function (transform: RotationTransformation) {
|
|
630
|
+
return function (coord) {
|
|
631
|
+
return kpoint.rotateDeg(
|
|
632
|
+
coord,
|
|
633
|
+
transform.angleDeg,
|
|
634
|
+
transform.center,
|
|
635
|
+
);
|
|
636
|
+
};
|
|
637
|
+
},
|
|
638
|
+
isValid: function (transform: RotationTransformation) {
|
|
639
|
+
return (
|
|
640
|
+
_.isFinite(transform.angleDeg) &&
|
|
641
|
+
_.isFinite(transform.center[0]) &&
|
|
642
|
+
_.isFinite(transform.center[1])
|
|
643
|
+
);
|
|
644
|
+
},
|
|
645
|
+
isEmpty: function (transform: RotationTransformation) {
|
|
646
|
+
return (
|
|
647
|
+
transform.angleDeg === null &&
|
|
648
|
+
transform.center[0] === null &&
|
|
649
|
+
transform.center[1] === null
|
|
650
|
+
);
|
|
651
|
+
},
|
|
652
|
+
isNoOp: function (transform: RotationTransformation) {
|
|
653
|
+
return knumber.equal(transform.angleDeg, 0);
|
|
654
|
+
},
|
|
655
|
+
collapse: function (
|
|
656
|
+
transform1: RotationTransformation,
|
|
657
|
+
transform2: RotationTransformation,
|
|
658
|
+
) {
|
|
659
|
+
if (!kpoint.equal(transform1.center, transform2.center)) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
center: transform1.center,
|
|
664
|
+
angleDeg: transform1.angleDeg + transform2.angleDeg,
|
|
665
|
+
};
|
|
666
|
+
},
|
|
667
|
+
toTeX: function (transform: RotationTransformation) {
|
|
668
|
+
return i18n.$_("Rotation by %(degrees)s about %(point)s", {
|
|
669
|
+
degrees: texFromAngleDeg(transform.angleDeg),
|
|
670
|
+
point: texFromPoint(transform.center),
|
|
671
|
+
});
|
|
672
|
+
},
|
|
673
|
+
Input: class extends React.Component<$FlowFixMe, $FlowFixMe> {
|
|
674
|
+
state = {
|
|
675
|
+
center: this.props.center || [null, null],
|
|
676
|
+
angleDeg: this.props.angleDeg || null,
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
componentDidUpdate(prevProps) {
|
|
680
|
+
if (!deepEq(this.props, prevProps)) {
|
|
681
|
+
this.setState({
|
|
682
|
+
center: this.props.center,
|
|
683
|
+
angleDeg: this.props.angleDeg,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
render(): React.Node {
|
|
689
|
+
const InputComponent = inputComponentForApiOptions(
|
|
690
|
+
this.props.apiOptions,
|
|
691
|
+
);
|
|
692
|
+
const {TeX} = getDependencies();
|
|
693
|
+
|
|
694
|
+
const point = [
|
|
695
|
+
<TeX key="(">(</TeX>,
|
|
696
|
+
<InputComponent
|
|
697
|
+
key="X"
|
|
698
|
+
// eslint-disable-next-line react/no-string-refs
|
|
699
|
+
ref="centerX"
|
|
700
|
+
placeholder={0}
|
|
701
|
+
value={this.state.center[0]}
|
|
702
|
+
useArrowKeys={true}
|
|
703
|
+
onChange={(val0) => {
|
|
704
|
+
const val1 = this.state.center[1];
|
|
705
|
+
this.setState({center: [val0, val1]}, () => {
|
|
706
|
+
this.props.onChange();
|
|
707
|
+
});
|
|
708
|
+
}}
|
|
709
|
+
onFocus={_.partial(this.props.onFocus, "centerX")}
|
|
710
|
+
onBlur={_.partial(this.props.onBlur, "centerX")}
|
|
711
|
+
keypadElement={this.props.keypadElement}
|
|
712
|
+
/>,
|
|
713
|
+
<TeX key=",">{", {}"}</TeX>,
|
|
714
|
+
<InputComponent
|
|
715
|
+
key="Y"
|
|
716
|
+
// eslint-disable-next-line react/no-string-refs
|
|
717
|
+
ref="centerY"
|
|
718
|
+
placeholder={0}
|
|
719
|
+
value={this.state.center[1]}
|
|
720
|
+
useArrowKeys={true}
|
|
721
|
+
onChange={(val1) => {
|
|
722
|
+
const val0 = this.state.center[0];
|
|
723
|
+
this.setState({center: [val0, val1]}, () => {
|
|
724
|
+
this.props.onChange();
|
|
725
|
+
});
|
|
726
|
+
}}
|
|
727
|
+
onFocus={_.partial(this.props.onFocus, "centerY")}
|
|
728
|
+
onBlur={_.partial(this.props.onBlur, "centerY")}
|
|
729
|
+
keypadElement={this.props.keypadElement}
|
|
730
|
+
/>,
|
|
731
|
+
<TeX key=")">)</TeX>,
|
|
732
|
+
];
|
|
733
|
+
const degrees = [
|
|
734
|
+
<InputComponent
|
|
735
|
+
key="deg"
|
|
736
|
+
// eslint-disable-next-line react/no-string-refs
|
|
737
|
+
ref="angleDeg"
|
|
738
|
+
placeholder={0}
|
|
739
|
+
value={this.state.angleDeg}
|
|
740
|
+
useArrowKeys={true}
|
|
741
|
+
onChange={(val) => {
|
|
742
|
+
this.setState({angleDeg: val}, () => {
|
|
743
|
+
this.props.onChange();
|
|
744
|
+
});
|
|
745
|
+
}}
|
|
746
|
+
onFocus={_.partial(this.props.onFocus, "angleDeg")}
|
|
747
|
+
onBlur={_.partial(this.props.onBlur, "angleDeg")}
|
|
748
|
+
keypadElement={this.props.keypadElement}
|
|
749
|
+
/>,
|
|
750
|
+
DEGREE_SIGN,
|
|
751
|
+
];
|
|
752
|
+
// I18N: %(point)s must come before %(degrees)s in this phrase
|
|
753
|
+
const text = i18n.$_(
|
|
754
|
+
"Rotation about %(point)s by %(degrees)s",
|
|
755
|
+
{
|
|
756
|
+
point,
|
|
757
|
+
degrees,
|
|
758
|
+
},
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
return <div>{text}</div>;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
value = () => {
|
|
765
|
+
// eslint-disable-next-line react/no-string-refs
|
|
766
|
+
const angleDeg = this.refs.angleDeg.getValue();
|
|
767
|
+
// eslint-disable-next-line react/no-string-refs
|
|
768
|
+
const centerX = this.refs.centerX.getValue();
|
|
769
|
+
// eslint-disable-next-line react/no-string-refs
|
|
770
|
+
const centerY = this.refs.centerY.getValue();
|
|
771
|
+
return {
|
|
772
|
+
angleDeg: angleDeg,
|
|
773
|
+
center: [centerX, centerY],
|
|
774
|
+
};
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
/* InputPath API */
|
|
778
|
+
setInputValue = (path, value, cb) => {
|
|
779
|
+
const id = _.first(path);
|
|
780
|
+
let angleDeg = _.clone(this.state.angleDeg);
|
|
781
|
+
const center = _.clone(this.state.center);
|
|
782
|
+
if (id === "angleDeg") {
|
|
783
|
+
angleDeg = value;
|
|
784
|
+
} else if (id === "centerX") {
|
|
785
|
+
center[0] = value;
|
|
786
|
+
} else if (id === "centerY") {
|
|
787
|
+
center[1] = value;
|
|
788
|
+
}
|
|
789
|
+
this.setState({angleDeg: angleDeg, center: center}, () => {
|
|
790
|
+
this.props.onChange(cb);
|
|
791
|
+
});
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
getInputPaths = () => {
|
|
795
|
+
return [["centerX"], ["centerY"], ["angleDeg"]];
|
|
796
|
+
};
|
|
797
|
+
},
|
|
798
|
+
},
|
|
799
|
+
|
|
800
|
+
reflection: {
|
|
801
|
+
// I18N: As in the command, "Reflect the polygon"
|
|
802
|
+
verbName: i18n._("Reflect"),
|
|
803
|
+
nounName: i18n._("Reflection"),
|
|
804
|
+
lowerNounName: i18n._("reflection"),
|
|
805
|
+
apply: function (transform: ReflectionTransformation) {
|
|
806
|
+
return function (coord) {
|
|
807
|
+
return kpoint.reflectOverLine(coord, transform.line);
|
|
808
|
+
};
|
|
809
|
+
},
|
|
810
|
+
isValid: function (transform: ReflectionTransformation) {
|
|
811
|
+
// A bit hacky, but we'll also define reflecting over a
|
|
812
|
+
// single point as a no-op, to avoid NaN fun.
|
|
813
|
+
return (
|
|
814
|
+
_.all(_.flatten(transform.line), _.isFinite) &&
|
|
815
|
+
!kpoint.equal(transform.line[0], transform.line[1])
|
|
816
|
+
);
|
|
817
|
+
},
|
|
818
|
+
isEmpty: function (transform: ReflectionTransformation) {
|
|
819
|
+
return _.all(_.flatten(transform.line), _.isNull);
|
|
820
|
+
},
|
|
821
|
+
isNoOp: function (transform: ReflectionTransformation) {
|
|
822
|
+
// Invalid transforms are implicitly no-ops, so we don't
|
|
823
|
+
// have to catch that case here.
|
|
824
|
+
return false;
|
|
825
|
+
},
|
|
826
|
+
collapse: function (
|
|
827
|
+
transform1: ReflectionTransformation,
|
|
828
|
+
transform2: ReflectionTransformation,
|
|
829
|
+
) {
|
|
830
|
+
// $FlowFixMe[incompatible-call]
|
|
831
|
+
if (!kline.equal(transform1.line, transform2.line)) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
return [];
|
|
835
|
+
},
|
|
836
|
+
toTeX: function (transform) {
|
|
837
|
+
const point1 = transform.line[0];
|
|
838
|
+
const point2 = transform.line[1];
|
|
839
|
+
return i18n.$_(
|
|
840
|
+
"Reflection over the line from %(point1)s to %(point2)s",
|
|
841
|
+
{
|
|
842
|
+
point1: texFromPoint(point1),
|
|
843
|
+
point2: texFromPoint(point2),
|
|
844
|
+
},
|
|
845
|
+
);
|
|
846
|
+
},
|
|
847
|
+
Input: class extends React.Component<$FlowFixMe, $FlowFixMe> {
|
|
848
|
+
state = {
|
|
849
|
+
line: this.props.line || [
|
|
850
|
+
[null, null],
|
|
851
|
+
[null, null],
|
|
852
|
+
],
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
componentDidUpdate(prevProps) {
|
|
856
|
+
if (!deepEq(this.props, prevProps)) {
|
|
857
|
+
this.setState({line: this.props.line});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
render(): React.Node {
|
|
862
|
+
const InputComponent = inputComponentForApiOptions(
|
|
863
|
+
this.props.apiOptions,
|
|
864
|
+
);
|
|
865
|
+
const {TeX} = getDependencies();
|
|
866
|
+
|
|
867
|
+
const point1 = [
|
|
868
|
+
<TeX key="(">(</TeX>,
|
|
869
|
+
<InputComponent
|
|
870
|
+
key="x1"
|
|
871
|
+
// eslint-disable-next-line react/no-string-refs
|
|
872
|
+
ref="x1"
|
|
873
|
+
value={this.state.line[0][0]}
|
|
874
|
+
useArrowKeys={true}
|
|
875
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
876
|
+
onChange={this.changePoint.bind(this, 0, 0)}
|
|
877
|
+
onFocus={_.partial(this.props.onFocus, "x1")}
|
|
878
|
+
onBlur={_.partial(this.props.onBlur, "x1")}
|
|
879
|
+
keypadElement={this.props.keypadElement}
|
|
880
|
+
/>,
|
|
881
|
+
<TeX key=",">{", {}"}</TeX>,
|
|
882
|
+
<InputComponent
|
|
883
|
+
key="y1"
|
|
884
|
+
// eslint-disable-next-line react/no-string-refs
|
|
885
|
+
ref="y1"
|
|
886
|
+
value={this.state.line[0][1]}
|
|
887
|
+
useArrowKeys={true}
|
|
888
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
889
|
+
onChange={this.changePoint.bind(this, 0, 1)}
|
|
890
|
+
onFocus={_.partial(this.props.onFocus, "y1")}
|
|
891
|
+
onBlur={_.partial(this.props.onBlur, "y1")}
|
|
892
|
+
keypadElement={this.props.keypadElement}
|
|
893
|
+
/>,
|
|
894
|
+
<TeX key=")">)</TeX>,
|
|
895
|
+
];
|
|
896
|
+
const point2 = [
|
|
897
|
+
<TeX key="(">(</TeX>,
|
|
898
|
+
<InputComponent
|
|
899
|
+
key="x2"
|
|
900
|
+
// eslint-disable-next-line react/no-string-refs
|
|
901
|
+
ref="x2"
|
|
902
|
+
value={this.state.line[1][0]}
|
|
903
|
+
useArrowKeys={true}
|
|
904
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
905
|
+
onChange={this.changePoint.bind(this, 1, 0)}
|
|
906
|
+
onFocus={_.partial(this.props.onFocus, "x2")}
|
|
907
|
+
onBlur={_.partial(this.props.onBlur, "x2")}
|
|
908
|
+
keypadElement={this.props.keypadElement}
|
|
909
|
+
/>,
|
|
910
|
+
<TeX key=",">{", {}"}</TeX>,
|
|
911
|
+
<InputComponent
|
|
912
|
+
key="y2"
|
|
913
|
+
// eslint-disable-next-line react/no-string-refs
|
|
914
|
+
ref="y2"
|
|
915
|
+
value={this.state.line[1][1]}
|
|
916
|
+
useArrowKeys={true}
|
|
917
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
918
|
+
onChange={this.changePoint.bind(this, 1, 1)}
|
|
919
|
+
onFocus={_.partial(this.props.onFocus, "y2")}
|
|
920
|
+
onBlur={_.partial(this.props.onBlur, "y2")}
|
|
921
|
+
keypadElement={this.props.keypadElement}
|
|
922
|
+
/>,
|
|
923
|
+
<TeX key=")">)</TeX>,
|
|
924
|
+
];
|
|
925
|
+
return (
|
|
926
|
+
<div>
|
|
927
|
+
{i18n.$_(
|
|
928
|
+
"Reflection over the line from " +
|
|
929
|
+
"%(point1)s to %(point2)s",
|
|
930
|
+
{point1, point2},
|
|
931
|
+
)}
|
|
932
|
+
</div>
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
changePoint = (i, j, val, cb) => {
|
|
937
|
+
const line = _.map(this.state.line, _.clone);
|
|
938
|
+
line[i][j] = val;
|
|
939
|
+
this.setState({line: line}, () => {
|
|
940
|
+
this.props.onChange(cb);
|
|
941
|
+
});
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
value = () => {
|
|
945
|
+
// eslint-disable-next-line react/no-string-refs
|
|
946
|
+
const x1 = this.refs.x1.getValue();
|
|
947
|
+
// eslint-disable-next-line react/no-string-refs
|
|
948
|
+
const y1 = this.refs.y1.getValue();
|
|
949
|
+
// eslint-disable-next-line react/no-string-refs
|
|
950
|
+
const x2 = this.refs.x2.getValue();
|
|
951
|
+
// eslint-disable-next-line react/no-string-refs
|
|
952
|
+
const y2 = this.refs.y2.getValue();
|
|
953
|
+
return {
|
|
954
|
+
line: [
|
|
955
|
+
[x1, y1],
|
|
956
|
+
[x2, y2],
|
|
957
|
+
],
|
|
958
|
+
};
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
/* InputPath API */
|
|
962
|
+
setInputValue = (path, value, cb) => {
|
|
963
|
+
const id = _.first(path);
|
|
964
|
+
let j;
|
|
965
|
+
if (id[0] === "x") {
|
|
966
|
+
j = 0;
|
|
967
|
+
} else if (id[0] === "y") {
|
|
968
|
+
j = 1;
|
|
969
|
+
}
|
|
970
|
+
let i;
|
|
971
|
+
if (id[1] === "1") {
|
|
972
|
+
i = 0;
|
|
973
|
+
} else if (id[1] === "2") {
|
|
974
|
+
i = 1;
|
|
975
|
+
}
|
|
976
|
+
this.changePoint(i, j, value, cb);
|
|
977
|
+
};
|
|
978
|
+
|
|
979
|
+
getInputPaths = () => {
|
|
980
|
+
return [["x1"], ["y1"], ["x2"], ["y2"]];
|
|
981
|
+
};
|
|
982
|
+
},
|
|
983
|
+
},
|
|
984
|
+
|
|
985
|
+
dilation: {
|
|
986
|
+
// I18N: As in the command, "Dilate the polygon"
|
|
987
|
+
verbName: i18n._("Dilate"),
|
|
988
|
+
nounName: i18n._("Dilation"),
|
|
989
|
+
lowerNounName: i18n._("dilation"),
|
|
990
|
+
apply: function (transform: DilationTransformation) {
|
|
991
|
+
return function (coord) {
|
|
992
|
+
return dilatePointFromCenter(
|
|
993
|
+
coord,
|
|
994
|
+
transform.center,
|
|
995
|
+
transform.scale,
|
|
996
|
+
);
|
|
997
|
+
};
|
|
998
|
+
},
|
|
999
|
+
isValid: function (transform: DilationTransformation) {
|
|
1000
|
+
return (
|
|
1001
|
+
_.isFinite(transform.scale) &&
|
|
1002
|
+
_.isFinite(transform.center[0]) &&
|
|
1003
|
+
_.isFinite(transform.center[1])
|
|
1004
|
+
);
|
|
1005
|
+
},
|
|
1006
|
+
isEmpty: function (transform: DilationTransformation) {
|
|
1007
|
+
return (
|
|
1008
|
+
transform.scale === null &&
|
|
1009
|
+
transform.center[0] === null &&
|
|
1010
|
+
transform.center[1] === null
|
|
1011
|
+
);
|
|
1012
|
+
},
|
|
1013
|
+
isNoOp: function (transform: DilationTransformation) {
|
|
1014
|
+
return knumber.equal(transform.scale, 1);
|
|
1015
|
+
},
|
|
1016
|
+
collapse: function (
|
|
1017
|
+
transform1: DilationTransformation,
|
|
1018
|
+
transform2: DilationTransformation,
|
|
1019
|
+
) {
|
|
1020
|
+
if (!kpoint.equal(transform1.center, transform2.center)) {
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
return {
|
|
1024
|
+
center: transform1.center,
|
|
1025
|
+
scale: transform1.scale * transform2.scale,
|
|
1026
|
+
};
|
|
1027
|
+
},
|
|
1028
|
+
toTeX: function (transform: DilationTransformation) {
|
|
1029
|
+
const scaleString = stringFromFraction(transform.scale);
|
|
1030
|
+
return i18n.$_("Dilation of scale %(scale)s about %(point)s", {
|
|
1031
|
+
scale: scaleString,
|
|
1032
|
+
point: texFromPoint(transform.center),
|
|
1033
|
+
});
|
|
1034
|
+
},
|
|
1035
|
+
Input: class extends React.Component<$FlowFixMe, $FlowFixMe> {
|
|
1036
|
+
state = {
|
|
1037
|
+
center: this.props.center || [null, null],
|
|
1038
|
+
scale: this.props.scale || null,
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
componentDidUpdate(prevProps) {
|
|
1042
|
+
if (!deepEq(this.props, prevProps)) {
|
|
1043
|
+
this.setState({
|
|
1044
|
+
center: this.props.center,
|
|
1045
|
+
scale: this.props.scale,
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
render(): React.Node {
|
|
1051
|
+
const InputComponent = inputComponentForApiOptions(
|
|
1052
|
+
this.props.apiOptions,
|
|
1053
|
+
);
|
|
1054
|
+
const {TeX} = getDependencies();
|
|
1055
|
+
|
|
1056
|
+
const point = [
|
|
1057
|
+
<TeX key="(">(</TeX>,
|
|
1058
|
+
<InputComponent
|
|
1059
|
+
key="point_x"
|
|
1060
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1061
|
+
ref="x"
|
|
1062
|
+
placeholder={0}
|
|
1063
|
+
value={this.state.center[0]}
|
|
1064
|
+
useArrowKeys={true}
|
|
1065
|
+
onChange={(val0) => {
|
|
1066
|
+
const val1 = this.state.center[1];
|
|
1067
|
+
this.setState({center: [val0, val1]}, () => {
|
|
1068
|
+
this.props.onChange();
|
|
1069
|
+
});
|
|
1070
|
+
}}
|
|
1071
|
+
onFocus={_.partial(this.props.onFocus, "x")}
|
|
1072
|
+
onBlur={_.partial(this.props.onBlur, "x")}
|
|
1073
|
+
keypadElement={this.props.keypadElement}
|
|
1074
|
+
/>,
|
|
1075
|
+
<TeX key=",">{", {}"}</TeX>,
|
|
1076
|
+
<InputComponent
|
|
1077
|
+
key="point_y"
|
|
1078
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1079
|
+
ref="y"
|
|
1080
|
+
placeholder={0}
|
|
1081
|
+
value={this.state.center[1]}
|
|
1082
|
+
useArrowKeys={true}
|
|
1083
|
+
onChange={(val1) => {
|
|
1084
|
+
const val0 = this.state.center[0];
|
|
1085
|
+
this.setState({center: [val0, val1]}, () => {
|
|
1086
|
+
this.props.onChange();
|
|
1087
|
+
});
|
|
1088
|
+
}}
|
|
1089
|
+
onFocus={_.partial(this.props.onFocus, "y")}
|
|
1090
|
+
onBlur={_.partial(this.props.onBlur, "y")}
|
|
1091
|
+
keypadElement={this.props.keypadElement}
|
|
1092
|
+
/>,
|
|
1093
|
+
<TeX key=")">)</TeX>,
|
|
1094
|
+
];
|
|
1095
|
+
const scale = (
|
|
1096
|
+
<InputComponent
|
|
1097
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1098
|
+
ref="scale"
|
|
1099
|
+
placeholder={1}
|
|
1100
|
+
value={this.state.scale}
|
|
1101
|
+
useArrowKeys={true}
|
|
1102
|
+
onChange={(val) => {
|
|
1103
|
+
this.setState({scale: val}, () => {
|
|
1104
|
+
this.props.onChange();
|
|
1105
|
+
});
|
|
1106
|
+
}}
|
|
1107
|
+
onFocus={_.partial(this.props.onFocus, "scale")}
|
|
1108
|
+
onBlur={_.partial(this.props.onBlur, "scale")}
|
|
1109
|
+
keypadElement={this.props.keypadElement}
|
|
1110
|
+
/>
|
|
1111
|
+
);
|
|
1112
|
+
return (
|
|
1113
|
+
<div>
|
|
1114
|
+
{i18n.$_("Dilation about %(point)s by %(scale)s", {
|
|
1115
|
+
point,
|
|
1116
|
+
scale,
|
|
1117
|
+
})}
|
|
1118
|
+
</div>
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
value = () => {
|
|
1123
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1124
|
+
const scale = this.refs.scale.getValue();
|
|
1125
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1126
|
+
const x = this.refs.x.getValue();
|
|
1127
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1128
|
+
const y = this.refs.y.getValue();
|
|
1129
|
+
return {
|
|
1130
|
+
scale: scale,
|
|
1131
|
+
center: [x, y],
|
|
1132
|
+
};
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
/* InputPath API */
|
|
1136
|
+
setInputValue = (path, value, cb) => {
|
|
1137
|
+
const id = _.first(path);
|
|
1138
|
+
let scale = this.state.scale;
|
|
1139
|
+
const center = _.clone(this.state.center);
|
|
1140
|
+
if (id === "x") {
|
|
1141
|
+
center[0] = value;
|
|
1142
|
+
} else if (id === "y") {
|
|
1143
|
+
center[1] = value;
|
|
1144
|
+
} else if (id === "scale") {
|
|
1145
|
+
scale = value;
|
|
1146
|
+
}
|
|
1147
|
+
this.setState({scale: scale, center: center}, () => {
|
|
1148
|
+
this.props.onChange(cb);
|
|
1149
|
+
});
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
getInputPaths = () => {
|
|
1153
|
+
return [["x"], ["y"], ["scale"]];
|
|
1154
|
+
};
|
|
1155
|
+
},
|
|
1156
|
+
},
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
/* Various functions to deal with different shape types */
|
|
1160
|
+
const ShapeTypes = {
|
|
1161
|
+
getPointCountForType: function (type) {
|
|
1162
|
+
const splitType = type.split("-");
|
|
1163
|
+
if (splitType[0] === "polygon") {
|
|
1164
|
+
return splitType[1] || 3;
|
|
1165
|
+
}
|
|
1166
|
+
if (splitType[0] === "line" || splitType[0] === "lineSegment") {
|
|
1167
|
+
return 2;
|
|
1168
|
+
}
|
|
1169
|
+
if (splitType[0] === "angle") {
|
|
1170
|
+
return 3;
|
|
1171
|
+
}
|
|
1172
|
+
if (splitType[0] === "circle") {
|
|
1173
|
+
return 2;
|
|
1174
|
+
}
|
|
1175
|
+
if (splitType[0] === "point") {
|
|
1176
|
+
return 1;
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
|
|
1180
|
+
addMovableShape: function (graphie, options) {
|
|
1181
|
+
if (options.editable && options.translatable) {
|
|
1182
|
+
throw new PerseusError(
|
|
1183
|
+
"It doesn't make sense to have a movable shape " +
|
|
1184
|
+
"where you can stretch the points and translate them " +
|
|
1185
|
+
"simultaneously. options: " +
|
|
1186
|
+
JSON.stringify(options),
|
|
1187
|
+
Errors.InvalidInput,
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const points = _.map(options.shape.coords, function (coord) {
|
|
1192
|
+
let isMoving = false;
|
|
1193
|
+
let previousCoord = coord;
|
|
1194
|
+
|
|
1195
|
+
const onMove = function (x, y) {
|
|
1196
|
+
if (!isMoving) {
|
|
1197
|
+
previousCoord = currentPoint.coord;
|
|
1198
|
+
isMoving = true;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
let moveVector = kvector.subtract([x, y], currentPoint.coord);
|
|
1202
|
+
|
|
1203
|
+
// Translate from (x, y) semantics to (dX, dY) semantics
|
|
1204
|
+
// This is more useful for translations on multiple points,
|
|
1205
|
+
// where we care about how the points moved, not where any
|
|
1206
|
+
// individual point ended up
|
|
1207
|
+
if (options.onMove) {
|
|
1208
|
+
moveVector = options.onMove(moveVector[0], moveVector[1]);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Perform a translation on all points in this shape when
|
|
1212
|
+
// any point moves
|
|
1213
|
+
if (options.translatable) {
|
|
1214
|
+
_.each(points, function (point) {
|
|
1215
|
+
// The point itself will be updated by the
|
|
1216
|
+
// movablePoint class, so only translate the other
|
|
1217
|
+
// points
|
|
1218
|
+
if (point !== currentPoint) {
|
|
1219
|
+
point.setCoord(
|
|
1220
|
+
kvector.add(point.coord, moveVector),
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Update our shape and our currentPoint
|
|
1227
|
+
// Without this, some shapes (circles, angles) appear
|
|
1228
|
+
// "bouncy" as they are updated with currentPoint at the
|
|
1229
|
+
// current mouse coordinate (oldCoord), rather than newCoord
|
|
1230
|
+
const oldCoord = currentPoint.coord;
|
|
1231
|
+
const newCoord = kvector.add(currentPoint.coord, moveVector);
|
|
1232
|
+
// Temporarily change our coordinate so that
|
|
1233
|
+
// shape.update() sees the new coordinate
|
|
1234
|
+
currentPoint.coord = newCoord;
|
|
1235
|
+
shape.update();
|
|
1236
|
+
// ...But don't break onMove, which assumes it
|
|
1237
|
+
// is the only thing changing our coord
|
|
1238
|
+
currentPoint.coord = oldCoord;
|
|
1239
|
+
return newCoord;
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
const onMoveEnd = function () {
|
|
1243
|
+
// onMove isn't guaranteed to be called before onMoveEnd, so
|
|
1244
|
+
// we have to take into account that we may not have moved and
|
|
1245
|
+
// set previousCoord.
|
|
1246
|
+
// $FlowFixMe[prop-missing]
|
|
1247
|
+
if (options.onMoveEnd && isMoving) {
|
|
1248
|
+
isMoving = false;
|
|
1249
|
+
// We don't use the supplied x and y parameters here
|
|
1250
|
+
// because MovablePoint's onMoveEnd semantics suck.
|
|
1251
|
+
// It returns the mouseX, mouseY without processing them
|
|
1252
|
+
// through onMove, leaving us with weird fractional moves
|
|
1253
|
+
const change = kvector.subtract(
|
|
1254
|
+
currentPoint.coord,
|
|
1255
|
+
previousCoord,
|
|
1256
|
+
);
|
|
1257
|
+
options.onMoveEnd(change[0], change[1]);
|
|
1258
|
+
}
|
|
1259
|
+
shape.update();
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
const currentPoint = graphie.addMovablePoint({
|
|
1263
|
+
coord: coord,
|
|
1264
|
+
normalStyle: options.normalPointStyle,
|
|
1265
|
+
highlightStyle: options.highlightPointStyle,
|
|
1266
|
+
constraints: {
|
|
1267
|
+
fixed: !options.translatable && !options.editable,
|
|
1268
|
+
},
|
|
1269
|
+
visible: options.showPoints,
|
|
1270
|
+
// $FlowFixMe[prop-missing]
|
|
1271
|
+
snapX: (options.snap && options.snap[0]) || 0,
|
|
1272
|
+
// $FlowFixMe[prop-missing]
|
|
1273
|
+
snapY: (options.snap && options.snap[1]) || 0,
|
|
1274
|
+
bounded: false, // Don't bound it when placing it on the graph
|
|
1275
|
+
onMove: onMove,
|
|
1276
|
+
onMoveEnd: onMoveEnd,
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
// Bound it when moving
|
|
1280
|
+
// We can't set this earlier, because doing so would mean any
|
|
1281
|
+
// points outside of the graph would be moved into a moved into
|
|
1282
|
+
// a position that doesn't preserve the shape
|
|
1283
|
+
currentPoint.bounded = true;
|
|
1284
|
+
|
|
1285
|
+
return currentPoint;
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
const shape = ShapeTypes.addShape(graphie, options, points);
|
|
1289
|
+
const removeShapeWithoutPoints = shape.remove;
|
|
1290
|
+
shape.remove = function () {
|
|
1291
|
+
removeShapeWithoutPoints.apply(shape);
|
|
1292
|
+
_.invoke(points, "remove");
|
|
1293
|
+
};
|
|
1294
|
+
return shape;
|
|
1295
|
+
},
|
|
1296
|
+
|
|
1297
|
+
addShape: function (graphie, options, points) {
|
|
1298
|
+
points = points || options.shape.coords;
|
|
1299
|
+
|
|
1300
|
+
const types = ShapeTypes._typesOf(options.shape);
|
|
1301
|
+
const typeOptions =
|
|
1302
|
+
options.shape.options || ShapeTypes.defaultOptions(types);
|
|
1303
|
+
|
|
1304
|
+
const shapes = ShapeTypes._mapTypes(
|
|
1305
|
+
types,
|
|
1306
|
+
points,
|
|
1307
|
+
function (type, points, i) {
|
|
1308
|
+
const shapeOptions = _.extend({}, options, typeOptions[i]);
|
|
1309
|
+
return ShapeTypes._addType(graphie, type, points, shapeOptions);
|
|
1310
|
+
},
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
const updateFuncs = _.filter(_.pluck(shapes, "update"), _.identity);
|
|
1314
|
+
const update = function () {
|
|
1315
|
+
_.invoke(updateFuncs, "call");
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
const removeFuncs = _.filter(_.pluck(shapes, "remove"), _.identity);
|
|
1319
|
+
const remove = function () {
|
|
1320
|
+
_.invoke(removeFuncs, "call");
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
const getOptions = function () {
|
|
1324
|
+
return _.map(shapes, function (shape) {
|
|
1325
|
+
if (shape.getOptions) {
|
|
1326
|
+
return shape.getOptions();
|
|
1327
|
+
}
|
|
1328
|
+
return {};
|
|
1329
|
+
});
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
const toJSON = function () {
|
|
1333
|
+
const coords = _.map(points, function (pt) {
|
|
1334
|
+
if (_.isArray(pt)) {
|
|
1335
|
+
return pt;
|
|
1336
|
+
}
|
|
1337
|
+
return pt.coord;
|
|
1338
|
+
});
|
|
1339
|
+
return {
|
|
1340
|
+
type: types,
|
|
1341
|
+
coords: coords,
|
|
1342
|
+
options: getOptions(),
|
|
1343
|
+
};
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
return {
|
|
1347
|
+
type: types,
|
|
1348
|
+
points: points,
|
|
1349
|
+
update: update,
|
|
1350
|
+
remove: remove,
|
|
1351
|
+
toJSON: toJSON,
|
|
1352
|
+
getOptions: getOptions,
|
|
1353
|
+
};
|
|
1354
|
+
},
|
|
1355
|
+
|
|
1356
|
+
equal: function (shape1, shape2) {
|
|
1357
|
+
const types1 = ShapeTypes._typesOf(shape1);
|
|
1358
|
+
const types2 = ShapeTypes._typesOf(shape2);
|
|
1359
|
+
if (types1.length !== types2.length) {
|
|
1360
|
+
return false;
|
|
1361
|
+
}
|
|
1362
|
+
const shapes1 = ShapeTypes._mapTypes(
|
|
1363
|
+
types1,
|
|
1364
|
+
shape1.coords,
|
|
1365
|
+
ShapeTypes._combine,
|
|
1366
|
+
);
|
|
1367
|
+
const shapes2 = ShapeTypes._mapTypes(
|
|
1368
|
+
types2,
|
|
1369
|
+
shape2.coords,
|
|
1370
|
+
ShapeTypes._combine,
|
|
1371
|
+
);
|
|
1372
|
+
return _.all(
|
|
1373
|
+
_.map(shapes1, function (partialShape1, i) {
|
|
1374
|
+
const partialShape2 = shapes2[i];
|
|
1375
|
+
if (partialShape1.type !== partialShape2.type) {
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
return ShapeTypes._forType(partialShape1.type).equal(
|
|
1379
|
+
partialShape1.coords,
|
|
1380
|
+
partialShape2.coords,
|
|
1381
|
+
);
|
|
1382
|
+
}),
|
|
1383
|
+
);
|
|
1384
|
+
},
|
|
1385
|
+
|
|
1386
|
+
_typesOf: function (shape) {
|
|
1387
|
+
let types = shape.type;
|
|
1388
|
+
if (!_.isArray(types)) {
|
|
1389
|
+
types = [types];
|
|
1390
|
+
}
|
|
1391
|
+
return _.map(types, function (type) {
|
|
1392
|
+
if (type === "polygon") {
|
|
1393
|
+
return "polygon-3";
|
|
1394
|
+
}
|
|
1395
|
+
return type;
|
|
1396
|
+
});
|
|
1397
|
+
},
|
|
1398
|
+
|
|
1399
|
+
defaultOptions: function (types) {
|
|
1400
|
+
return _.map(types, function (type) {
|
|
1401
|
+
const typeDefaultOptions = ShapeTypes._forType(type).defaultOptions;
|
|
1402
|
+
return _.extend({}, typeDefaultOptions);
|
|
1403
|
+
});
|
|
1404
|
+
},
|
|
1405
|
+
|
|
1406
|
+
_forType: function (type) {
|
|
1407
|
+
const baseType = type.split("-")[0];
|
|
1408
|
+
return ShapeTypes[baseType];
|
|
1409
|
+
},
|
|
1410
|
+
|
|
1411
|
+
_mapTypes: function (types, points, func, context) {
|
|
1412
|
+
return _.map(types, function (type, i) {
|
|
1413
|
+
const pointCount = ShapeTypes.getPointCountForType(type);
|
|
1414
|
+
const currentPoints = _.first(points, pointCount);
|
|
1415
|
+
points = _.rest(points, pointCount);
|
|
1416
|
+
// $FlowFixMe[extra-arg]
|
|
1417
|
+
return func.call(context, type, currentPoints, i);
|
|
1418
|
+
});
|
|
1419
|
+
},
|
|
1420
|
+
|
|
1421
|
+
_addType: function (graphie, type, points, options) {
|
|
1422
|
+
const lineCoords = _.isArray(points[0])
|
|
1423
|
+
? {
|
|
1424
|
+
coordA: points[0],
|
|
1425
|
+
coordZ: points[1],
|
|
1426
|
+
}
|
|
1427
|
+
: {
|
|
1428
|
+
pointA: points[0],
|
|
1429
|
+
pointZ: points[1],
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
type = type.split("-")[0];
|
|
1433
|
+
if (type === "polygon") {
|
|
1434
|
+
const polygon = graphie.addMovablePolygon(
|
|
1435
|
+
_.extend({}, options, {
|
|
1436
|
+
fixed: !options.editable,
|
|
1437
|
+
snapX: (options.snap && options.snap[0]) || 0,
|
|
1438
|
+
snapY: (options.snap && options.snap[1]) || 0,
|
|
1439
|
+
points: points,
|
|
1440
|
+
constrainToGraph: false,
|
|
1441
|
+
}),
|
|
1442
|
+
);
|
|
1443
|
+
return {
|
|
1444
|
+
update: polygon.transform.bind(polygon),
|
|
1445
|
+
remove: polygon.remove.bind(polygon),
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
if (type === "line" || type === "lineSegment") {
|
|
1449
|
+
const line = graphie.addMovableLineSegment(
|
|
1450
|
+
_.extend({}, options, lineCoords, {
|
|
1451
|
+
movePointsWithLine: true,
|
|
1452
|
+
fixed: true,
|
|
1453
|
+
constraints: {
|
|
1454
|
+
fixed: true,
|
|
1455
|
+
},
|
|
1456
|
+
extendLine: type === "line",
|
|
1457
|
+
}),
|
|
1458
|
+
);
|
|
1459
|
+
|
|
1460
|
+
// TODO(jack): Hide points on uneditable lines when translation
|
|
1461
|
+
// is a vector.
|
|
1462
|
+
// We can't just remove the points yet, because they are the
|
|
1463
|
+
// translation handle for the line.
|
|
1464
|
+
return {
|
|
1465
|
+
update: line.transform.bind(line, true),
|
|
1466
|
+
remove: line.remove.bind(line),
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
if (type === "angle") {
|
|
1470
|
+
// If this angle is editable, we want to be able to make angles
|
|
1471
|
+
// both larger and smaller than 180 degrees.
|
|
1472
|
+
// If this angle is not editable, it should always maintain
|
|
1473
|
+
// it's angle measure, even if it is reflected (causing the
|
|
1474
|
+
// clockwise-ness of the points to change)
|
|
1475
|
+
const shouldChangeReflexivity = options.editable ? null : false;
|
|
1476
|
+
|
|
1477
|
+
const angle = graphie.addMovableAngle({
|
|
1478
|
+
angleLabel: "$deg0",
|
|
1479
|
+
fixed: true,
|
|
1480
|
+
points: points,
|
|
1481
|
+
normalStyle: options.normalStyle,
|
|
1482
|
+
reflex: options.reflex,
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
// Hide non-vertex points on uneditable angles
|
|
1486
|
+
if (!_.isArray(points[0]) && !options.editable) {
|
|
1487
|
+
points[0].remove();
|
|
1488
|
+
points[2].remove();
|
|
1489
|
+
}
|
|
1490
|
+
return {
|
|
1491
|
+
update: angle.update.bind(angle, shouldChangeReflexivity),
|
|
1492
|
+
remove: angle.remove.bind(angle),
|
|
1493
|
+
getOptions: function () {
|
|
1494
|
+
return {
|
|
1495
|
+
reflex: angle.isReflex(),
|
|
1496
|
+
};
|
|
1497
|
+
},
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
if (type === "circle") {
|
|
1501
|
+
let perimeter = {
|
|
1502
|
+
// temporary object for the first removal
|
|
1503
|
+
remove: _.identity,
|
|
1504
|
+
};
|
|
1505
|
+
const redrawPerim = function () {
|
|
1506
|
+
const coord0 = points[0].coord || points[0];
|
|
1507
|
+
const coord1 = points[1].coord || points[1];
|
|
1508
|
+
const radius = kpoint.distanceToPoint(coord0, coord1);
|
|
1509
|
+
perimeter.remove();
|
|
1510
|
+
perimeter = graphie.circle(
|
|
1511
|
+
coord0,
|
|
1512
|
+
radius,
|
|
1513
|
+
_.extend(
|
|
1514
|
+
{
|
|
1515
|
+
stroke: KhanColors.DYNAMIC,
|
|
1516
|
+
"stroke-width": 2,
|
|
1517
|
+
},
|
|
1518
|
+
options.normalStyle,
|
|
1519
|
+
),
|
|
1520
|
+
);
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
redrawPerim();
|
|
1524
|
+
if (points[1].remove && !options.editable) {
|
|
1525
|
+
points[1].remove();
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
return {
|
|
1529
|
+
update: redrawPerim,
|
|
1530
|
+
remove: function () {
|
|
1531
|
+
// Not _.bind because the remove function changes
|
|
1532
|
+
// when the perimeter is redrawn
|
|
1533
|
+
perimeter.remove();
|
|
1534
|
+
},
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
if (type === "point") {
|
|
1538
|
+
// do nothing
|
|
1539
|
+
return {
|
|
1540
|
+
update: null,
|
|
1541
|
+
remove: null,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
throw new PerseusError(
|
|
1545
|
+
"Invalid shape type " + type,
|
|
1546
|
+
Errors.InvalidInput,
|
|
1547
|
+
);
|
|
1548
|
+
},
|
|
1549
|
+
|
|
1550
|
+
_combine: function (type, coords) {
|
|
1551
|
+
return {
|
|
1552
|
+
type: type,
|
|
1553
|
+
coords: coords,
|
|
1554
|
+
};
|
|
1555
|
+
},
|
|
1556
|
+
|
|
1557
|
+
polygon: {
|
|
1558
|
+
equal: orderInsensitiveCoordsEqual,
|
|
1559
|
+
},
|
|
1560
|
+
|
|
1561
|
+
line: {
|
|
1562
|
+
equal: kline.equal,
|
|
1563
|
+
},
|
|
1564
|
+
|
|
1565
|
+
lineSegment: {
|
|
1566
|
+
equal: orderInsensitiveCoordsEqual,
|
|
1567
|
+
},
|
|
1568
|
+
|
|
1569
|
+
angle: {
|
|
1570
|
+
equal: function (points1, points2) {
|
|
1571
|
+
if (!kpoint.equal(points1[1], points2[1])) {
|
|
1572
|
+
return false;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
const line1_0 = [points1[1], points1[0]];
|
|
1576
|
+
const line1_2 = [points1[1], points1[2]];
|
|
1577
|
+
const line2_0 = [points2[1], points2[0]];
|
|
1578
|
+
const line2_2 = [points2[1], points2[2]];
|
|
1579
|
+
|
|
1580
|
+
const equalUnflipped =
|
|
1581
|
+
kray.equal(line1_0, line2_0) && kray.equal(line1_2, line2_2);
|
|
1582
|
+
const equalFlipped =
|
|
1583
|
+
kray.equal(line1_0, line2_2) && kray.equal(line1_2, line2_0);
|
|
1584
|
+
|
|
1585
|
+
return equalUnflipped || equalFlipped;
|
|
1586
|
+
},
|
|
1587
|
+
|
|
1588
|
+
defaultOptions: {
|
|
1589
|
+
reflex: false,
|
|
1590
|
+
},
|
|
1591
|
+
},
|
|
1592
|
+
|
|
1593
|
+
circle: {
|
|
1594
|
+
equal: function (points1, points2) {
|
|
1595
|
+
const radius1 = kpoint.distanceToPoint(points1[0], points1[1]);
|
|
1596
|
+
const radius2 = kpoint.distanceToPoint(points2[0], points2[1]);
|
|
1597
|
+
return (
|
|
1598
|
+
kpoint.equal(points1[0], points2[0]) &&
|
|
1599
|
+
knumber.equal(radius1, radius2)
|
|
1600
|
+
);
|
|
1601
|
+
},
|
|
1602
|
+
},
|
|
1603
|
+
|
|
1604
|
+
point: {
|
|
1605
|
+
equal: kpoint.equal,
|
|
1606
|
+
},
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
const TransformationListItem = TransformOps.ListItem;
|
|
1610
|
+
|
|
1611
|
+
class TransformationList extends React.Component<$FlowFixMe, $FlowFixMe> {
|
|
1612
|
+
render(): React.Node {
|
|
1613
|
+
if (this.props.mode === "static") {
|
|
1614
|
+
return <span />; // don't render anything
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
const transformationList = _.map(
|
|
1618
|
+
this.props.transformations,
|
|
1619
|
+
function (transform, i) {
|
|
1620
|
+
return (
|
|
1621
|
+
<TransformationListItem
|
|
1622
|
+
ref={"transformation" + i}
|
|
1623
|
+
key={"transformation" + i}
|
|
1624
|
+
transform={transform}
|
|
1625
|
+
mode={this.props.mode}
|
|
1626
|
+
onChange={this.handleChange}
|
|
1627
|
+
onFocus={_.partial(this.props.onFocus, "" + i)}
|
|
1628
|
+
onBlur={_.partial(this.props.onBlur, "" + i)}
|
|
1629
|
+
keypadElement={this.props.keypadElement}
|
|
1630
|
+
apiOptions={this.props.apiOptions}
|
|
1631
|
+
/>
|
|
1632
|
+
);
|
|
1633
|
+
},
|
|
1634
|
+
this,
|
|
1635
|
+
);
|
|
1636
|
+
|
|
1637
|
+
return (
|
|
1638
|
+
<div className="perseus-transformation-list">
|
|
1639
|
+
{transformationList}
|
|
1640
|
+
</div>
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
_transformationRefs = () => {
|
|
1645
|
+
return _.times(this.props.transformations.length, (i) => {
|
|
1646
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1647
|
+
return this.refs["transformation" + i];
|
|
1648
|
+
});
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
value = () => {
|
|
1652
|
+
return _.invoke(this._transformationRefs(), "value");
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1655
|
+
handleChange = (changed, callback) => {
|
|
1656
|
+
this.props.onChange(this.value(), callback);
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
focusLast = () => {
|
|
1660
|
+
const transformationRefs = this._transformationRefs();
|
|
1661
|
+
if (transformationRefs.length !== 0) {
|
|
1662
|
+
_.last(transformationRefs).focus();
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
class ToolButton extends React.Component<$FlowFixMe, $FlowFixMe> {
|
|
1668
|
+
render(): React.Node {
|
|
1669
|
+
const classes = this.props.toggled
|
|
1670
|
+
? "simple-button exercise-orange toggled highlighted-tool-button"
|
|
1671
|
+
: "simple-button";
|
|
1672
|
+
|
|
1673
|
+
return (
|
|
1674
|
+
<button
|
|
1675
|
+
type="button"
|
|
1676
|
+
className={classes}
|
|
1677
|
+
disabled={this.props.disabled}
|
|
1678
|
+
onClick={this.props.onClick}
|
|
1679
|
+
onTouchStart={captureScratchpadTouchStart}
|
|
1680
|
+
>
|
|
1681
|
+
{this.props.children}
|
|
1682
|
+
</button>
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
class ToolsBar extends React.Component<$FlowFixMe, $FlowFixMe> {
|
|
1688
|
+
state = {
|
|
1689
|
+
selected: null,
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
render(): React.Node {
|
|
1693
|
+
const tools = _.map(
|
|
1694
|
+
Transformations,
|
|
1695
|
+
function (tool, type) {
|
|
1696
|
+
if (this.props.enabled[type]) {
|
|
1697
|
+
return (
|
|
1698
|
+
<ToolButton
|
|
1699
|
+
key={type}
|
|
1700
|
+
disabled={this.props.apiOptions.readOnly}
|
|
1701
|
+
toggled={this.state.selected === type}
|
|
1702
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
1703
|
+
onClick={this.changeSelected.bind(this, type)}
|
|
1704
|
+
>
|
|
1705
|
+
{tool.verbName}
|
|
1706
|
+
</ToolButton>
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
},
|
|
1710
|
+
this,
|
|
1711
|
+
);
|
|
1712
|
+
|
|
1713
|
+
return (
|
|
1714
|
+
<div className="transformer-tools-bar">
|
|
1715
|
+
<span className="simple-button-group">{tools}</span>
|
|
1716
|
+
<button
|
|
1717
|
+
className="transformer-undo-button simple-button"
|
|
1718
|
+
type="button"
|
|
1719
|
+
disabled={this.props.apiOptions.readOnly}
|
|
1720
|
+
onClick={this.props.onUndoClick}
|
|
1721
|
+
onTouchStart={captureScratchpadTouchStart}
|
|
1722
|
+
>
|
|
1723
|
+
<InlineIcon {...iconUndo} />
|
|
1724
|
+
{" " + i18n._("Undo")}
|
|
1725
|
+
</button>
|
|
1726
|
+
<div className="clear" />
|
|
1727
|
+
</div>
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
changeSelected = (tool) => {
|
|
1732
|
+
this.props.removeTool(this.state.selected);
|
|
1733
|
+
|
|
1734
|
+
if (!tool || tool === this.state.selected) {
|
|
1735
|
+
this.setState({
|
|
1736
|
+
selected: null,
|
|
1737
|
+
});
|
|
1738
|
+
} else {
|
|
1739
|
+
this.props.addTool(tool);
|
|
1740
|
+
this.setState({
|
|
1741
|
+
selected: tool,
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
class AddTransformBar extends React.Component<$FlowFixMe, $FlowFixMe> {
|
|
1748
|
+
render(): React.Node {
|
|
1749
|
+
const tools = _.map(
|
|
1750
|
+
Transformations,
|
|
1751
|
+
function (tool, type) {
|
|
1752
|
+
if (this.props.enabled[type]) {
|
|
1753
|
+
return (
|
|
1754
|
+
<ToolButton
|
|
1755
|
+
key={type}
|
|
1756
|
+
toggled={false}
|
|
1757
|
+
disabled={this.props.apiOptions.readOnly}
|
|
1758
|
+
// eslint-disable-next-line react/jsx-no-bind
|
|
1759
|
+
onClick={this.changeSelected.bind(this, type)}
|
|
1760
|
+
>
|
|
1761
|
+
<InlineIcon {...iconPlus} /> {tool.nounName}
|
|
1762
|
+
</ToolButton>
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
},
|
|
1766
|
+
this,
|
|
1767
|
+
);
|
|
1768
|
+
|
|
1769
|
+
return (
|
|
1770
|
+
<div className="transformer-tools-bar">
|
|
1771
|
+
{tools}
|
|
1772
|
+
<button
|
|
1773
|
+
className="transformer-undo-button simple-button"
|
|
1774
|
+
type="button"
|
|
1775
|
+
onClick={this.props.onUndoClick}
|
|
1776
|
+
disabled={this.props.apiOptions.readOnly}
|
|
1777
|
+
onTouchStart={captureScratchpadTouchStart}
|
|
1778
|
+
>
|
|
1779
|
+
<InlineIcon {...iconUndo} />
|
|
1780
|
+
{" " + i18n._("Undo")}
|
|
1781
|
+
</button>
|
|
1782
|
+
<div className="clear" />
|
|
1783
|
+
</div>
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
changeSelected = (tool) => {
|
|
1788
|
+
if (tool) {
|
|
1789
|
+
this.props.addTool(tool);
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
type Graphie = $FlowFixMe;
|
|
1795
|
+
type Tool = $FlowFixMe;
|
|
1796
|
+
type ToolCleanup = $FlowFixMe;
|
|
1797
|
+
|
|
1798
|
+
type Rubric = PerseusTransformerWidgetOptions;
|
|
1799
|
+
type RenderProps = PerseusTransformerWidgetOptions; // there's no transform in the exports
|
|
1800
|
+
type UserInput = $FlowFixMe;
|
|
1801
|
+
type Props = {|
|
|
1802
|
+
...WidgetProps<RenderProps, Rubric>,
|
|
1803
|
+
transformations: $ReadOnlyArray<TransformerTransformation>,
|
|
1804
|
+
keypadElement: $FlowFixMe,
|
|
1805
|
+
|};
|
|
1806
|
+
|
|
1807
|
+
class Transformer extends React.Component<Props> {
|
|
1808
|
+
tools: {|[string]: Tool|};
|
|
1809
|
+
currentTool: ?Tool;
|
|
1810
|
+
transformations: $ReadOnlyArray<TransformerTransformation>;
|
|
1811
|
+
shape: ?$FlowFixMe; // This is not the same as TransformerShape
|
|
1812
|
+
dilationCircle: ?$FlowFixMe;
|
|
1813
|
+
rotatePoint: ?$FlowFixMe;
|
|
1814
|
+
rotateHandle: ?$FlowFixMe;
|
|
1815
|
+
|
|
1816
|
+
static defaultProps: $FlowFixMe = _.defaults(
|
|
1817
|
+
{
|
|
1818
|
+
transformations: [],
|
|
1819
|
+
},
|
|
1820
|
+
defaultTransformerProps,
|
|
1821
|
+
);
|
|
1822
|
+
|
|
1823
|
+
render(): React.Node {
|
|
1824
|
+
// Fill in any missing value in this.props.graph
|
|
1825
|
+
// this can happen because the graph json doesn't include
|
|
1826
|
+
// box, for example
|
|
1827
|
+
const graph = _.extend(
|
|
1828
|
+
defaultGraphProps(this.props.graph, defaultBoxSize),
|
|
1829
|
+
this.props.graph,
|
|
1830
|
+
);
|
|
1831
|
+
|
|
1832
|
+
const interactiveToolsMode = this.props.graphMode === "interactive";
|
|
1833
|
+
|
|
1834
|
+
const ToolsBarClass = interactiveToolsMode ? ToolsBar : AddTransformBar;
|
|
1835
|
+
|
|
1836
|
+
// This style is applied inline because it is dependent on the
|
|
1837
|
+
// size of the graph as set by the graph.box prop, and this also
|
|
1838
|
+
// lets us specify it in the same place the graph's width is
|
|
1839
|
+
// specified.
|
|
1840
|
+
const toolsBar = (
|
|
1841
|
+
<div style={{width: graph.box[0]}}>
|
|
1842
|
+
<ToolsBarClass
|
|
1843
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1844
|
+
ref="toolsBar"
|
|
1845
|
+
enabled={pluckObject(this.props.tools, "enabled")}
|
|
1846
|
+
apiOptions={this.props.apiOptions}
|
|
1847
|
+
addTool={this.addTool}
|
|
1848
|
+
removeTool={this.removeTool}
|
|
1849
|
+
onUndoClick={this.handleUndoClick}
|
|
1850
|
+
/>
|
|
1851
|
+
</div>
|
|
1852
|
+
);
|
|
1853
|
+
|
|
1854
|
+
return (
|
|
1855
|
+
<div className={"perseus-widget " + "perseus-widget-transformer"}>
|
|
1856
|
+
<Graph
|
|
1857
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1858
|
+
ref="graph"
|
|
1859
|
+
box={graph.box}
|
|
1860
|
+
range={graph.range}
|
|
1861
|
+
labels={graph.labels}
|
|
1862
|
+
step={graph.step}
|
|
1863
|
+
gridStep={graph.gridStep}
|
|
1864
|
+
markings={graph.markings}
|
|
1865
|
+
backgroundImage={graph.backgroundImage}
|
|
1866
|
+
showProtractor={graph.showProtractor}
|
|
1867
|
+
onGraphieUpdated={this.setupGraphie}
|
|
1868
|
+
setDrawingAreaAvailable={
|
|
1869
|
+
this.props.apiOptions.setDrawingAreaAvailable
|
|
1870
|
+
}
|
|
1871
|
+
/>
|
|
1872
|
+
|
|
1873
|
+
{!interactiveToolsMode && i18n._("Add transformations below:")}
|
|
1874
|
+
|
|
1875
|
+
{this.props.graphMode === "static" && [
|
|
1876
|
+
<br key="static-br" />,
|
|
1877
|
+
<em key="static-nomove">
|
|
1878
|
+
{" "}
|
|
1879
|
+
{i18n._(
|
|
1880
|
+
"Note: For this question, the shape will not move.",
|
|
1881
|
+
)}{" "}
|
|
1882
|
+
</em>,
|
|
1883
|
+
]}
|
|
1884
|
+
|
|
1885
|
+
{interactiveToolsMode && toolsBar}
|
|
1886
|
+
|
|
1887
|
+
<TransformationList
|
|
1888
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1889
|
+
ref="transformationList"
|
|
1890
|
+
mode={this.props.listMode}
|
|
1891
|
+
transformations={this.props.transformations}
|
|
1892
|
+
onChange={this.setTransformationProps}
|
|
1893
|
+
onFocus={this._handleFocus}
|
|
1894
|
+
onBlur={this._handleBlur}
|
|
1895
|
+
keypadElement={this.props.keypadElement}
|
|
1896
|
+
apiOptions={this.props.apiOptions}
|
|
1897
|
+
/>
|
|
1898
|
+
|
|
1899
|
+
{!interactiveToolsMode && toolsBar}
|
|
1900
|
+
</div>
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
componentDidMount() {
|
|
1905
|
+
this.setupGraphie(this.graphie());
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
componentDidUpdate(prevProps: Props) {
|
|
1909
|
+
if (this.shouldSetupGraphie(this.props, prevProps)) {
|
|
1910
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1911
|
+
this.refs.graph.reset();
|
|
1912
|
+
} else if (!deepEq(this.props.transformations, this.transformations)) {
|
|
1913
|
+
this.setTransformations(this.props.transformations);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
shouldSetupGraphie: (Props, Props) => boolean = (nextProps, prevProps) => {
|
|
1918
|
+
if (!deepEq(prevProps.starting, nextProps.starting)) {
|
|
1919
|
+
return true;
|
|
1920
|
+
}
|
|
1921
|
+
if (prevProps.graphMode !== nextProps.graphMode) {
|
|
1922
|
+
return true;
|
|
1923
|
+
}
|
|
1924
|
+
if (prevProps.listMode !== nextProps.listMode) {
|
|
1925
|
+
return true;
|
|
1926
|
+
}
|
|
1927
|
+
if (prevProps.drawSolutionShape !== nextProps.drawSolutionShape) {
|
|
1928
|
+
return true;
|
|
1929
|
+
}
|
|
1930
|
+
if (
|
|
1931
|
+
nextProps.drawSolutionShape &&
|
|
1932
|
+
!deepEq(prevProps.correct.shape, nextProps.correct.shape)
|
|
1933
|
+
) {
|
|
1934
|
+
return true;
|
|
1935
|
+
}
|
|
1936
|
+
if (!deepEq(this.tools, nextProps.tools)) {
|
|
1937
|
+
return true;
|
|
1938
|
+
}
|
|
1939
|
+
return false;
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
graphie: () => Graphie = () => {
|
|
1943
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1944
|
+
return this.refs.graph.graphie();
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
setupGraphie: (Graphie) => void = (graphie) => {
|
|
1948
|
+
// A background image of our solution:
|
|
1949
|
+
if (
|
|
1950
|
+
this.props.drawSolutionShape &&
|
|
1951
|
+
this.props.correct.shape &&
|
|
1952
|
+
this.props.correct.shape.coords
|
|
1953
|
+
) {
|
|
1954
|
+
ShapeTypes.addShape(graphie, {
|
|
1955
|
+
fixed: true,
|
|
1956
|
+
shape: this.props.correct.shape,
|
|
1957
|
+
normalStyle: {
|
|
1958
|
+
stroke: KhanColors.GRAY,
|
|
1959
|
+
"stroke-dasharray": "",
|
|
1960
|
+
"stroke-width": 2,
|
|
1961
|
+
},
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
this.currentTool = null;
|
|
1966
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1967
|
+
this.refs.toolsBar.changeSelected(null);
|
|
1968
|
+
this.addTransformerShape(
|
|
1969
|
+
this.props.starting.shape,
|
|
1970
|
+
/* translatable */ false,
|
|
1971
|
+
);
|
|
1972
|
+
this.setTransformations(this.props.transformations);
|
|
1973
|
+
|
|
1974
|
+
// Save a copy of our tools so that we can check future
|
|
1975
|
+
// this.props.tools changes against them
|
|
1976
|
+
// This seems weird, but gives us an easy way to tell whether
|
|
1977
|
+
// props changes were self-inflicted (for which a graphie reset
|
|
1978
|
+
// is not required, and is in fact a bad idea right now because
|
|
1979
|
+
// of resetting the size of the dilation tool).
|
|
1980
|
+
// TODO (jack): A deepClone method would be nice here
|
|
1981
|
+
this.tools = {
|
|
1982
|
+
translation: _.clone(this.props.tools.translation),
|
|
1983
|
+
rotation: _.clone(this.props.tools.rotation),
|
|
1984
|
+
reflection: _.clone(this.props.tools.reflection),
|
|
1985
|
+
dilation: _.clone(this.props.tools.dilation),
|
|
1986
|
+
};
|
|
1987
|
+
};
|
|
1988
|
+
|
|
1989
|
+
/* Applies all transformations in `transformations`
|
|
1990
|
+
* to the starting shape, and updates this.transformations
|
|
1991
|
+
* to reflect this
|
|
1992
|
+
*
|
|
1993
|
+
* Usually called with this.props.transformations
|
|
1994
|
+
*/
|
|
1995
|
+
setTransformations: ($ReadOnlyArray<TransformerTransformation>) => void = (
|
|
1996
|
+
transformations,
|
|
1997
|
+
) => {
|
|
1998
|
+
this.resetCoords();
|
|
1999
|
+
this.transformations = _.clone(transformations);
|
|
2000
|
+
_.each(this.transformations, this.applyTransform);
|
|
2001
|
+
};
|
|
2002
|
+
|
|
2003
|
+
// the polygon that we transform
|
|
2004
|
+
addTransformerShape: ($FlowFixMe, boolean) => void = (
|
|
2005
|
+
shape,
|
|
2006
|
+
translatable,
|
|
2007
|
+
) => {
|
|
2008
|
+
const self = this;
|
|
2009
|
+
const graphie = this.graphie();
|
|
2010
|
+
|
|
2011
|
+
this.shape = ShapeTypes.addMovableShape(graphie, {
|
|
2012
|
+
shape: shape,
|
|
2013
|
+
editable: false,
|
|
2014
|
+
showPoints: this.props.graphMode !== "static",
|
|
2015
|
+
translatable: translatable,
|
|
2016
|
+
onMove: function (dX, dY) {
|
|
2017
|
+
dX = KhanMath.roundToNearest(graphie.snap[0], dX);
|
|
2018
|
+
dY = KhanMath.roundToNearest(graphie.snap[1], dY);
|
|
2019
|
+
// NOTE(kevinb): object is missing .constraints property
|
|
2020
|
+
// $FlowFixMe[prop-missing]
|
|
2021
|
+
self.addTransform({
|
|
2022
|
+
type: "translation",
|
|
2023
|
+
vector: [dX, dY],
|
|
2024
|
+
});
|
|
2025
|
+
return [dX, dY];
|
|
2026
|
+
},
|
|
2027
|
+
normalPointStyle: {
|
|
2028
|
+
fill: translatable
|
|
2029
|
+
? KhanColors.INTERACTIVE
|
|
2030
|
+
: KhanColors.DYNAMIC,
|
|
2031
|
+
stroke: translatable
|
|
2032
|
+
? KhanColors.INTERACTIVE
|
|
2033
|
+
: KhanColors.DYNAMIC,
|
|
2034
|
+
},
|
|
2035
|
+
highlightPointStyle: {
|
|
2036
|
+
fill: KhanColors.INTERACTING,
|
|
2037
|
+
stroke: KhanColors.INTERACTING,
|
|
2038
|
+
},
|
|
2039
|
+
});
|
|
2040
|
+
};
|
|
2041
|
+
|
|
2042
|
+
addTool: (TransformerTransformation["type"]) => void = (toolId) => {
|
|
2043
|
+
const self = this;
|
|
2044
|
+
|
|
2045
|
+
if (this.props.graphMode === "interactive") {
|
|
2046
|
+
if (toolId === "translation") {
|
|
2047
|
+
this.currentTool = this.addTranslationTool();
|
|
2048
|
+
} else if (toolId === "rotation") {
|
|
2049
|
+
this.currentTool = this.addRotationTool();
|
|
2050
|
+
} else if (toolId === "reflection") {
|
|
2051
|
+
this.currentTool = this.addReflectionTool();
|
|
2052
|
+
} else if (toolId === "dilation") {
|
|
2053
|
+
this.currentTool = this.addDilationTool();
|
|
2054
|
+
} else {
|
|
2055
|
+
throw new PerseusError(
|
|
2056
|
+
"Invalid tool id: " + toolId,
|
|
2057
|
+
Errors.InvalidInput,
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
} else {
|
|
2061
|
+
let transform: TransformerTransformation;
|
|
2062
|
+
if (toolId === "translation") {
|
|
2063
|
+
// NOTE(kevinb): object is missing .constraints property
|
|
2064
|
+
// $FlowFixMe[prop-missing]
|
|
2065
|
+
transform = {
|
|
2066
|
+
type: "translation",
|
|
2067
|
+
// NOTE(kevib): these should be numbers
|
|
2068
|
+
// $FlowFixMe[incompatible-type]
|
|
2069
|
+
vector: [null, null],
|
|
2070
|
+
};
|
|
2071
|
+
} else if (toolId === "rotation") {
|
|
2072
|
+
// NOTE(kevinb): object is missing .constraints property
|
|
2073
|
+
// $FlowFixMe[prop-missing]
|
|
2074
|
+
transform = {
|
|
2075
|
+
type: "rotation",
|
|
2076
|
+
// NOTE(kevib): these should be numbers
|
|
2077
|
+
// $FlowFixMe[incompatible-type]
|
|
2078
|
+
center: [null, null],
|
|
2079
|
+
// NOTE(kevib): this should be a number
|
|
2080
|
+
// $FlowFixMe[incompatible-type]
|
|
2081
|
+
angleDeg: null,
|
|
2082
|
+
};
|
|
2083
|
+
} else if (toolId === "reflection") {
|
|
2084
|
+
// Reflections with nulls in them won't be applied until
|
|
2085
|
+
// fills in the blanks
|
|
2086
|
+
transform = {
|
|
2087
|
+
type: "reflection",
|
|
2088
|
+
line: [
|
|
2089
|
+
// NOTE(kevib): these should be numbers
|
|
2090
|
+
// $FlowFixMe[incompatible-type]
|
|
2091
|
+
[null, null],
|
|
2092
|
+
// NOTE(kevib): these should be numbers
|
|
2093
|
+
// $FlowFixMe[incompatible-type]
|
|
2094
|
+
[null, null],
|
|
2095
|
+
],
|
|
2096
|
+
};
|
|
2097
|
+
} else if (toolId === "dilation") {
|
|
2098
|
+
// NOTE(kevinb): object is missing .constraints property
|
|
2099
|
+
// $FlowFixMe[prop-missing]
|
|
2100
|
+
transform = {
|
|
2101
|
+
type: "dilation",
|
|
2102
|
+
// NOTE(kevib): these should be numbers
|
|
2103
|
+
// $FlowFixMe[incompatible-type]
|
|
2104
|
+
center: [null, null],
|
|
2105
|
+
// NOTE(kevib): this should be a number
|
|
2106
|
+
// $FlowFixMe[incompatible-type]
|
|
2107
|
+
scale: null,
|
|
2108
|
+
};
|
|
2109
|
+
} else {
|
|
2110
|
+
throw new PerseusError(
|
|
2111
|
+
"Invalid tool id: " + toolId,
|
|
2112
|
+
Errors.InvalidInput,
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// $FlowFixMe[prop-missing]
|
|
2117
|
+
this.doTransform(transform, function () {
|
|
2118
|
+
self.refs.transformationList.focusLast();
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
|
|
2123
|
+
removeTool: (string) => void = (toolId) => {
|
|
2124
|
+
if (this.currentTool) {
|
|
2125
|
+
this.currentTool.remove();
|
|
2126
|
+
}
|
|
2127
|
+
this.currentTool = null;
|
|
2128
|
+
};
|
|
2129
|
+
|
|
2130
|
+
addTranslationTool: () => $FlowFixMe = () => {
|
|
2131
|
+
const self = this;
|
|
2132
|
+
this.shape?.remove();
|
|
2133
|
+
this.addTransformerShape(this.shape?.toJSON(), /* translatable */ true);
|
|
2134
|
+
|
|
2135
|
+
return {
|
|
2136
|
+
remove: function () {
|
|
2137
|
+
self.shape?.remove();
|
|
2138
|
+
self.addTransformerShape(
|
|
2139
|
+
self.shape?.toJSON(),
|
|
2140
|
+
/* translatable */ false,
|
|
2141
|
+
);
|
|
2142
|
+
},
|
|
2143
|
+
};
|
|
2144
|
+
};
|
|
2145
|
+
|
|
2146
|
+
// Snaps a coord to this.graphie()'s snap
|
|
2147
|
+
snapCoord: (Coord) => Coord = (coord) => {
|
|
2148
|
+
const graphie = this.graphie();
|
|
2149
|
+
return _.map(coord, function (val, dim) {
|
|
2150
|
+
return KhanMath.roundToNearest(graphie.snap[dim], val);
|
|
2151
|
+
});
|
|
2152
|
+
};
|
|
2153
|
+
|
|
2154
|
+
// Normalize the coords into something that fits the new 45 degree
|
|
2155
|
+
// reflection line.
|
|
2156
|
+
normalizeReflectionCoords: ([Coord, Coord]) => [Coord, Coord] = (
|
|
2157
|
+
messyCoords,
|
|
2158
|
+
) => {
|
|
2159
|
+
// $FlowFixMe[incompatible-call]
|
|
2160
|
+
const midpoint = this.snapCoord(kline.midpoint(messyCoords));
|
|
2161
|
+
const origDirectionPolar = kvector.polarDegFromCart(
|
|
2162
|
+
kvector.subtract(messyCoords[0], messyCoords[1]),
|
|
2163
|
+
);
|
|
2164
|
+
|
|
2165
|
+
const direction = kvector.cartFromPolarDeg(
|
|
2166
|
+
1,
|
|
2167
|
+
KhanMath.roundToNearest(45, origDirectionPolar[1]),
|
|
2168
|
+
);
|
|
2169
|
+
const coords = _.map(
|
|
2170
|
+
[-1, 1],
|
|
2171
|
+
function (directionCoefficient) {
|
|
2172
|
+
const coord = kvector.add(
|
|
2173
|
+
midpoint,
|
|
2174
|
+
kvector.scale(
|
|
2175
|
+
direction,
|
|
2176
|
+
directionCoefficient *
|
|
2177
|
+
this.scaleToCurrentRange(
|
|
2178
|
+
REFLECT_ROTATE_HANDLE_DIST,
|
|
2179
|
+
),
|
|
2180
|
+
),
|
|
2181
|
+
);
|
|
2182
|
+
return this.snapCoord(coord);
|
|
2183
|
+
},
|
|
2184
|
+
this,
|
|
2185
|
+
);
|
|
2186
|
+
return coords;
|
|
2187
|
+
};
|
|
2188
|
+
|
|
2189
|
+
addReflectionTool: () => ToolCleanup = () => {
|
|
2190
|
+
const options = this.props.tools.reflection;
|
|
2191
|
+
if (!options.enabled) {
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
const self = this;
|
|
2195
|
+
// eslint-disable-next-line react/no-string-refs
|
|
2196
|
+
const graphie = this.refs.graph.graphie();
|
|
2197
|
+
|
|
2198
|
+
const updateReflectionTool = function () {
|
|
2199
|
+
self.changeTool("reflection", {
|
|
2200
|
+
coords: _.pluck(reflectPoints, "coord"),
|
|
2201
|
+
});
|
|
2202
|
+
};
|
|
2203
|
+
|
|
2204
|
+
const coords = this.normalizeReflectionCoords(options.coords);
|
|
2205
|
+
|
|
2206
|
+
// The points defining the line of reflection; hidden from the
|
|
2207
|
+
// user.
|
|
2208
|
+
const reflectPoints = _.map(
|
|
2209
|
+
coords,
|
|
2210
|
+
function (coord) {
|
|
2211
|
+
return graphie.addMovablePoint({
|
|
2212
|
+
coord: coord,
|
|
2213
|
+
visible: false,
|
|
2214
|
+
});
|
|
2215
|
+
},
|
|
2216
|
+
this,
|
|
2217
|
+
);
|
|
2218
|
+
|
|
2219
|
+
// the line of reflection
|
|
2220
|
+
// TODO(jack): graphie.style here is a hack to prevent the dashed
|
|
2221
|
+
// style from leaking into the rest of the shapes. Remove when
|
|
2222
|
+
// graphie.addMovableLineSegment doesn't leak styles anymore.
|
|
2223
|
+
let reflectLine;
|
|
2224
|
+
const normalColor = colorForTool(options);
|
|
2225
|
+
graphie.style({}, function () {
|
|
2226
|
+
reflectLine = graphie.addMovableLineSegment({
|
|
2227
|
+
fixed: options.constraints.fixed,
|
|
2228
|
+
constraints: options.constraints,
|
|
2229
|
+
pointA: reflectPoints[0],
|
|
2230
|
+
pointZ: reflectPoints[1],
|
|
2231
|
+
snapX: graphie.snap[0],
|
|
2232
|
+
snapY: graphie.snap[1],
|
|
2233
|
+
extendLine: true,
|
|
2234
|
+
normalStyle: {
|
|
2235
|
+
stroke: normalColor,
|
|
2236
|
+
"stroke-width": 2,
|
|
2237
|
+
"stroke-dasharray": "- ",
|
|
2238
|
+
},
|
|
2239
|
+
highlightStyle: {
|
|
2240
|
+
stroke: KhanColors.INTERACTING,
|
|
2241
|
+
"stroke-width": 2,
|
|
2242
|
+
"stroke-dasharray": "- ", // TODO(jack) solid doesn't
|
|
2243
|
+
// work here, but would be
|
|
2244
|
+
// nicer
|
|
2245
|
+
},
|
|
2246
|
+
movePointsWithLine: true,
|
|
2247
|
+
onMoveEnd: updateReflectionTool,
|
|
2248
|
+
});
|
|
2249
|
+
});
|
|
2250
|
+
|
|
2251
|
+
// the "button" point in the center of the line of reflection
|
|
2252
|
+
const reflectButton = graphie.addReflectButton({
|
|
2253
|
+
fixed: options.constraints.fixed,
|
|
2254
|
+
line: reflectLine,
|
|
2255
|
+
size: this.scaleToCurrentRange(REFLECT_BUTTON_SIZE),
|
|
2256
|
+
onClick: function () {
|
|
2257
|
+
self.doTransform({
|
|
2258
|
+
type: "reflection",
|
|
2259
|
+
line: _.pluck(reflectPoints, "coord"),
|
|
2260
|
+
});
|
|
2261
|
+
if (reflectRotateHandle) {
|
|
2262
|
+
// flip the rotation handle
|
|
2263
|
+
reflectRotateHandle.setCoord(
|
|
2264
|
+
kvector.add(
|
|
2265
|
+
reflectButton.coord,
|
|
2266
|
+
kvector.subtract(
|
|
2267
|
+
reflectButton.coord,
|
|
2268
|
+
reflectRotateHandle.coord,
|
|
2269
|
+
),
|
|
2270
|
+
),
|
|
2271
|
+
);
|
|
2272
|
+
reflectRotateHandle.update();
|
|
2273
|
+
}
|
|
2274
|
+
},
|
|
2275
|
+
normalStyle: {
|
|
2276
|
+
stroke: normalColor,
|
|
2277
|
+
"stroke-width": 2,
|
|
2278
|
+
fill: normalColor,
|
|
2279
|
+
},
|
|
2280
|
+
highlightStyle: {
|
|
2281
|
+
stroke: KhanColors.INTERACTING,
|
|
2282
|
+
"stroke-width": 3,
|
|
2283
|
+
fill: KhanColors.INTERACTING,
|
|
2284
|
+
},
|
|
2285
|
+
onMoveEnd: updateReflectionTool,
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
let reflectRotateHandle = null;
|
|
2289
|
+
if (!options.constraints.fixed) {
|
|
2290
|
+
// The rotation handle for rotating the line of reflection
|
|
2291
|
+
const initRotateHandleAngle =
|
|
2292
|
+
kvector.polarDegFromCart(
|
|
2293
|
+
kvector.subtract(
|
|
2294
|
+
reflectPoints[1].coord,
|
|
2295
|
+
reflectPoints[0].coord,
|
|
2296
|
+
),
|
|
2297
|
+
)[1] + 90; // 90 degrees off of the line
|
|
2298
|
+
reflectRotateHandle = graphie.addRotateHandle({
|
|
2299
|
+
center: reflectButton,
|
|
2300
|
+
radius: this.scaleToCurrentRange(REFLECT_ROTATE_HANDLE_DIST),
|
|
2301
|
+
angleDeg: initRotateHandleAngle,
|
|
2302
|
+
width: this.scaleToCurrentRange(0.24),
|
|
2303
|
+
hoverWidth: this.scaleToCurrentRange(0.4),
|
|
2304
|
+
lengthAngle: 17,
|
|
2305
|
+
onMove: function (newAngle) {
|
|
2306
|
+
return KhanMath.roundToNearest(45, newAngle);
|
|
2307
|
+
},
|
|
2308
|
+
onMoveEnd: updateReflectionTool,
|
|
2309
|
+
});
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
// Move the reflectButton and reflectRotateHandle with the line
|
|
2313
|
+
$(reflectLine).on("move", function () {
|
|
2314
|
+
reflectButton.update();
|
|
2315
|
+
$(reflectButton).trigger("move"); // update the rotation handle,
|
|
2316
|
+
// which watches for this in util/interactive.js.
|
|
2317
|
+
});
|
|
2318
|
+
|
|
2319
|
+
// Update the line and reflect button when the reflectRotateHandle is
|
|
2320
|
+
// rotated
|
|
2321
|
+
if (reflectRotateHandle) {
|
|
2322
|
+
$(reflectRotateHandle).on("move", function () {
|
|
2323
|
+
const rotateHandleApprox = self.snapCoord(
|
|
2324
|
+
// $FlowFixMe[incompatible-use]
|
|
2325
|
+
reflectRotateHandle.coord,
|
|
2326
|
+
);
|
|
2327
|
+
|
|
2328
|
+
const rotateVector = kvector.subtract(
|
|
2329
|
+
rotateHandleApprox,
|
|
2330
|
+
reflectButton.coord,
|
|
2331
|
+
);
|
|
2332
|
+
|
|
2333
|
+
const flipped = reflectButton.isFlipped() ? 1 : 0;
|
|
2334
|
+
reflectPoints[flipped].setCoord(
|
|
2335
|
+
kvector.add(
|
|
2336
|
+
reflectButton.coord,
|
|
2337
|
+
kvector.rotateDeg(rotateVector, 90),
|
|
2338
|
+
),
|
|
2339
|
+
);
|
|
2340
|
+
reflectPoints[1 - flipped].setCoord(
|
|
2341
|
+
kvector.add(
|
|
2342
|
+
reflectButton.coord,
|
|
2343
|
+
kvector.rotateDeg(rotateVector, -90),
|
|
2344
|
+
),
|
|
2345
|
+
);
|
|
2346
|
+
|
|
2347
|
+
reflectLine.transform(true);
|
|
2348
|
+
reflectButton.update();
|
|
2349
|
+
});
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
return {
|
|
2353
|
+
remove: function () {
|
|
2354
|
+
reflectButton.remove();
|
|
2355
|
+
if (reflectRotateHandle) {
|
|
2356
|
+
reflectRotateHandle.remove();
|
|
2357
|
+
}
|
|
2358
|
+
reflectLine.remove();
|
|
2359
|
+
reflectPoints[0].remove();
|
|
2360
|
+
reflectPoints[1].remove();
|
|
2361
|
+
},
|
|
2362
|
+
};
|
|
2363
|
+
};
|
|
2364
|
+
|
|
2365
|
+
/* Scales a distance from the default range of
|
|
2366
|
+
* [-10, 10] to the current this.props.graph.range
|
|
2367
|
+
*
|
|
2368
|
+
* Used for sizing various transformation tools
|
|
2369
|
+
* (rotation handle, dilation circle)
|
|
2370
|
+
*/
|
|
2371
|
+
scaleToCurrentRange: (number) => number = (dist) => {
|
|
2372
|
+
// eslint-disable-next-line react/no-string-refs
|
|
2373
|
+
return scaleToRange(dist, this.refs.graph.props.range);
|
|
2374
|
+
};
|
|
2375
|
+
|
|
2376
|
+
addRotationTool: () => ToolCleanup = () => {
|
|
2377
|
+
const options = this.props.tools.rotation;
|
|
2378
|
+
if (!options.enabled) {
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
const self = this;
|
|
2382
|
+
// eslint-disable-next-line react/no-string-refs
|
|
2383
|
+
const graphie = this.refs.graph.graphie();
|
|
2384
|
+
|
|
2385
|
+
const pointColor = colorForTool(options);
|
|
2386
|
+
// The center of our rotation, which can be moved to change the
|
|
2387
|
+
// center of rotation
|
|
2388
|
+
this.rotatePoint = graphie.addMovablePoint({
|
|
2389
|
+
constraints: options.constraints,
|
|
2390
|
+
coord: options.coord,
|
|
2391
|
+
snapX: graphie.snap[0],
|
|
2392
|
+
snapY: graphie.snap[1],
|
|
2393
|
+
normalStyle: {
|
|
2394
|
+
// ugh, this seems to be a global and
|
|
2395
|
+
"stroke-dasharray": "", // is set to dash above
|
|
2396
|
+
stroke: pointColor,
|
|
2397
|
+
fill: pointColor,
|
|
2398
|
+
},
|
|
2399
|
+
highlightStyle: {
|
|
2400
|
+
"stroke-dasharray": "",
|
|
2401
|
+
stroke: KhanColors.INTERACTING,
|
|
2402
|
+
fill: KhanColors.INTERACTING,
|
|
2403
|
+
},
|
|
2404
|
+
});
|
|
2405
|
+
|
|
2406
|
+
// The point that we move around the center of rotation to actually
|
|
2407
|
+
// cause rotations
|
|
2408
|
+
this.rotateHandle = graphie.addRotateHandle({
|
|
2409
|
+
center: this.rotatePoint,
|
|
2410
|
+
radius: this.scaleToCurrentRange(ROTATE_HANDLE_DIST),
|
|
2411
|
+
width: this.scaleToCurrentRange(0.24),
|
|
2412
|
+
hoverWidth: this.scaleToCurrentRange(0.4),
|
|
2413
|
+
onMove: function (newAngle, oldAngle) {
|
|
2414
|
+
const transform = self.getRotationTransformFromAngle(
|
|
2415
|
+
// $FlowFixMe[incompatible-use]
|
|
2416
|
+
self.rotatePoint.coord,
|
|
2417
|
+
newAngle - oldAngle,
|
|
2418
|
+
);
|
|
2419
|
+
|
|
2420
|
+
// Rotate polygon with rotateHandle
|
|
2421
|
+
self.doTransform(transform);
|
|
2422
|
+
|
|
2423
|
+
// $FlowFixMe[prop-missing]
|
|
2424
|
+
return oldAngle + transform.angleDeg;
|
|
2425
|
+
},
|
|
2426
|
+
});
|
|
2427
|
+
|
|
2428
|
+
// Update tools.rotation.coord
|
|
2429
|
+
// $FlowFixMe[incompatible-use]
|
|
2430
|
+
this.rotatePoint.onMoveEnd = (x, y) => {
|
|
2431
|
+
self.changeTool("rotation", {
|
|
2432
|
+
coord: [x, y],
|
|
2433
|
+
});
|
|
2434
|
+
};
|
|
2435
|
+
|
|
2436
|
+
return {
|
|
2437
|
+
remove: function () {
|
|
2438
|
+
self.rotateHandle?.remove();
|
|
2439
|
+
self.rotatePoint?.remove();
|
|
2440
|
+
},
|
|
2441
|
+
};
|
|
2442
|
+
};
|
|
2443
|
+
|
|
2444
|
+
addDilationTool: () => ToolCleanup = () => {
|
|
2445
|
+
const options = this.props.tools.dilation;
|
|
2446
|
+
if (!options.enabled) {
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
const self = this;
|
|
2450
|
+
// eslint-disable-next-line react/no-string-refs
|
|
2451
|
+
const graphie = this.refs.graph.graphie();
|
|
2452
|
+
|
|
2453
|
+
const pointColor = colorForTool(options);
|
|
2454
|
+
// the circle for causing dilation transforms
|
|
2455
|
+
self.dilationCircle = graphie.addCircleGraph({
|
|
2456
|
+
centerConstraints: options.constraints,
|
|
2457
|
+
center: options.coord,
|
|
2458
|
+
radius: self.scaleToCurrentRange(2),
|
|
2459
|
+
snapX: graphie.snap[0],
|
|
2460
|
+
snapY: graphie.snap[1],
|
|
2461
|
+
minRadius: self.scaleToCurrentRange(1),
|
|
2462
|
+
snapRadius: self.scaleToCurrentRange(0.5),
|
|
2463
|
+
onResize: function (newRadius, oldRadius) {
|
|
2464
|
+
// NOTE(kevinb): object is missing .constraints property
|
|
2465
|
+
// $FlowFixMe[prop-missing]
|
|
2466
|
+
self.doTransform({
|
|
2467
|
+
type: "dilation",
|
|
2468
|
+
// NOTE(kevinb): It's hard to convince that this.dilationCircle
|
|
2469
|
+
// will still be defined here.
|
|
2470
|
+
// $FlowFixMe[incompatible-use]
|
|
2471
|
+
center: self.dilationCircle.centerPoint.coord,
|
|
2472
|
+
scale: newRadius / oldRadius,
|
|
2473
|
+
});
|
|
2474
|
+
},
|
|
2475
|
+
circleNormalStyle: {
|
|
2476
|
+
stroke: pointColor,
|
|
2477
|
+
"stroke-width": 2,
|
|
2478
|
+
"stroke-dasharray": "- ",
|
|
2479
|
+
"fill-opacity": 0,
|
|
2480
|
+
},
|
|
2481
|
+
circleHighlightStyle: {
|
|
2482
|
+
stroke: KhanColors.INTERACTING,
|
|
2483
|
+
"stroke-width": 2,
|
|
2484
|
+
"stroke-dasharray": "",
|
|
2485
|
+
fill: KhanColors.INTERACTING,
|
|
2486
|
+
"fill-opacity": 0.05,
|
|
2487
|
+
},
|
|
2488
|
+
centerNormalStyle: {
|
|
2489
|
+
stroke: pointColor,
|
|
2490
|
+
fill: pointColor,
|
|
2491
|
+
"stroke-width": 2,
|
|
2492
|
+
"stroke-dasharray": "",
|
|
2493
|
+
},
|
|
2494
|
+
centerHighlightStyle: {
|
|
2495
|
+
stroke: pointColor,
|
|
2496
|
+
fill: pointColor,
|
|
2497
|
+
"stroke-width": 2,
|
|
2498
|
+
"stroke-dasharray": "",
|
|
2499
|
+
},
|
|
2500
|
+
});
|
|
2501
|
+
|
|
2502
|
+
// $FlowFixMe[incompatible-use]
|
|
2503
|
+
const origOnMoveEnd = this.dilationCircle.centerPoint.onMoveEnd;
|
|
2504
|
+
// $FlowFixMe[incompatible-use]
|
|
2505
|
+
this.dilationCircle.centerPoint.onMoveEnd = (...args) => {
|
|
2506
|
+
if (origOnMoveEnd) {
|
|
2507
|
+
origOnMoveEnd(args);
|
|
2508
|
+
}
|
|
2509
|
+
self.changeTool("dilation", {
|
|
2510
|
+
// $FlowFixMe[incompatible-use]
|
|
2511
|
+
coord: self.dilationCircle.centerPoint.coord,
|
|
2512
|
+
});
|
|
2513
|
+
};
|
|
2514
|
+
|
|
2515
|
+
return {
|
|
2516
|
+
remove: function () {
|
|
2517
|
+
self.dilationCircle?.remove();
|
|
2518
|
+
},
|
|
2519
|
+
};
|
|
2520
|
+
};
|
|
2521
|
+
|
|
2522
|
+
// returns a transformation object representing a rotation
|
|
2523
|
+
// rounds the angle to the nearest 15 degrees
|
|
2524
|
+
getRotationTransformFromAngle: (
|
|
2525
|
+
Coord,
|
|
2526
|
+
number,
|
|
2527
|
+
) => TransformerTransformation = (center, angleChanged) => {
|
|
2528
|
+
angleChanged = (angleChanged + 360) % 360;
|
|
2529
|
+
if (angleChanged > 180) {
|
|
2530
|
+
angleChanged -= 360;
|
|
2531
|
+
}
|
|
2532
|
+
const roundedAngle =
|
|
2533
|
+
Math.round(angleChanged / ROTATE_SNAP_DEGREES) *
|
|
2534
|
+
ROTATE_SNAP_DEGREES;
|
|
2535
|
+
|
|
2536
|
+
// NOTE(kevinb): return object is missing .constraints property
|
|
2537
|
+
// $FlowFixMe[prop-missing]
|
|
2538
|
+
return {
|
|
2539
|
+
type: "rotation",
|
|
2540
|
+
center: center,
|
|
2541
|
+
angleDeg: roundedAngle,
|
|
2542
|
+
};
|
|
2543
|
+
};
|
|
2544
|
+
|
|
2545
|
+
// apply and save a transform
|
|
2546
|
+
doTransform: (TransformerTransformation, ?($FlowFixMe) => mixed) => void = (
|
|
2547
|
+
transform,
|
|
2548
|
+
callback,
|
|
2549
|
+
) => {
|
|
2550
|
+
this.applyTransform(transform);
|
|
2551
|
+
this.addTransform(transform, callback);
|
|
2552
|
+
};
|
|
2553
|
+
|
|
2554
|
+
// apply a transform to our polygon (without modifying our transformation
|
|
2555
|
+
// list)
|
|
2556
|
+
applyTransform: (TransformerTransformation) => void = (transform) => {
|
|
2557
|
+
if (this.props.graphMode !== "static") {
|
|
2558
|
+
const transformFunc = TransformOps.apply(transform);
|
|
2559
|
+
// $FlowFixMe[incompatible-call]
|
|
2560
|
+
this.applyCoordTransformation(transformFunc);
|
|
2561
|
+
}
|
|
2562
|
+
};
|
|
2563
|
+
|
|
2564
|
+
// transform our polygon by transforming each point using a given function
|
|
2565
|
+
applyCoordTransformation: (TransformerTransformation) => void = (
|
|
2566
|
+
pointTransform,
|
|
2567
|
+
) => {
|
|
2568
|
+
// $FlowFixMe[incompatible-use]
|
|
2569
|
+
_.each(this.shape.points, function (point) {
|
|
2570
|
+
// $FlowFixMe[prop-missing]
|
|
2571
|
+
const newCoord = pointTransform(point.coord);
|
|
2572
|
+
point.setCoord(newCoord);
|
|
2573
|
+
});
|
|
2574
|
+
this.shape?.update();
|
|
2575
|
+
};
|
|
2576
|
+
|
|
2577
|
+
resetCoords: () => void = () => {
|
|
2578
|
+
const startCoords = this.props.starting.shape.coords;
|
|
2579
|
+
// $FlowFixMe[incompatible-use]
|
|
2580
|
+
_.each(this.shape.points, function (point, i) {
|
|
2581
|
+
point.setCoord(startCoords[i]);
|
|
2582
|
+
});
|
|
2583
|
+
this.shape?.update();
|
|
2584
|
+
};
|
|
2585
|
+
|
|
2586
|
+
// Remove the last transformation
|
|
2587
|
+
handleUndoClick: () => void = () => {
|
|
2588
|
+
// eslint-disable-next-line react/no-string-refs
|
|
2589
|
+
this.refs.toolsBar.changeSelected(null);
|
|
2590
|
+
if (this.props.transformations.length) {
|
|
2591
|
+
this.props.onChange({
|
|
2592
|
+
transformations: _.initial(this.props.transformations),
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
};
|
|
2596
|
+
|
|
2597
|
+
setTransformationProps: (
|
|
2598
|
+
$ReadOnlyArray<TransformerTransformation>,
|
|
2599
|
+
($FlowFixMe) => mixed,
|
|
2600
|
+
) => void = (newTransfomationList, callback) => {
|
|
2601
|
+
this.props.onChange(
|
|
2602
|
+
{
|
|
2603
|
+
transformations: newTransfomationList,
|
|
2604
|
+
},
|
|
2605
|
+
callback,
|
|
2606
|
+
);
|
|
2607
|
+
};
|
|
2608
|
+
|
|
2609
|
+
// add a transformation to our props list of transformation
|
|
2610
|
+
addTransform: (TransformerTransformation, ?($FlowFixMe) => mixed) => void =
|
|
2611
|
+
(transform, callback) => {
|
|
2612
|
+
this.transformations = TransformOps.append(
|
|
2613
|
+
this.transformations,
|
|
2614
|
+
transform,
|
|
2615
|
+
);
|
|
2616
|
+
this.props.onChange(
|
|
2617
|
+
{
|
|
2618
|
+
transformations: _.clone(this.transformations),
|
|
2619
|
+
},
|
|
2620
|
+
callback,
|
|
2621
|
+
);
|
|
2622
|
+
};
|
|
2623
|
+
|
|
2624
|
+
changeTool: (Tool, $FlowFixMe) => void = (tool, changes) => {
|
|
2625
|
+
const newTools = _.clone(this.props.tools);
|
|
2626
|
+
newTools[tool] = _.extend({}, this.props.tools[tool], changes);
|
|
2627
|
+
this.tools[tool] = _.clone(newTools[tool]);
|
|
2628
|
+
this.props.onChange({
|
|
2629
|
+
tools: newTools,
|
|
2630
|
+
});
|
|
2631
|
+
this.props.trackInteraction();
|
|
2632
|
+
};
|
|
2633
|
+
|
|
2634
|
+
simpleValidate: (Rubric) => PerseusScore = (rubric) => {
|
|
2635
|
+
return Transformer.validate(this.getUserInput(), rubric);
|
|
2636
|
+
};
|
|
2637
|
+
|
|
2638
|
+
/**
|
|
2639
|
+
* Calculate where the coordinates would be if they were
|
|
2640
|
+
* moved, even if we're in formal mode with no movement
|
|
2641
|
+
* (and thus the actual movablepoints may not have moved
|
|
2642
|
+
*/
|
|
2643
|
+
getCoords: () => Coord = () => {
|
|
2644
|
+
const startCoords = this.props.starting.shape.coords;
|
|
2645
|
+
const transforms = this.props.transformations;
|
|
2646
|
+
return _.reduce(
|
|
2647
|
+
transforms,
|
|
2648
|
+
function (coords, transform) {
|
|
2649
|
+
return _.map(coords, TransformOps.apply(transform));
|
|
2650
|
+
},
|
|
2651
|
+
startCoords,
|
|
2652
|
+
);
|
|
2653
|
+
};
|
|
2654
|
+
|
|
2655
|
+
getEditorJSON: () => $FlowFixMe = () => {
|
|
2656
|
+
const json = _.pick(
|
|
2657
|
+
this.props,
|
|
2658
|
+
"grading",
|
|
2659
|
+
"starting",
|
|
2660
|
+
"graphMode",
|
|
2661
|
+
"listMode",
|
|
2662
|
+
"tools",
|
|
2663
|
+
"drawSolutionShape",
|
|
2664
|
+
"gradeEmpty",
|
|
2665
|
+
);
|
|
2666
|
+
// eslint-disable-next-line react/no-string-refs
|
|
2667
|
+
json.graph = this.refs.graph.toJSON();
|
|
2668
|
+
json.version = 1.2; // Give us some safety to change the format
|
|
2669
|
+
// when we realize that I wrote
|
|
2670
|
+
// a horrible json spec for this widget
|
|
2671
|
+
|
|
2672
|
+
json.answer = this.getUserInput();
|
|
2673
|
+
return json;
|
|
2674
|
+
};
|
|
2675
|
+
|
|
2676
|
+
getUserInput: UserInput = () => {
|
|
2677
|
+
return {
|
|
2678
|
+
transformations: this.props.transformations,
|
|
2679
|
+
// This doesn't call this.shape.toJSON() because that doesn't
|
|
2680
|
+
// handle coordinates in formal mode without movement, since
|
|
2681
|
+
// the movablepoints never move
|
|
2682
|
+
shape: {
|
|
2683
|
+
// $FlowFixMe[incompatible-use]
|
|
2684
|
+
type: this.shape.type,
|
|
2685
|
+
coords: this.getCoords(),
|
|
2686
|
+
options: this.shape?.getOptions(),
|
|
2687
|
+
},
|
|
2688
|
+
};
|
|
2689
|
+
};
|
|
2690
|
+
|
|
2691
|
+
/* InputPath API */
|
|
2692
|
+
|
|
2693
|
+
_handleFocus: (...$ReadOnlyArray<string>) => void = (...path) => {
|
|
2694
|
+
this.props.onFocus(path);
|
|
2695
|
+
};
|
|
2696
|
+
|
|
2697
|
+
_handleBlur: (...$ReadOnlyArray<string>) => void = (...path) => {
|
|
2698
|
+
this.props.onBlur(path);
|
|
2699
|
+
};
|
|
2700
|
+
|
|
2701
|
+
_getTransformationForID: (string) => $FlowFixMe = (transformationID) => {
|
|
2702
|
+
// Returns the 'transformation' component corresponding to a given ID
|
|
2703
|
+
const refPath = [
|
|
2704
|
+
"transformationList",
|
|
2705
|
+
"transformation" + transformationID,
|
|
2706
|
+
];
|
|
2707
|
+
|
|
2708
|
+
// Follow the path of references
|
|
2709
|
+
let component = this;
|
|
2710
|
+
_.each(refPath, (ref) => {
|
|
2711
|
+
component = component.refs[ref];
|
|
2712
|
+
});
|
|
2713
|
+
return component;
|
|
2714
|
+
};
|
|
2715
|
+
|
|
2716
|
+
getInputPaths: () => $ReadOnlyArray<Path> = () => {
|
|
2717
|
+
// If we're in static mode, then there is no transformation list, and,
|
|
2718
|
+
// as a result, no input paths.
|
|
2719
|
+
if (this.props.listMode === "static") {
|
|
2720
|
+
return [];
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
let inputPaths = [];
|
|
2724
|
+
_.each(this.props.transformations, (transformation, i) => {
|
|
2725
|
+
transformation = this._getTransformationForID(i);
|
|
2726
|
+
const innerPaths = transformation.getInputPaths();
|
|
2727
|
+
const fullPaths = _.map(innerPaths, (innerPath) => {
|
|
2728
|
+
return ["" + i].concat(innerPath);
|
|
2729
|
+
});
|
|
2730
|
+
inputPaths = inputPaths.concat(fullPaths);
|
|
2731
|
+
});
|
|
2732
|
+
return inputPaths;
|
|
2733
|
+
};
|
|
2734
|
+
|
|
2735
|
+
_passToInner: (
|
|
2736
|
+
string,
|
|
2737
|
+
$ReadOnlyArray<string>,
|
|
2738
|
+
?$FlowFixMe,
|
|
2739
|
+
?($FlowFixMe) => mixed,
|
|
2740
|
+
) => $FlowFixMe = function (functionName, path, ...args) {
|
|
2741
|
+
if (!path || !path.length) {
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
// First argument tells us which transformation will receive the call;
|
|
2746
|
+
// remaining arguments are used within that transformation to identify
|
|
2747
|
+
// a specific input.
|
|
2748
|
+
const innerPath = _.rest(path);
|
|
2749
|
+
|
|
2750
|
+
// Pass arguments down to appropriate 'transformation' component
|
|
2751
|
+
const transformationID = _.head(path);
|
|
2752
|
+
const caller = this._getTransformationForID(transformationID);
|
|
2753
|
+
return caller[functionName](innerPath, ...args);
|
|
2754
|
+
};
|
|
2755
|
+
|
|
2756
|
+
focus: () => boolean = () => {
|
|
2757
|
+
// Just focus the first showing input
|
|
2758
|
+
const inputs = this.getInputPaths();
|
|
2759
|
+
if (inputs.length > 0) {
|
|
2760
|
+
this.focusInputPath(inputs[0]);
|
|
2761
|
+
return true;
|
|
2762
|
+
}
|
|
2763
|
+
return false;
|
|
2764
|
+
};
|
|
2765
|
+
|
|
2766
|
+
focusInputPath: (Path) => boolean = (path) => {
|
|
2767
|
+
// Since the transformer exposes the input API, it needs to be robust
|
|
2768
|
+
// to empty paths. We don't expect this to happen, as entire-widget
|
|
2769
|
+
// focusing is typically done through the focus() method, which already
|
|
2770
|
+
// handles the empty path case properly, but it's better to be safe
|
|
2771
|
+
// here.
|
|
2772
|
+
if (path.length === 0) {
|
|
2773
|
+
return false;
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
assert(path.length >= 2);
|
|
2777
|
+
return this._passToInner("focusInputPath", path);
|
|
2778
|
+
};
|
|
2779
|
+
|
|
2780
|
+
blurInputPath: (Path) => boolean = (path) => {
|
|
2781
|
+
// Since the transformer exposes the input API, it needs to be robust
|
|
2782
|
+
// to empty paths (which indicate a blurring of the entire widget,
|
|
2783
|
+
// e.g., when switching from interacting with the transformer to
|
|
2784
|
+
// interacting with some other widget).
|
|
2785
|
+
if (path.length === 0) {
|
|
2786
|
+
return false;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
assert(path.length >= 2);
|
|
2790
|
+
return this._passToInner("blurInputPath", path);
|
|
2791
|
+
};
|
|
2792
|
+
|
|
2793
|
+
setInputValue: (Path, $FlowFixMe, ($FlowFixMe) => mixed) => $FlowFixMe = (
|
|
2794
|
+
path,
|
|
2795
|
+
value,
|
|
2796
|
+
cb,
|
|
2797
|
+
) => {
|
|
2798
|
+
assert(path.length >= 2);
|
|
2799
|
+
return this._passToInner("setInputValue", path, value, cb);
|
|
2800
|
+
};
|
|
2801
|
+
|
|
2802
|
+
getDOMNodeForPath: (Path) => $FlowFixMe = (path) => {
|
|
2803
|
+
assert(path.length >= 2);
|
|
2804
|
+
return this._passToInner("getDOMNodeForPath", path);
|
|
2805
|
+
};
|
|
2806
|
+
|
|
2807
|
+
getGrammarTypeForPath: (Path) => $FlowFixMe = (path) => {
|
|
2808
|
+
assert(path.length >= 2);
|
|
2809
|
+
return this._passToInner("getGrammarTypeForPath", path);
|
|
2810
|
+
};
|
|
2811
|
+
|
|
2812
|
+
static validate(guess: UserInput, rubric: Rubric): PerseusScore {
|
|
2813
|
+
// Check for any required transformations
|
|
2814
|
+
for (const type in Transformations) {
|
|
2815
|
+
if (rubric.tools[type].required) {
|
|
2816
|
+
const isUsed = _.any(
|
|
2817
|
+
_.map(guess.transformations, function (transform) {
|
|
2818
|
+
// Required transformations must appear in the
|
|
2819
|
+
// transformation list, and must not be no-ops
|
|
2820
|
+
return (
|
|
2821
|
+
transform.type === type &&
|
|
2822
|
+
!TransformOps.isEmpty(transform) &&
|
|
2823
|
+
!TransformOps.isNoOp(transform)
|
|
2824
|
+
);
|
|
2825
|
+
}),
|
|
2826
|
+
);
|
|
2827
|
+
|
|
2828
|
+
if (!isUsed) {
|
|
2829
|
+
return {
|
|
2830
|
+
type: "invalid",
|
|
2831
|
+
message: i18n._(
|
|
2832
|
+
"Your transformation must use a " + "%(type)s.",
|
|
2833
|
+
{
|
|
2834
|
+
type: Transformations[type].lowerNounName,
|
|
2835
|
+
},
|
|
2836
|
+
),
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
// Compare shapes
|
|
2843
|
+
if (ShapeTypes.equal(guess.shape, rubric.correct.shape)) {
|
|
2844
|
+
return {
|
|
2845
|
+
type: "points",
|
|
2846
|
+
earned: 1,
|
|
2847
|
+
total: 1,
|
|
2848
|
+
message: null,
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
if (
|
|
2852
|
+
!rubric.gradeEmpty &&
|
|
2853
|
+
deepEq(guess.shape.coords, rubric.starting.shape.coords)
|
|
2854
|
+
) {
|
|
2855
|
+
return {
|
|
2856
|
+
type: "invalid",
|
|
2857
|
+
message: i18n._(
|
|
2858
|
+
"Use the interactive graph to define a " +
|
|
2859
|
+
"correct transformation.",
|
|
2860
|
+
),
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
return {
|
|
2864
|
+
type: "points",
|
|
2865
|
+
earned: 0,
|
|
2866
|
+
total: 1,
|
|
2867
|
+
message: null,
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
export default ({
|
|
2873
|
+
name: "transformer",
|
|
2874
|
+
displayName: "Transformer",
|
|
2875
|
+
widget: Transformer,
|
|
2876
|
+
}: WidgetExports<typeof Transformer>);
|