@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,2820 @@
|
|
|
1
|
+
/* eslint-disable @babel/no-invalid-this, react/no-unsafe, react/sort-comp */
|
|
2
|
+
// @flow
|
|
3
|
+
import {number as knumber, point as kpoint} from "@khanacademy/kmath";
|
|
4
|
+
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
5
|
+
import $ from "jquery";
|
|
6
|
+
import debounce from "lodash.debounce";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import _ from "underscore";
|
|
9
|
+
|
|
10
|
+
import Graph from "../components/graph.jsx";
|
|
11
|
+
import Interactive2 from "../interactive2.js";
|
|
12
|
+
import WrappedLine from "../interactive2/wrapped-line.js";
|
|
13
|
+
import {Errors} from "../logging/log.js";
|
|
14
|
+
import {PerseusError} from "../perseus-error.js";
|
|
15
|
+
import Util from "../util.js";
|
|
16
|
+
import KhanColors from "../util/colors.js";
|
|
17
|
+
import GraphUtils from "../util/graph-utils.js";
|
|
18
|
+
import {getInteractiveBoxFromSizeClass} from "../util/sizing-utils.js";
|
|
19
|
+
|
|
20
|
+
import type {Coord, Line} from "../interactive2/types.js";
|
|
21
|
+
import type {
|
|
22
|
+
PerseusGraphType,
|
|
23
|
+
PerseusGraphTypeAngle,
|
|
24
|
+
PerseusGraphTypePoint,
|
|
25
|
+
PerseusGraphTypePolygon,
|
|
26
|
+
PerseusGraphTypeSegment,
|
|
27
|
+
PerseusInteractiveGraphWidgetOptions,
|
|
28
|
+
} from "../perseus-types.js";
|
|
29
|
+
import type {PerseusScore, WidgetExports, WidgetProps} from "../types.js";
|
|
30
|
+
|
|
31
|
+
type Range = $ReadOnlyArray<number>; // This should really be a readonly tuple of [number, number]
|
|
32
|
+
type SineCoefficient = [
|
|
33
|
+
/* amplitude */ number,
|
|
34
|
+
/* angularFrequency */ number,
|
|
35
|
+
/* phase */ number,
|
|
36
|
+
/* verticalOffset */ number,
|
|
37
|
+
];
|
|
38
|
+
type QuadraticCoefficient = [/* a */ number, /* b */ number, /* c */ number];
|
|
39
|
+
|
|
40
|
+
const {DeprecationMixin} = Util;
|
|
41
|
+
|
|
42
|
+
const TRASH_ICON_URI =
|
|
43
|
+
"https://ka-perseus-graphie.s3.amazonaws.com/b1452c0d79fd0f7ff4c3af9488474a0a0decb361.png";
|
|
44
|
+
|
|
45
|
+
const defaultBackgroundImage = {
|
|
46
|
+
url: null,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const eq = Util.eq;
|
|
50
|
+
const deepEq = Util.deepEq;
|
|
51
|
+
|
|
52
|
+
const UNLIMITED = ("unlimited": "unlimited");
|
|
53
|
+
|
|
54
|
+
// Sample background image:
|
|
55
|
+
// https://ka-perseus-graphie.s3.amazonaws.com/29c1b0fcd17fe63df0f148fe357044d5d5c7d0bb.png
|
|
56
|
+
|
|
57
|
+
function ccw(a: Coord, b: Coord, c: Coord): number {
|
|
58
|
+
return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collinear(a: Coord, b: Coord, c: Coord): boolean {
|
|
62
|
+
return eq(ccw(a, b, c), 0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sign(val: number): 0 | 1 | -1 {
|
|
66
|
+
if (eq(val, 0)) {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
return val > 0 ? 1 : -1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// default to defaultValue if actual is null or undefined
|
|
73
|
+
function defaultVal<T>(actual: ?T, defaultValue: T): T {
|
|
74
|
+
return actual == null ? defaultValue : actual;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Given rect bounding points A and B, whether point C is inside the rect
|
|
78
|
+
function pointInRect(a: Coord, b: Coord, c: Coord): boolean {
|
|
79
|
+
return (
|
|
80
|
+
c[0] <= Math.max(a[0], b[0]) &&
|
|
81
|
+
c[0] >= Math.min(a[0], b[0]) &&
|
|
82
|
+
c[1] <= Math.max(a[1], b[1]) &&
|
|
83
|
+
c[1] >= Math.min(a[1], b[1])
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Whether line segment AB intersects line segment CD
|
|
88
|
+
// http://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
|
|
89
|
+
function intersects(ab: Line, cd: Line): boolean {
|
|
90
|
+
const triplets = [
|
|
91
|
+
[ab[0], ab[1], cd[0]],
|
|
92
|
+
[ab[0], ab[1], cd[1]],
|
|
93
|
+
[cd[0], cd[1], ab[0]],
|
|
94
|
+
[cd[0], cd[1], ab[1]],
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const orientations = _.map(triplets, function (triplet) {
|
|
98
|
+
return sign(ccw(...triplet));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
orientations[0] !== orientations[1] &&
|
|
103
|
+
orientations[2] !== orientations[3]
|
|
104
|
+
) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < 4; i++) {
|
|
109
|
+
if (orientations[i] === 0 && pointInRect(...triplets[i])) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function vector(a, b) {
|
|
118
|
+
return _.map(_.zip(a, b), function (pair) {
|
|
119
|
+
return pair[0] - pair[1];
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function magnitude(v: $ReadOnlyArray<Coord>): number {
|
|
124
|
+
return Math.sqrt(
|
|
125
|
+
_.reduce(
|
|
126
|
+
v,
|
|
127
|
+
function (memo, el) {
|
|
128
|
+
return memo + Math.pow(el, 2);
|
|
129
|
+
},
|
|
130
|
+
0,
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function dotProduct(a: Coord, b: Coord): number {
|
|
136
|
+
return _.reduce(
|
|
137
|
+
_.zip(a, b),
|
|
138
|
+
function (memo, pair) {
|
|
139
|
+
return memo + pair[0] * pair[1];
|
|
140
|
+
},
|
|
141
|
+
0,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sideLengths(coords: $ReadOnlyArray<Coord>): $ReadOnlyArray<number> {
|
|
146
|
+
const segments = _.zip(coords, rotate(coords));
|
|
147
|
+
return segments.map(function (segment) {
|
|
148
|
+
return magnitude(vector(...segment));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Based on http://math.stackexchange.com/a/151149
|
|
153
|
+
function angleMeasures(coords: $ReadOnlyArray<Coord>): $ReadOnlyArray<number> {
|
|
154
|
+
const triplets = _.zip(rotate(coords, -1), coords, rotate(coords, 1));
|
|
155
|
+
|
|
156
|
+
const offsets = _.map(triplets, function (triplet) {
|
|
157
|
+
const p = vector(triplet[1], triplet[0]);
|
|
158
|
+
const q = vector(triplet[2], triplet[1]);
|
|
159
|
+
const raw = Math.acos(dotProduct(p, q) / (magnitude(p) * magnitude(q)));
|
|
160
|
+
return sign(ccw(...triplet)) > 0 ? raw : -raw;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const sum = _.reduce(
|
|
164
|
+
offsets,
|
|
165
|
+
function (memo, arg) {
|
|
166
|
+
return memo + arg;
|
|
167
|
+
},
|
|
168
|
+
0,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
return _.map(offsets, function (offset) {
|
|
172
|
+
return sum > 0 ? Math.PI - offset : Math.PI + offset;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Whether two polygons are similar (or if specified, congruent)
|
|
177
|
+
function similar(
|
|
178
|
+
coords1: $ReadOnlyArray<Coord>,
|
|
179
|
+
coords2: $ReadOnlyArray<Coord>,
|
|
180
|
+
tolerance: number,
|
|
181
|
+
): boolean {
|
|
182
|
+
if (coords1.length !== coords2.length) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const n = coords1.length;
|
|
187
|
+
|
|
188
|
+
const angles1 = angleMeasures(coords1);
|
|
189
|
+
const angles2 = angleMeasures(coords2);
|
|
190
|
+
|
|
191
|
+
const sides1 = sideLengths(coords1);
|
|
192
|
+
const sides2 = sideLengths(coords2);
|
|
193
|
+
|
|
194
|
+
for (let i = 0; i < 2 * n; i++) {
|
|
195
|
+
let angles = angles2.slice();
|
|
196
|
+
let sides = sides2.slice();
|
|
197
|
+
|
|
198
|
+
// Reverse angles and sides to allow matching reflected polygons
|
|
199
|
+
if (i >= n) {
|
|
200
|
+
angles.reverse();
|
|
201
|
+
sides.reverse();
|
|
202
|
+
// Since sides are calculated from two coordinates,
|
|
203
|
+
// simply reversing results in an off by one error
|
|
204
|
+
sides = rotate(sides, 1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
angles = rotate(angles, i);
|
|
208
|
+
sides = rotate(sides, i);
|
|
209
|
+
|
|
210
|
+
if (deepEq(angles1, angles)) {
|
|
211
|
+
const sidePairs = _.zip(sides1, sides);
|
|
212
|
+
|
|
213
|
+
const factors = _.map(sidePairs, function (pair) {
|
|
214
|
+
return pair[0] / pair[1];
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const same = _.all(factors, function (factor) {
|
|
218
|
+
return eq(factors[0], factor);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const congruentEnough = _.all(sidePairs, function (pair) {
|
|
222
|
+
return knumber.equal(pair[0], pair[1], tolerance);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (same && congruentEnough) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Less than or approximately equal
|
|
235
|
+
function leq(a, b) {
|
|
236
|
+
return a < b || eq(a, b);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Given triangle with sides ABC return angle opposite side C in degrees
|
|
240
|
+
function lawOfCosines(a: number, b: number, c: number): number {
|
|
241
|
+
return (Math.acos((a * a + b * b - c * c) / (2 * a * b)) * 180) / Math.PI;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function canonicalSineCoefficients([
|
|
245
|
+
amplitude,
|
|
246
|
+
angularFrequency,
|
|
247
|
+
phase,
|
|
248
|
+
verticalOffset,
|
|
249
|
+
]: SineCoefficient) {
|
|
250
|
+
// For a curve of the form f(x) = a * Sin(b * x - c) + d,
|
|
251
|
+
// this function ensures that a, b > 0, and c is its
|
|
252
|
+
// smallest possible positive value.
|
|
253
|
+
|
|
254
|
+
// Guarantee a > 0
|
|
255
|
+
if (amplitude < 0) {
|
|
256
|
+
amplitude *= -1;
|
|
257
|
+
angularFrequency *= -1;
|
|
258
|
+
phase *= -1;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const period = 2 * Math.PI;
|
|
262
|
+
// Guarantee b > 0
|
|
263
|
+
if (angularFrequency < 0) {
|
|
264
|
+
angularFrequency *= -1;
|
|
265
|
+
phase *= -1;
|
|
266
|
+
phase += period / 2;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Guarantee c is smallest possible positive value
|
|
270
|
+
while (phase > 0) {
|
|
271
|
+
phase -= period;
|
|
272
|
+
}
|
|
273
|
+
while (phase < 0) {
|
|
274
|
+
phase += period;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return [amplitude, angularFrequency, phase, verticalOffset];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// e.g. rotate([1, 2, 3]) -> [2, 3, 1]
|
|
281
|
+
function rotate<T>(array: $ReadOnlyArray<T>, n?: number): $ReadOnlyArray<T> {
|
|
282
|
+
n = typeof n === "undefined" ? 1 : n % array.length;
|
|
283
|
+
return array.slice(n).concat(array.slice(0, n));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function capitalize(str) {
|
|
287
|
+
return str.replace(/(?:^|-)(.)/g, function (match, letter) {
|
|
288
|
+
return letter.toUpperCase();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function getLineEquation(first: Coord, second: Coord): string {
|
|
293
|
+
if (eq(first[0], second[0])) {
|
|
294
|
+
return "x = " + first[0].toFixed(3);
|
|
295
|
+
}
|
|
296
|
+
const m = (second[1] - first[1]) / (second[0] - first[0]);
|
|
297
|
+
const b = first[1] - m * first[0];
|
|
298
|
+
return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Stolen from the wikipedia article
|
|
302
|
+
// http://en.wikipedia.org/wiki/Line-line_intersection
|
|
303
|
+
function getLineIntersection(
|
|
304
|
+
// TODO(LP-10725): update these to be 2-tuples
|
|
305
|
+
firstPoints: $ReadOnlyArray<Coord>,
|
|
306
|
+
secondPoints: $ReadOnlyArray<Coord>,
|
|
307
|
+
): string {
|
|
308
|
+
const x1 = firstPoints[0][0];
|
|
309
|
+
const y1 = firstPoints[0][1];
|
|
310
|
+
const x2 = firstPoints[1][0];
|
|
311
|
+
const y2 = firstPoints[1][1];
|
|
312
|
+
const x3 = secondPoints[0][0];
|
|
313
|
+
const y3 = secondPoints[0][1];
|
|
314
|
+
const x4 = secondPoints[1][0];
|
|
315
|
+
const y4 = secondPoints[1][1];
|
|
316
|
+
|
|
317
|
+
const determinant = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
|
318
|
+
|
|
319
|
+
if (Math.abs(determinant) < 1e-9) {
|
|
320
|
+
return "Lines are parallel";
|
|
321
|
+
}
|
|
322
|
+
const x =
|
|
323
|
+
((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) /
|
|
324
|
+
determinant;
|
|
325
|
+
const y =
|
|
326
|
+
((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) /
|
|
327
|
+
determinant;
|
|
328
|
+
return "Intersection: (" + x.toFixed(3) + ", " + y.toFixed(3) + ")";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function numSteps(range: Range, step: number) {
|
|
332
|
+
return Math.floor((range[1] - range[0]) / step);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const deprecatedProps = {
|
|
336
|
+
showGraph: function (props) {
|
|
337
|
+
return {markings: props.showGraph ? "graph" : "none"};
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const _getShouldShowInstructions: (Props) => boolean = (props) => {
|
|
342
|
+
return (
|
|
343
|
+
_isClickToAddPoints(props) &&
|
|
344
|
+
(props.graph.coords == null || props.graph.coords.length === 0)
|
|
345
|
+
);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const _isClickToAddPoints: (Props) => boolean = (props) => {
|
|
349
|
+
return (
|
|
350
|
+
(props.graph.type === "point" && props.graph.numPoints === UNLIMITED) ||
|
|
351
|
+
(props.graph.type === "polygon" && props.graph.numSides === UNLIMITED)
|
|
352
|
+
);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const makeInvalidTypeError = (
|
|
356
|
+
functionName: string,
|
|
357
|
+
graphType: string,
|
|
358
|
+
): PerseusError => {
|
|
359
|
+
return new PerseusError(
|
|
360
|
+
`${functionName} called but current graph type is not a '${graphType}'`,
|
|
361
|
+
Errors.NotAllowed,
|
|
362
|
+
{metadata: {graphType}},
|
|
363
|
+
);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
type RenderProps = PerseusInteractiveGraphWidgetOptions; // There's no transform function in exports
|
|
367
|
+
type Rubric = PerseusInteractiveGraphWidgetOptions;
|
|
368
|
+
type Props = WidgetProps<RenderProps, Rubric>;
|
|
369
|
+
type State = $FlowFixMe;
|
|
370
|
+
type DefaultProps = {|
|
|
371
|
+
labels: Props["labels"],
|
|
372
|
+
range: Props["range"],
|
|
373
|
+
step: Props["step"],
|
|
374
|
+
backgroundImage: Props["backgroundImage"],
|
|
375
|
+
markings: Props["markings"],
|
|
376
|
+
showTooltips: Props["showTooltips"],
|
|
377
|
+
showProtractor: Props["showProtractor"],
|
|
378
|
+
showRuler: Props["showRuler"],
|
|
379
|
+
rulerLabel: Props["rulerLabel"],
|
|
380
|
+
rulerTicks: Props["rulerTicks"],
|
|
381
|
+
graph: Props["graph"],
|
|
382
|
+
|};
|
|
383
|
+
|
|
384
|
+
class InteractiveGraph extends React.Component<Props, State> {
|
|
385
|
+
angle: ?$FlowFixMe;
|
|
386
|
+
circle: ?$FlowFixMe;
|
|
387
|
+
graphie: ?$FlowFixMe;
|
|
388
|
+
horizHairline: ?$FlowFixMe;
|
|
389
|
+
line: ?$FlowFixMe;
|
|
390
|
+
lines: ?$ReadOnlyArray<$ReadOnlyArray<Coord>>;
|
|
391
|
+
parabola: ?$FlowFixMe;
|
|
392
|
+
pointA: ?$FlowFixMe;
|
|
393
|
+
pointB: ?$FlowFixMe;
|
|
394
|
+
pointC: ?$FlowFixMe;
|
|
395
|
+
// eslint-disable-next-line flowtype/no-mutable-array
|
|
396
|
+
points: Array<$FlowFixMe>;
|
|
397
|
+
polygon: ?$FlowFixMe;
|
|
398
|
+
shouldResetGraphie: boolean;
|
|
399
|
+
sinusoid: ?$FlowFixMe;
|
|
400
|
+
trashCan: ?$FlowFixMe;
|
|
401
|
+
vertHairline: ?$FlowFixMe;
|
|
402
|
+
|
|
403
|
+
static defaultProps: DefaultProps = {
|
|
404
|
+
labels: ["x", "y"],
|
|
405
|
+
range: [
|
|
406
|
+
[-10, 10],
|
|
407
|
+
[-10, 10],
|
|
408
|
+
],
|
|
409
|
+
step: [1, 1],
|
|
410
|
+
// $FlowFixMe[prop-missing]
|
|
411
|
+
backgroundImage: defaultBackgroundImage,
|
|
412
|
+
markings: "graph",
|
|
413
|
+
showTooltips: false,
|
|
414
|
+
showProtractor: false,
|
|
415
|
+
showRuler: false,
|
|
416
|
+
rulerLabel: "",
|
|
417
|
+
rulerTicks: 10,
|
|
418
|
+
graph: {
|
|
419
|
+
type: "linear",
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
state: State = {
|
|
424
|
+
shouldShowInstructions: _getShouldShowInstructions(this.props),
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// TODO(jangmi, CP-3288): Remove usage of `UNSAFE_componentWillMount`
|
|
428
|
+
UNSAFE_componentWillMount() {
|
|
429
|
+
DeprecationMixin.UNSAFE_componentWillMount.call(this);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
componentDidMount() {
|
|
433
|
+
// eslint-disable-next-line react/no-string-refs
|
|
434
|
+
this.setGraphie(this.refs.graph.graphie());
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
|
438
|
+
if (this.isClickToAddPoints() !== this.isClickToAddPoints(nextProps)) {
|
|
439
|
+
this.shouldResetGraphie = true;
|
|
440
|
+
this.setState({
|
|
441
|
+
shouldShowInstructions: _getShouldShowInstructions(nextProps),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (
|
|
446
|
+
this.props.backgroundImage?.url !==
|
|
447
|
+
nextProps.backgroundImage?.url ||
|
|
448
|
+
this.props.backgroundImage !== nextProps.backgroundImage ||
|
|
449
|
+
this.props.containerSizeClass !== nextProps.containerSizeClass
|
|
450
|
+
) {
|
|
451
|
+
this.shouldResetGraphie = true;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
componentDidUpdate(prevProps: Props, prevState: State) {
|
|
456
|
+
// TODO(jeremy): This feels underspecified. For example, if the props
|
|
457
|
+
// change the coords of a segment graph, but not the numSegments, we'd
|
|
458
|
+
// not count that as an update.
|
|
459
|
+
const oldType = prevProps.graph.type;
|
|
460
|
+
const newType = this.props.graph.type;
|
|
461
|
+
if (
|
|
462
|
+
oldType !== newType ||
|
|
463
|
+
(prevProps.graph.type === "angle" &&
|
|
464
|
+
this.props.graph.type === "angle" &&
|
|
465
|
+
this.areAngleGraphsEqual(prevProps.graph, this.props.graph)) ||
|
|
466
|
+
(prevProps.graph.type === "point" &&
|
|
467
|
+
this.props.graph.type === "point" &&
|
|
468
|
+
this.arePointGraphsEqual(prevProps.graph, this.props.graph)) ||
|
|
469
|
+
(prevProps.graph.type === "polygon" &&
|
|
470
|
+
this.props.graph.type === "polygon" &&
|
|
471
|
+
this.arePolygonGraphsEqual(
|
|
472
|
+
prevProps.graph,
|
|
473
|
+
this.props.graph,
|
|
474
|
+
)) ||
|
|
475
|
+
(prevProps.graph.type === "segment" &&
|
|
476
|
+
this.props.graph.type === "segment" &&
|
|
477
|
+
this.areSegmentGraphsEqual(prevProps.graph, this.props.graph))
|
|
478
|
+
) {
|
|
479
|
+
// $FlowFixMe[incompatible-use]
|
|
480
|
+
this["remove" + capitalize(oldType) + "Controls"]();
|
|
481
|
+
// $FlowFixMe[incompatible-use]
|
|
482
|
+
this["add" + capitalize(newType) + "Controls"]();
|
|
483
|
+
}
|
|
484
|
+
if (this.shouldResetGraphie) {
|
|
485
|
+
this.resetGraphie();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
_getShouldShowInstructions: (?Props) => boolean = (props) => {
|
|
490
|
+
props = props || this.props;
|
|
491
|
+
return (
|
|
492
|
+
this.isClickToAddPoints(props) &&
|
|
493
|
+
(props.graph.coords == null || props.graph.coords.length === 0)
|
|
494
|
+
);
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
deprecatedProps: $FlowFixMe = deprecatedProps;
|
|
498
|
+
|
|
499
|
+
setGraphie: ($FlowFixMe) => void = (newGraphie) => {
|
|
500
|
+
this.graphie = newGraphie;
|
|
501
|
+
this.setupGraphie();
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
handleAddPointsMouseDown: (Coord) => void = (coord) => {
|
|
505
|
+
// This function should only be called when this.isClickToAddPoints()
|
|
506
|
+
// is true
|
|
507
|
+
if (!this.isClickToAddPoints()) {
|
|
508
|
+
throw new PerseusError(
|
|
509
|
+
"handleAddPointsClick should not be registered" +
|
|
510
|
+
"when isClickToAddPoints() is false",
|
|
511
|
+
Errors.NotAllowed,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
if (!this.isCoordInTrash(coord)) {
|
|
515
|
+
let point;
|
|
516
|
+
|
|
517
|
+
if (this.props.graph.type === "point") {
|
|
518
|
+
point = this.createPointForPointsType(
|
|
519
|
+
coord,
|
|
520
|
+
this.points.length,
|
|
521
|
+
);
|
|
522
|
+
if (!point.constrain()) {
|
|
523
|
+
point.remove();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
this.points.push(point);
|
|
527
|
+
|
|
528
|
+
// interactive2 allows us to grab the point
|
|
529
|
+
const idx = this.points.length - 1;
|
|
530
|
+
this.points[idx].grab(coord);
|
|
531
|
+
|
|
532
|
+
this.updateCoordsFromPoints();
|
|
533
|
+
} else if (this.props.graph.type === "polygon") {
|
|
534
|
+
if (this.polygon?.closed()) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
point = this.createPointForPolygonType(
|
|
538
|
+
coord,
|
|
539
|
+
this.points.length,
|
|
540
|
+
);
|
|
541
|
+
this.points.push(point);
|
|
542
|
+
|
|
543
|
+
const idx = this.points.length - 1;
|
|
544
|
+
this.points[idx].grab(coord);
|
|
545
|
+
|
|
546
|
+
// We don't call updateCoordsFromPoints for
|
|
547
|
+
// polygons, since the polygon won't be
|
|
548
|
+
// closed yet.
|
|
549
|
+
this.updatePolygon();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
this.setState({
|
|
553
|
+
shouldShowInstructions: false,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
resetGraphie: () => void = () => {
|
|
559
|
+
this.shouldResetGraphie = false;
|
|
560
|
+
this.parabola = null;
|
|
561
|
+
this.sinusoid = null;
|
|
562
|
+
// eslint-disable-next-line react/no-string-refs
|
|
563
|
+
this.refs.graph.reset();
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
setupGraphie: () => void = () => {
|
|
567
|
+
this.setTrashCanVisibility(0);
|
|
568
|
+
if (this.isClickToAddPoints()) {
|
|
569
|
+
this.setTrashCanVisibility(0.5);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (this.props.apiOptions.isMobile) {
|
|
573
|
+
// $FlowFixMe[invalid-constructor]
|
|
574
|
+
this.horizHairline = new WrappedLine(this.graphie, [0, 0], [0, 0], {
|
|
575
|
+
normalStyle: {
|
|
576
|
+
strokeWidth: 1,
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
this.horizHairline.attr({
|
|
580
|
+
stroke: KhanColors.INTERACTIVE,
|
|
581
|
+
});
|
|
582
|
+
this.horizHairline?.hide();
|
|
583
|
+
|
|
584
|
+
// $FlowFixMe[invalid-constructor]
|
|
585
|
+
this.vertHairline = new WrappedLine(this.graphie, [0, 0], [0, 0], {
|
|
586
|
+
normalStyle: {
|
|
587
|
+
strokeWidth: 1,
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
this.vertHairline.attr({
|
|
591
|
+
stroke: KhanColors.INTERACTIVE,
|
|
592
|
+
});
|
|
593
|
+
this.vertHairline?.hide();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const type = this.props.graph.type;
|
|
597
|
+
// $FlowFixMe[incompatible-use]
|
|
598
|
+
this["add" + capitalize(type) + "Controls"]();
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
showHairlines: (Coord) => void = (point) => {
|
|
602
|
+
if (this.props.apiOptions.isMobile && this.props.markings !== "none") {
|
|
603
|
+
// Hairlines are already initialized when the graph is loaded, so
|
|
604
|
+
// here we just move them to the updated location and make them
|
|
605
|
+
// visible.
|
|
606
|
+
this.horizHairline?.moveTo(
|
|
607
|
+
[this.props.range[0][0], point[1]],
|
|
608
|
+
[this.props.range[0][1], point[1]],
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
this.horizHairline?.show();
|
|
612
|
+
|
|
613
|
+
this.vertHairline?.moveTo(
|
|
614
|
+
[point[0], this.props.range[1][0]],
|
|
615
|
+
[point[0], this.props.range[1][1]],
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
this.vertHairline?.show();
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
hideHairlines: () => void = () => {
|
|
623
|
+
if (this.props.apiOptions.isMobile) {
|
|
624
|
+
this.horizHairline?.hide();
|
|
625
|
+
this.vertHairline?.hide();
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
setTrashCanVisibility: (number) => void = (opacity) => {
|
|
630
|
+
const graphie = this.graphie;
|
|
631
|
+
|
|
632
|
+
if (knumber.equal(opacity, 0)) {
|
|
633
|
+
if (this.trashCan) {
|
|
634
|
+
this.trashCan.remove();
|
|
635
|
+
this.trashCan = null;
|
|
636
|
+
}
|
|
637
|
+
} else if (!this.props.apiOptions.isMobile) {
|
|
638
|
+
// Only if trash tooltips are not being used, we initialize the old
|
|
639
|
+
// trash can area.
|
|
640
|
+
if (!this.trashCan) {
|
|
641
|
+
this.trashCan = graphie?.raphael?.image(
|
|
642
|
+
TRASH_ICON_URI,
|
|
643
|
+
graphie.xpixels - 40,
|
|
644
|
+
graphie.ypixels - 40,
|
|
645
|
+
40,
|
|
646
|
+
40,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
this.trashCan?.attr({
|
|
650
|
+
opacity: opacity,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
isClickToAddPoints: (?Props) => boolean = (props) => {
|
|
656
|
+
props = props || this.props;
|
|
657
|
+
return _isClickToAddPoints(props);
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
areAngleGraphsEqual(
|
|
661
|
+
prevGraph: PerseusGraphTypeAngle,
|
|
662
|
+
currentGraph: PerseusGraphTypeAngle,
|
|
663
|
+
): boolean {
|
|
664
|
+
return (
|
|
665
|
+
prevGraph.allowReflexAngles !== currentGraph.allowReflexAngles ||
|
|
666
|
+
prevGraph.angleOffsetDeg !== currentGraph.angleOffsetDeg ||
|
|
667
|
+
prevGraph.snapDegrees !== currentGraph.snapDegrees
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
arePointGraphsEqual(
|
|
672
|
+
prevGraph: PerseusGraphTypePoint,
|
|
673
|
+
currentGraph: PerseusGraphTypePoint,
|
|
674
|
+
): boolean {
|
|
675
|
+
return prevGraph.numPoints !== currentGraph.numPoints;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
arePolygonGraphsEqual(
|
|
679
|
+
prevGraph: PerseusGraphTypePolygon,
|
|
680
|
+
currentGraph: PerseusGraphTypePolygon,
|
|
681
|
+
): boolean {
|
|
682
|
+
return (
|
|
683
|
+
prevGraph.numSides !== currentGraph.numSides ||
|
|
684
|
+
prevGraph.showAngles !== currentGraph.showAngles ||
|
|
685
|
+
prevGraph.showSides !== currentGraph.showSides ||
|
|
686
|
+
prevGraph.snapTo !== currentGraph.snapTo
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
areSegmentGraphsEqual(
|
|
691
|
+
prevGraph: PerseusGraphTypeSegment,
|
|
692
|
+
currentGraph: PerseusGraphTypeSegment,
|
|
693
|
+
): boolean {
|
|
694
|
+
return prevGraph.numSegments !== currentGraph.numSegments;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
_lineStroke: () => {|"stroke-width"?: number|} = () => {
|
|
698
|
+
// This should probably use: this.props.apiOptions.isMobile
|
|
699
|
+
// $FlowFixMe[prop-missing]
|
|
700
|
+
// $FlowFixMe[incompatible-exact]
|
|
701
|
+
return this.props.isMobile ? {"stroke-width": 3} : {};
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
addLine: (string) => void = (type) => {
|
|
705
|
+
const self = this;
|
|
706
|
+
const graphie = self.graphie;
|
|
707
|
+
const coords = InteractiveGraph.getLineCoords(
|
|
708
|
+
self.props.graph,
|
|
709
|
+
self.props,
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
const points = (self.points = _.map(coords, (coord) => {
|
|
713
|
+
return Interactive2.addMaybeMobileMovablePoint(this, {
|
|
714
|
+
coord: coord,
|
|
715
|
+
constraints: [
|
|
716
|
+
// $FlowFixMe[prop-missing]
|
|
717
|
+
Interactive2.MovablePoint.constraints.bound(),
|
|
718
|
+
// $FlowFixMe[prop-missing]
|
|
719
|
+
Interactive2.MovablePoint.constraints.snap(),
|
|
720
|
+
],
|
|
721
|
+
onMove: () => {
|
|
722
|
+
const graph = _.extend({}, self.props.graph, {
|
|
723
|
+
coords: _.invoke(points, "coord"),
|
|
724
|
+
});
|
|
725
|
+
self.onChange({graph: graph});
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
}));
|
|
729
|
+
|
|
730
|
+
const lineConfig = {
|
|
731
|
+
points: points,
|
|
732
|
+
static: true,
|
|
733
|
+
normalStyle: {
|
|
734
|
+
stroke: this.props.apiOptions.isMobile
|
|
735
|
+
? KhanColors.BLUE_C
|
|
736
|
+
: KhanColors.INTERACTIVE,
|
|
737
|
+
...this._lineStroke(),
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
if (type === "line") {
|
|
742
|
+
// $FlowFixMe[prop-missing]
|
|
743
|
+
lineConfig.extendLine = true;
|
|
744
|
+
} else if (type === "ray") {
|
|
745
|
+
// $FlowFixMe[prop-missing]
|
|
746
|
+
lineConfig.extendRay = true;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
self.line = Interactive2.addMovableLine(graphie, lineConfig);
|
|
750
|
+
|
|
751
|
+
// A and B can't be in the same place
|
|
752
|
+
points[0].listen("constraints", "isLine", (coord) => {
|
|
753
|
+
return !kpoint.equal(coord, points[1].coord());
|
|
754
|
+
});
|
|
755
|
+
points[1].listen("constraints", "isLine", (coord) => {
|
|
756
|
+
return !kpoint.equal(coord, points[0].coord());
|
|
757
|
+
});
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
removeLine: () => void = () => {
|
|
761
|
+
_.invoke(this.points, "remove");
|
|
762
|
+
this.line?.remove();
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
addLinearControls: () => void = () => {
|
|
766
|
+
this.addLine("line");
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
removeLinearControls: () => void = () => {
|
|
770
|
+
this.removeLine();
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
addQuadraticControls: () => void = () => {
|
|
774
|
+
if (this.props.graph.type !== "quadratic") {
|
|
775
|
+
throw makeInvalidTypeError("addQuadraticControls", "quadratic");
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
let coords = this.props.graph.coords;
|
|
779
|
+
if (!coords) {
|
|
780
|
+
coords = InteractiveGraph.defaultQuadraticCoords(this.props);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const onMoveHandler = () => {
|
|
784
|
+
const graph = _.extend({}, this.props.graph, {
|
|
785
|
+
// $FlowFixMe[incompatible-use]
|
|
786
|
+
coords: [pointA.coord(), pointB.coord(), pointC.coord()],
|
|
787
|
+
});
|
|
788
|
+
this.onChange({graph});
|
|
789
|
+
this.updateQuadratic();
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// NOTE(jeresig): This code attempts to access the variable while it's
|
|
793
|
+
// still being defined, it should be refactored! The callbacks appear
|
|
794
|
+
// to be executed synchronously, which causes this issue.
|
|
795
|
+
let pointA = null;
|
|
796
|
+
pointA = this.pointA = Interactive2.addMaybeMobileMovablePoint(this, {
|
|
797
|
+
coord: coords[0],
|
|
798
|
+
constraints: [
|
|
799
|
+
// $FlowFixMe[prop-missing]
|
|
800
|
+
Interactive2.MovablePoint.constraints.bound(),
|
|
801
|
+
// $FlowFixMe[prop-missing]
|
|
802
|
+
Interactive2.MovablePoint.constraints.snap(),
|
|
803
|
+
(coord) => {
|
|
804
|
+
return (
|
|
805
|
+
!pointA ||
|
|
806
|
+
// $FlowFixMe[incompatible-use]
|
|
807
|
+
(coord[0] !== pointB.coord()[0] &&
|
|
808
|
+
// $FlowFixMe[incompatible-use]
|
|
809
|
+
coord[0] !== pointC.coord()[0])
|
|
810
|
+
);
|
|
811
|
+
},
|
|
812
|
+
],
|
|
813
|
+
onMove: onMoveHandler,
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// NOTE(jeresig): This code attempts to access the variable while it's
|
|
817
|
+
// still being defined, it should be refactored! The callbacks appear
|
|
818
|
+
// to be executed synchronously, which causes this issue.
|
|
819
|
+
let pointB = null;
|
|
820
|
+
pointB = this.pointB = Interactive2.addMaybeMobileMovablePoint(this, {
|
|
821
|
+
coord: coords[1],
|
|
822
|
+
constraints: [
|
|
823
|
+
// $FlowFixMe[prop-missing]
|
|
824
|
+
Interactive2.MovablePoint.constraints.bound(),
|
|
825
|
+
// $FlowFixMe[prop-missing]
|
|
826
|
+
Interactive2.MovablePoint.constraints.snap(),
|
|
827
|
+
(coord) => {
|
|
828
|
+
return (
|
|
829
|
+
!pointB ||
|
|
830
|
+
// $FlowFixMe[incompatible-use]
|
|
831
|
+
(coord[0] !== pointA.coord()[0] &&
|
|
832
|
+
// $FlowFixMe[incompatible-use]
|
|
833
|
+
coord[0] !== pointC.coord()[0])
|
|
834
|
+
);
|
|
835
|
+
},
|
|
836
|
+
],
|
|
837
|
+
onMove: onMoveHandler,
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
// NOTE(jeresig): This code attempts to access the variable while it's
|
|
841
|
+
// still being defined, it should be refactored! The callbacks appear
|
|
842
|
+
// to be executed synchronously, which causes this issue.
|
|
843
|
+
let pointC = null;
|
|
844
|
+
pointC = this.pointC = Interactive2.addMaybeMobileMovablePoint(this, {
|
|
845
|
+
coord: coords[2],
|
|
846
|
+
constraints: [
|
|
847
|
+
// $FlowFixMe[prop-missing]
|
|
848
|
+
Interactive2.MovablePoint.constraints.bound(),
|
|
849
|
+
// $FlowFixMe[prop-missing]
|
|
850
|
+
Interactive2.MovablePoint.constraints.snap(),
|
|
851
|
+
(coord) => {
|
|
852
|
+
return (
|
|
853
|
+
!pointC ||
|
|
854
|
+
// $FlowFixMe[incompatible-use]
|
|
855
|
+
(coord[0] !== pointA.coord()[0] &&
|
|
856
|
+
// $FlowFixMe[incompatible-use]
|
|
857
|
+
coord[0] !== pointB.coord()[0])
|
|
858
|
+
);
|
|
859
|
+
},
|
|
860
|
+
],
|
|
861
|
+
onMove: onMoveHandler,
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
this.updateQuadratic();
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
updateQuadratic: () => void = () => {
|
|
868
|
+
const coeffs = InteractiveGraph.getCurrentQuadraticCoefficients(
|
|
869
|
+
this.props,
|
|
870
|
+
);
|
|
871
|
+
if (!coeffs) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Extract coefficients the parabola
|
|
876
|
+
const a = coeffs[0];
|
|
877
|
+
const b = coeffs[1];
|
|
878
|
+
const c = coeffs[2];
|
|
879
|
+
|
|
880
|
+
// Plot and style
|
|
881
|
+
if (this.parabola) {
|
|
882
|
+
const path = this.graphie?.svgParabolaPath(a, b, c);
|
|
883
|
+
this.parabola?.attr({path: path});
|
|
884
|
+
} else {
|
|
885
|
+
this.parabola = this.graphie?.parabola(a, b, c);
|
|
886
|
+
this.parabola?.attr({
|
|
887
|
+
stroke: this.props.apiOptions.isMobile
|
|
888
|
+
? KhanColors.BLUE_C
|
|
889
|
+
: KhanColors.DYNAMIC,
|
|
890
|
+
...this._lineStroke(),
|
|
891
|
+
});
|
|
892
|
+
this.parabola?.toBack();
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
removeQuadraticControls: () => void = () => {
|
|
897
|
+
this.pointA?.remove();
|
|
898
|
+
this.pointB?.remove();
|
|
899
|
+
this.pointC?.remove();
|
|
900
|
+
if (this.parabola) {
|
|
901
|
+
this.parabola.remove();
|
|
902
|
+
this.parabola = null;
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
addSinusoidControls: () => void = () => {
|
|
907
|
+
if (this.props.graph.type !== "sinusoid") {
|
|
908
|
+
throw makeInvalidTypeError("addSinusoidControls", "sinusoid");
|
|
909
|
+
}
|
|
910
|
+
let coords = this.props.graph.coords;
|
|
911
|
+
if (!coords) {
|
|
912
|
+
coords = InteractiveGraph.defaultSinusoidCoords(this.props);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const onMoveHandler = () => {
|
|
916
|
+
const graph = _.extend({}, this.props.graph, {
|
|
917
|
+
// $FlowFixMe[incompatible-use]
|
|
918
|
+
coords: [pointA.coord(), pointB.coord()],
|
|
919
|
+
});
|
|
920
|
+
this.onChange({graph: graph});
|
|
921
|
+
this.updateSinusoid();
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
// NOTE(jeresig): This code attempts to access the variable while it's
|
|
925
|
+
// still being defined, it should be refactored! The callbacks appear
|
|
926
|
+
// to be executed synchronously, which causes this issue.
|
|
927
|
+
let pointA = null;
|
|
928
|
+
pointA = this.pointA = Interactive2.addMaybeMobileMovablePoint(this, {
|
|
929
|
+
coord: coords[0],
|
|
930
|
+
constraints: [
|
|
931
|
+
// $FlowFixMe[prop-missing]
|
|
932
|
+
Interactive2.MovablePoint.constraints.bound(),
|
|
933
|
+
// $FlowFixMe[prop-missing]
|
|
934
|
+
Interactive2.MovablePoint.constraints.snap(),
|
|
935
|
+
(coord) => {
|
|
936
|
+
return !pointA || coord[0] !== pointB.coord()[0];
|
|
937
|
+
},
|
|
938
|
+
],
|
|
939
|
+
onMove: onMoveHandler,
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
const pointB = (this.pointB = Interactive2.addMaybeMobileMovablePoint(
|
|
943
|
+
this,
|
|
944
|
+
{
|
|
945
|
+
coord: coords[1],
|
|
946
|
+
constraints: [
|
|
947
|
+
// $FlowFixMe[prop-missing]
|
|
948
|
+
Interactive2.MovablePoint.constraints.bound(),
|
|
949
|
+
// $FlowFixMe[prop-missing]
|
|
950
|
+
Interactive2.MovablePoint.constraints.snap(),
|
|
951
|
+
(coord) => {
|
|
952
|
+
return !pointA || coord[0] !== pointA.coord()[0];
|
|
953
|
+
},
|
|
954
|
+
],
|
|
955
|
+
onMove: onMoveHandler,
|
|
956
|
+
},
|
|
957
|
+
));
|
|
958
|
+
|
|
959
|
+
this.updateSinusoid();
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
updateSinusoid: () => void = () => {
|
|
963
|
+
const coeffs = InteractiveGraph.getCurrentSinusoidCoefficients(
|
|
964
|
+
this.props,
|
|
965
|
+
);
|
|
966
|
+
if (!coeffs) {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const a = coeffs[0];
|
|
971
|
+
const b = coeffs[1];
|
|
972
|
+
const c = coeffs[2];
|
|
973
|
+
const d = coeffs[3];
|
|
974
|
+
|
|
975
|
+
// Plot and style
|
|
976
|
+
if (this.sinusoid) {
|
|
977
|
+
const path = this.graphie?.svgSinusoidPath(a, b, c, d);
|
|
978
|
+
this.sinusoid?.attr({path: path});
|
|
979
|
+
} else {
|
|
980
|
+
this.sinusoid = this.graphie?.sinusoid(a, b, c, d);
|
|
981
|
+
this.sinusoid?.attr({
|
|
982
|
+
stroke: this.props.apiOptions.isMobile
|
|
983
|
+
? KhanColors.BLUE_C
|
|
984
|
+
: KhanColors.DYNAMIC,
|
|
985
|
+
...this._lineStroke(),
|
|
986
|
+
});
|
|
987
|
+
this.sinusoid?.toBack();
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
removeSinusoidControls: () => void = () => {
|
|
992
|
+
this.pointA?.remove();
|
|
993
|
+
this.pointB?.remove();
|
|
994
|
+
if (this.sinusoid) {
|
|
995
|
+
this.sinusoid.remove();
|
|
996
|
+
this.sinusoid = null;
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
|
|
1000
|
+
addCircleControls: () => void = () => {
|
|
1001
|
+
const graphie = this.graphie;
|
|
1002
|
+
// $FlowFixMe[incompatible-use]
|
|
1003
|
+
const minSnap = _.min(graphie.snap);
|
|
1004
|
+
|
|
1005
|
+
const circle = (this.circle = graphie?.addCircleGraph({
|
|
1006
|
+
center: this.props.graph.center || [0, 0],
|
|
1007
|
+
radius: this.props.graph.radius || _.min(this.props.step),
|
|
1008
|
+
snapX: graphie.snap[0],
|
|
1009
|
+
snapY: graphie.snap[1],
|
|
1010
|
+
minRadius: minSnap,
|
|
1011
|
+
snapRadius: minSnap,
|
|
1012
|
+
}));
|
|
1013
|
+
|
|
1014
|
+
$(circle).on("move", () => {
|
|
1015
|
+
const graph = _.extend({}, this.props.graph, {
|
|
1016
|
+
// $FlowFixMe[incompatible-use]
|
|
1017
|
+
center: circle.center,
|
|
1018
|
+
// $FlowFixMe[incompatible-use]
|
|
1019
|
+
radius: circle.radius,
|
|
1020
|
+
});
|
|
1021
|
+
this.onChange({graph: graph});
|
|
1022
|
+
});
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
removeCircleControls: () => void = () => {
|
|
1026
|
+
this.circle?.remove();
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
addLinearSystemControls: () => void = () => {
|
|
1030
|
+
const graphie = this.graphie;
|
|
1031
|
+
const coords = InteractiveGraph.getLinearSystemCoords(
|
|
1032
|
+
this.props.graph,
|
|
1033
|
+
this.props,
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
const segmentColors = [KhanColors.INTERACTIVE, KhanColors.GREEN];
|
|
1037
|
+
const points = (this.points = _.map(
|
|
1038
|
+
coords,
|
|
1039
|
+
(segmentCoords, segmentIndex) => {
|
|
1040
|
+
// NOTE(jeresig): This code attempts to access the variable
|
|
1041
|
+
// while it's still being defined, it should be refactored!
|
|
1042
|
+
// The callbacks appear to be executed synchronously, which
|
|
1043
|
+
// causes this issue.
|
|
1044
|
+
const segmentPoints = [];
|
|
1045
|
+
|
|
1046
|
+
for (let i = 0; i < segmentCoords.length; i += 1) {
|
|
1047
|
+
const coord = segmentCoords[i];
|
|
1048
|
+
segmentPoints.push(
|
|
1049
|
+
Interactive2.addMaybeMobileMovablePoint(this, {
|
|
1050
|
+
coord: coord,
|
|
1051
|
+
constraints: [
|
|
1052
|
+
// $FlowFixMe[prop-missing]
|
|
1053
|
+
Interactive2.MovablePoint.constraints.bound(),
|
|
1054
|
+
// $FlowFixMe[prop-missing]
|
|
1055
|
+
Interactive2.MovablePoint.constraints.snap(),
|
|
1056
|
+
(coord) => {
|
|
1057
|
+
const otherSegment = segmentPoints[1 - i];
|
|
1058
|
+
if (!otherSegment) {
|
|
1059
|
+
// segment hasn't been defined yet
|
|
1060
|
+
// because we're still creating them
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
return !kpoint.equal(
|
|
1064
|
+
coord,
|
|
1065
|
+
otherSegment.coord(),
|
|
1066
|
+
);
|
|
1067
|
+
},
|
|
1068
|
+
],
|
|
1069
|
+
onMove: () => {
|
|
1070
|
+
const graph = _.extend({}, this.props.graph, {
|
|
1071
|
+
coords: _.map(this.points, (segment) =>
|
|
1072
|
+
_.invoke(segment, "coord"),
|
|
1073
|
+
),
|
|
1074
|
+
});
|
|
1075
|
+
this.onChange({graph: graph});
|
|
1076
|
+
},
|
|
1077
|
+
normalStyle: {
|
|
1078
|
+
fill: segmentColors[segmentIndex],
|
|
1079
|
+
},
|
|
1080
|
+
highlightStyle: {
|
|
1081
|
+
fill: segmentColors[segmentIndex],
|
|
1082
|
+
},
|
|
1083
|
+
}),
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return segmentPoints;
|
|
1088
|
+
},
|
|
1089
|
+
));
|
|
1090
|
+
|
|
1091
|
+
this.lines = _.map(points, (segmentPoints, segmentIndex) => {
|
|
1092
|
+
return Interactive2.addMovableLine(graphie, {
|
|
1093
|
+
points: segmentPoints,
|
|
1094
|
+
static: true,
|
|
1095
|
+
extendLine: true,
|
|
1096
|
+
normalStyle: {
|
|
1097
|
+
stroke: segmentColors[segmentIndex],
|
|
1098
|
+
},
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
removeLinearSystemControls: () => void = () => {
|
|
1104
|
+
_.invoke(this.lines, "remove");
|
|
1105
|
+
_.map(this.points, (segment) => _.invoke(segment, "remove"));
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
isCoordInTrash: (Coord) => boolean = (coord) => {
|
|
1109
|
+
if (this.props.apiOptions.isMobile) {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const graphie = this.graphie;
|
|
1114
|
+
const screenPoint = graphie?.scalePoint(coord);
|
|
1115
|
+
return (
|
|
1116
|
+
// $FlowFixMe[incompatible-use]
|
|
1117
|
+
screenPoint[0] >= graphie.xpixels - 40 &&
|
|
1118
|
+
// $FlowFixMe[incompatible-use]
|
|
1119
|
+
screenPoint[1] >= graphie.ypixels - 40
|
|
1120
|
+
);
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
createPointForPointsType: (Coord, number) => $FlowFixMe = (coord, i) => {
|
|
1124
|
+
const self = this;
|
|
1125
|
+
|
|
1126
|
+
const remove = () => {
|
|
1127
|
+
self.points = _.filter(self.points, function (pt) {
|
|
1128
|
+
return pt !== point;
|
|
1129
|
+
});
|
|
1130
|
+
// update the correct answer box
|
|
1131
|
+
self.updateCoordsFromPoints();
|
|
1132
|
+
|
|
1133
|
+
// remove this movablePoint from graphie.
|
|
1134
|
+
// we wait to do this until we're not inside of
|
|
1135
|
+
// said point's onMoveEnd method so its state is
|
|
1136
|
+
// consistent throughout this method call
|
|
1137
|
+
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
|
|
1138
|
+
// $FlowFixMe[incompatible-use]
|
|
1139
|
+
setTimeout(point.remove.bind(point), 0); // eslint-disable-line no-restricted-syntax
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
// NOTE(jeresig): This code attempts to access the variable while it's
|
|
1143
|
+
// still being defined, it should be refactored! The callbacks appear
|
|
1144
|
+
// to be executed synchronously, which causes this issue.
|
|
1145
|
+
let point = null;
|
|
1146
|
+
point = Interactive2.addMaybeMobileMovablePoint(this, {
|
|
1147
|
+
coord: coord,
|
|
1148
|
+
constraints: [
|
|
1149
|
+
// $FlowFixMe[prop-missing]
|
|
1150
|
+
Interactive2.MovablePoint.constraints.bound(),
|
|
1151
|
+
// $FlowFixMe[prop-missing]
|
|
1152
|
+
Interactive2.MovablePoint.constraints.snap(),
|
|
1153
|
+
function (coord) {
|
|
1154
|
+
// TODO(jack): There ought to be a
|
|
1155
|
+
// MovablePoint.constraints.avoid
|
|
1156
|
+
// default that lets you do things like this
|
|
1157
|
+
return _.all(self.points, function (pt) {
|
|
1158
|
+
return point === pt || !kpoint.equal(coord, pt.coord());
|
|
1159
|
+
});
|
|
1160
|
+
},
|
|
1161
|
+
],
|
|
1162
|
+
onMoveStart: function () {
|
|
1163
|
+
if (self.isClickToAddPoints()) {
|
|
1164
|
+
self.setTrashCanVisibility(1);
|
|
1165
|
+
}
|
|
1166
|
+
},
|
|
1167
|
+
onMove: self.updateCoordsFromPoints,
|
|
1168
|
+
onMoveEnd: function (coord) {
|
|
1169
|
+
if (self.isClickToAddPoints()) {
|
|
1170
|
+
if (self.isCoordInTrash(coord)) {
|
|
1171
|
+
remove();
|
|
1172
|
+
}
|
|
1173
|
+
// In case we mouseup'd off the graphie and that
|
|
1174
|
+
// stopped the move (in which case, we might not
|
|
1175
|
+
// be in isCoordInTrash()
|
|
1176
|
+
self.setTrashCanVisibility(0.5);
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
...(this.props.apiOptions.isMobile && self.isClickToAddPoints()
|
|
1180
|
+
? {onRemove: remove}
|
|
1181
|
+
: {}),
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
return point;
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
removePoint: (Coord) => ?number = (point) => {
|
|
1188
|
+
let index = null;
|
|
1189
|
+
this.points = _.filter(this.points, function (pt, i) {
|
|
1190
|
+
if (pt === point) {
|
|
1191
|
+
index = i;
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
return true;
|
|
1195
|
+
});
|
|
1196
|
+
return index;
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
createPointForPolygonType: (Coord, number) => $FlowFixMe = (coord, i) => {
|
|
1200
|
+
if (this.props.graph.type !== "polygon") {
|
|
1201
|
+
throw makeInvalidTypeError("createPointForPolygonType", "polygon");
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// TODO(alex): check against "grid" instead, use constants
|
|
1205
|
+
const snapToGrid = !_.contains(
|
|
1206
|
+
["angles", "sides"],
|
|
1207
|
+
this.props.graph.snapTo,
|
|
1208
|
+
);
|
|
1209
|
+
|
|
1210
|
+
// Index relative to current point -> absolute index
|
|
1211
|
+
// NOTE: This does not work when isClickToAddPoints() == true,
|
|
1212
|
+
// as `i` can be changed by dragging a point to the trash
|
|
1213
|
+
// Currently this function is only called when !isClickToAddPoints()
|
|
1214
|
+
const rel = (j) => {
|
|
1215
|
+
return (i + j + this.points.length) % this.points.length;
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
const remove = () => {
|
|
1219
|
+
// remove this point from points
|
|
1220
|
+
const index = this.removePoint(point);
|
|
1221
|
+
if (this.polygon?.closed()) {
|
|
1222
|
+
// We should be checking if this.points is defined before rotating them.
|
|
1223
|
+
// $FlowFixMe[incompatible-call]
|
|
1224
|
+
// $FlowFixMe[incompatible-type]
|
|
1225
|
+
this.points = rotate(this.points, index);
|
|
1226
|
+
this.polygon?.update({closed: false});
|
|
1227
|
+
}
|
|
1228
|
+
this.updatePolygon();
|
|
1229
|
+
// the polygon is now unclosed, so we need to
|
|
1230
|
+
// remove any points props
|
|
1231
|
+
this.clearCoords();
|
|
1232
|
+
|
|
1233
|
+
// remove this movablePoint from graphie.
|
|
1234
|
+
// wait to do this until we're not inside of
|
|
1235
|
+
// said point's onMoveEnd method so state is
|
|
1236
|
+
// consistent throughout the method call
|
|
1237
|
+
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
|
|
1238
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1239
|
+
setTimeout(point.remove.bind(point), 0);
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
const setInitialMove = debounce((point, bool) => {
|
|
1243
|
+
point.state.isInitialMove = bool;
|
|
1244
|
+
}, 1000);
|
|
1245
|
+
|
|
1246
|
+
const onMoveEndHandler = (coord) => {
|
|
1247
|
+
// If the user clicked and dragged a point over endpoint,
|
|
1248
|
+
// join the them
|
|
1249
|
+
const overlappedPoints =
|
|
1250
|
+
this.points.length > 1 &&
|
|
1251
|
+
((point === this.points[0] &&
|
|
1252
|
+
kpoint.equal(coord, _.last(this.points).coord())) ||
|
|
1253
|
+
(point === _.last(this.points) &&
|
|
1254
|
+
kpoint.equal(coord, this.points[0].coord())));
|
|
1255
|
+
|
|
1256
|
+
if (this.isClickToAddPoints()) {
|
|
1257
|
+
if (this.isCoordInTrash(coord)) {
|
|
1258
|
+
remove();
|
|
1259
|
+
} else if (overlappedPoints) {
|
|
1260
|
+
const pointToRemove = this.points.pop();
|
|
1261
|
+
|
|
1262
|
+
if (this.points.length > 2) {
|
|
1263
|
+
this.polygon?.update({closed: true});
|
|
1264
|
+
this.updateCoordsFromPoints();
|
|
1265
|
+
} else {
|
|
1266
|
+
this.polygon?.update({closed: false});
|
|
1267
|
+
this.clearCoords();
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
this.updatePolygon();
|
|
1271
|
+
// remove this movablePoint from graphie.
|
|
1272
|
+
// wait to do this until we're not inside of
|
|
1273
|
+
// said point's onMoveEnd method so state is
|
|
1274
|
+
// consistent throughout the method call
|
|
1275
|
+
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
|
|
1276
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1277
|
+
setTimeout(pointToRemove.remove.bind(pointToRemove), 0);
|
|
1278
|
+
} else {
|
|
1279
|
+
// If the user clicked and dragged a point over any other
|
|
1280
|
+
// existing point, fix shape
|
|
1281
|
+
const shouldRemove = _.any(this.points, function (pt) {
|
|
1282
|
+
return pt !== point && kpoint.equal(pt.coord(), coord);
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
if (shouldRemove) {
|
|
1286
|
+
this.removePoint(point);
|
|
1287
|
+
|
|
1288
|
+
if (this.points.length < 3) {
|
|
1289
|
+
this.polygon?.update({closed: false});
|
|
1290
|
+
this.clearCoords();
|
|
1291
|
+
} else if (this.polygon?.closed()) {
|
|
1292
|
+
this.updateCoordsFromPoints();
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
this.updatePolygon();
|
|
1296
|
+
// remove this movablePoint from graphie.
|
|
1297
|
+
// wait to do this until we're not inside
|
|
1298
|
+
// said point's onMoveEnd method so state
|
|
1299
|
+
// is consistent throughout the method call
|
|
1300
|
+
// TODO(jeff, CP-3128): Use Wonder Blocks Timing API.
|
|
1301
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
1302
|
+
setTimeout(point.remove.bind(point), 0);
|
|
1303
|
+
} else {
|
|
1304
|
+
// If this was
|
|
1305
|
+
// * a click on the first or last point
|
|
1306
|
+
// * and not a deletion
|
|
1307
|
+
// * and not a drag,
|
|
1308
|
+
// * and not a creation of a new point
|
|
1309
|
+
// (see !point.state.isInitialMove, below),
|
|
1310
|
+
// * and our polygon is not closed,
|
|
1311
|
+
// * and we can close it (we need at least the same
|
|
1312
|
+
// number of points as coordinates asked for in the
|
|
1313
|
+
// question),
|
|
1314
|
+
// then close it
|
|
1315
|
+
const isFirstOrLastPoint =
|
|
1316
|
+
point === _.head(this.points) ||
|
|
1317
|
+
point === _.last(this.points);
|
|
1318
|
+
const pointNotMoved = !point.hasMoved();
|
|
1319
|
+
// NOTE (jsatk): Currently different on mobile versus
|
|
1320
|
+
// destkop
|
|
1321
|
+
const notANewPoint = !point.state.isInitialMove;
|
|
1322
|
+
const polygonNotClosed = !this.polygon?.closed();
|
|
1323
|
+
const allPointsPlotted = this.points.length > 2;
|
|
1324
|
+
|
|
1325
|
+
const canCloseUnclosedPolygon =
|
|
1326
|
+
isFirstOrLastPoint &&
|
|
1327
|
+
pointNotMoved &&
|
|
1328
|
+
notANewPoint &&
|
|
1329
|
+
polygonNotClosed &&
|
|
1330
|
+
allPointsPlotted;
|
|
1331
|
+
|
|
1332
|
+
if (canCloseUnclosedPolygon) {
|
|
1333
|
+
this.polygon?.update({closed: true});
|
|
1334
|
+
this.updatePolygon();
|
|
1335
|
+
|
|
1336
|
+
// We finally have a closed polygon, so save our
|
|
1337
|
+
// points to props
|
|
1338
|
+
this.updateCoordsFromPoints();
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// In case we mouseup'd off the graphie and that
|
|
1344
|
+
// stopped the move
|
|
1345
|
+
this.setTrashCanVisibility(0.5);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
setInitialMove(point, false);
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
const graphConstraint = (coord) => {
|
|
1352
|
+
// These constraints are all relative to the other points, so if
|
|
1353
|
+
// we're creating the initial points and haven't added any others
|
|
1354
|
+
// to the graph, we can't enforce them.
|
|
1355
|
+
if (this.points == null || this.points.length === 0) {
|
|
1356
|
+
return true;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// eslint-disable-next-line flowtype/no-mutable-array
|
|
1360
|
+
const coords: Array<Coord> = _.invoke(this.points, "coord");
|
|
1361
|
+
coords[i] = coord;
|
|
1362
|
+
|
|
1363
|
+
// Check for invalid positioning, but only if we aren't adding
|
|
1364
|
+
// points one click at a time, since those added points could
|
|
1365
|
+
// have already violated these constraints
|
|
1366
|
+
if (!this.isClickToAddPoints()) {
|
|
1367
|
+
// Polygons can't have consecutive collinear points
|
|
1368
|
+
if (
|
|
1369
|
+
collinear(coords[rel(-2)], coords[rel(-1)], coords[i]) ||
|
|
1370
|
+
collinear(coords[rel(-1)], coords[i], coords[rel(1)]) ||
|
|
1371
|
+
collinear(coords[i], coords[rel(1)], coords[rel(2)])
|
|
1372
|
+
) {
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const segments = _.zip(coords, rotate(coords));
|
|
1377
|
+
|
|
1378
|
+
if (this.points.length > 3) {
|
|
1379
|
+
// Constrain to simple (non self-intersecting) polygon by
|
|
1380
|
+
// testing whether adjacent segments intersect any others
|
|
1381
|
+
for (let j = -1; j <= 0; j++) {
|
|
1382
|
+
const segment = segments[rel(j)];
|
|
1383
|
+
const others = _.without(
|
|
1384
|
+
segments,
|
|
1385
|
+
segment,
|
|
1386
|
+
segments[rel(j - 1)],
|
|
1387
|
+
segments[rel(j + 1)],
|
|
1388
|
+
);
|
|
1389
|
+
|
|
1390
|
+
for (let k = 0; k < others.length; k++) {
|
|
1391
|
+
const other = others[k];
|
|
1392
|
+
if (intersects(segment, other)) {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
if (
|
|
1401
|
+
this.props.graph.snapTo === "angles" &&
|
|
1402
|
+
this.points.length > 2
|
|
1403
|
+
) {
|
|
1404
|
+
// Snap to whole degree interior angles
|
|
1405
|
+
|
|
1406
|
+
const angles = _.map(angleMeasures(coords), function (rad) {
|
|
1407
|
+
return (rad * 180) / Math.PI;
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
_.each([-1, 1], function (j) {
|
|
1411
|
+
angles[rel(j)] = Math.round(angles[rel(j)]);
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
const getAngle = function (a, vertex, b) {
|
|
1415
|
+
const angle = GraphUtils.findAngle(
|
|
1416
|
+
coords[rel(a)],
|
|
1417
|
+
coords[rel(b)],
|
|
1418
|
+
coords[rel(vertex)],
|
|
1419
|
+
);
|
|
1420
|
+
return (angle + 360) % 360;
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
const innerAngles = [
|
|
1424
|
+
angles[rel(-1)] - getAngle(-2, -1, 1),
|
|
1425
|
+
angles[rel(1)] - getAngle(-1, 1, 2),
|
|
1426
|
+
];
|
|
1427
|
+
innerAngles[2] = 180 - (innerAngles[0] + innerAngles[1]);
|
|
1428
|
+
|
|
1429
|
+
// Avoid degenerate triangles
|
|
1430
|
+
if (
|
|
1431
|
+
_.any(innerAngles, function (angle) {
|
|
1432
|
+
return leq(angle, 1);
|
|
1433
|
+
})
|
|
1434
|
+
) {
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
const knownSide = magnitude(
|
|
1439
|
+
vector(coords[rel(-1)], coords[rel(1)]),
|
|
1440
|
+
);
|
|
1441
|
+
|
|
1442
|
+
const onLeft =
|
|
1443
|
+
sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
|
|
1444
|
+
|
|
1445
|
+
// Solve for side by using the law of sines
|
|
1446
|
+
const side =
|
|
1447
|
+
(Math.sin((innerAngles[1] * Math.PI) / 180) /
|
|
1448
|
+
Math.sin((innerAngles[2] * Math.PI) / 180)) *
|
|
1449
|
+
knownSide;
|
|
1450
|
+
|
|
1451
|
+
const outerAngle = GraphUtils.findAngle(
|
|
1452
|
+
coords[rel(1)],
|
|
1453
|
+
coords[rel(-1)],
|
|
1454
|
+
);
|
|
1455
|
+
|
|
1456
|
+
const offset = this.graphie?.polar(
|
|
1457
|
+
side,
|
|
1458
|
+
outerAngle + (onLeft ? 1 : -1) * innerAngles[0],
|
|
1459
|
+
);
|
|
1460
|
+
|
|
1461
|
+
return this.graphie?.addPoints(coords[rel(-1)], offset);
|
|
1462
|
+
}
|
|
1463
|
+
if (this.props.graph.snapTo === "sides" && this.points.length > 1) {
|
|
1464
|
+
// Snap to whole unit side measures
|
|
1465
|
+
|
|
1466
|
+
const sides = _.map(
|
|
1467
|
+
[
|
|
1468
|
+
[coords[rel(-1)], coords[i]],
|
|
1469
|
+
[coords[i], coords[rel(1)]],
|
|
1470
|
+
[coords[rel(-1)], coords[rel(1)]],
|
|
1471
|
+
],
|
|
1472
|
+
function (coords) {
|
|
1473
|
+
return magnitude(vector(...coords));
|
|
1474
|
+
},
|
|
1475
|
+
);
|
|
1476
|
+
|
|
1477
|
+
_.each([0, 1], function (j) {
|
|
1478
|
+
sides[j] = Math.round(sides[j]);
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
// Avoid degenerate triangles
|
|
1482
|
+
if (
|
|
1483
|
+
leq(sides[1] + sides[2], sides[0]) ||
|
|
1484
|
+
leq(sides[0] + sides[2], sides[1]) ||
|
|
1485
|
+
leq(sides[0] + sides[1], sides[2])
|
|
1486
|
+
) {
|
|
1487
|
+
return false;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Solve for angle by using the law of cosines
|
|
1491
|
+
const innerAngle = lawOfCosines(sides[0], sides[2], sides[1]);
|
|
1492
|
+
|
|
1493
|
+
const outerAngle = GraphUtils.findAngle(
|
|
1494
|
+
coords[rel(1)],
|
|
1495
|
+
coords[rel(-1)],
|
|
1496
|
+
);
|
|
1497
|
+
|
|
1498
|
+
const onLeft =
|
|
1499
|
+
sign(ccw(coords[rel(-1)], coords[rel(1)], coords[i])) === 1;
|
|
1500
|
+
|
|
1501
|
+
const offset = this.graphie?.polar(
|
|
1502
|
+
sides[0],
|
|
1503
|
+
outerAngle + (onLeft ? 1 : -1) * innerAngle,
|
|
1504
|
+
);
|
|
1505
|
+
|
|
1506
|
+
return this.graphie?.addPoints(coords[rel(-1)], offset);
|
|
1507
|
+
}
|
|
1508
|
+
// Snap to grid (already done)
|
|
1509
|
+
return true;
|
|
1510
|
+
};
|
|
1511
|
+
|
|
1512
|
+
const point = Interactive2.addMaybeMobileMovablePoint(this, {
|
|
1513
|
+
coord: coord,
|
|
1514
|
+
constraints: [
|
|
1515
|
+
// $FlowFixMe[prop-missing]
|
|
1516
|
+
Interactive2.MovablePoint.constraints.bound(),
|
|
1517
|
+
snapToGrid
|
|
1518
|
+
? // $FlowFixMe[prop-missing]
|
|
1519
|
+
Interactive2.MovablePoint.constraints.snap()
|
|
1520
|
+
: null,
|
|
1521
|
+
graphConstraint,
|
|
1522
|
+
],
|
|
1523
|
+
onMoveStart: () => {
|
|
1524
|
+
if (this.isClickToAddPoints()) {
|
|
1525
|
+
this.setTrashCanVisibility(1);
|
|
1526
|
+
}
|
|
1527
|
+
},
|
|
1528
|
+
onMove: () => {
|
|
1529
|
+
if (this.polygon?.closed()) {
|
|
1530
|
+
this.updateCoordsFromPoints();
|
|
1531
|
+
}
|
|
1532
|
+
},
|
|
1533
|
+
onMoveEnd: onMoveEndHandler,
|
|
1534
|
+
...(this.props.apiOptions.isMobile && this.isClickToAddPoints()
|
|
1535
|
+
? {onRemove: remove}
|
|
1536
|
+
: {}),
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
point.state.isInitialMove = true;
|
|
1540
|
+
|
|
1541
|
+
return point;
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1544
|
+
updateCoordsFromPoints: () => void = () => {
|
|
1545
|
+
const graph = _.extend({}, this.props.graph, {
|
|
1546
|
+
// Handle old movable points with .coord, or
|
|
1547
|
+
// Interactive2.MovablePoint's with .coord()
|
|
1548
|
+
coords: _.map(this.points, function (point) {
|
|
1549
|
+
return _.result(point, "coord");
|
|
1550
|
+
}),
|
|
1551
|
+
});
|
|
1552
|
+
this.onChange({graph: graph});
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
clearCoords: () => void = () => {
|
|
1556
|
+
const graph = _.extend({}, this.props.graph, {
|
|
1557
|
+
coords: null,
|
|
1558
|
+
});
|
|
1559
|
+
this.onChange({graph: graph});
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
onChange: ($FlowFixMe) => void = (data) => {
|
|
1563
|
+
this.props.onChange(data);
|
|
1564
|
+
this.props.trackInteraction();
|
|
1565
|
+
};
|
|
1566
|
+
|
|
1567
|
+
addPointControls: () => void = () => {
|
|
1568
|
+
if (this.props.graph.type !== "point") {
|
|
1569
|
+
throw makeInvalidTypeError("addPointControls", "point");
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
const coords = InteractiveGraph.getPointCoords(
|
|
1573
|
+
this.props.graph,
|
|
1574
|
+
this.props,
|
|
1575
|
+
);
|
|
1576
|
+
// Clear out our old points so that newly added points don't
|
|
1577
|
+
// "collide" with them and reposition when being added
|
|
1578
|
+
// Without this, when added, each point checks whether it is on top
|
|
1579
|
+
// of a point in this.points, which (a) shouldn't matter since
|
|
1580
|
+
// we're clearing out this.points anyways, and (b) can cause problems
|
|
1581
|
+
// if each of this.points is a MovablePoint instead of an
|
|
1582
|
+
// Interactive2.MovablePoint, since one has a .coord and the other
|
|
1583
|
+
// has .coord()
|
|
1584
|
+
// TODO(jack): Figure out a better way to do this
|
|
1585
|
+
this.points = [];
|
|
1586
|
+
this.points = _.map(coords, this.createPointForPointsType, this);
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
removePointControls: () => void = () => {
|
|
1590
|
+
_.invoke(this.points, "remove");
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
addSegmentControls: () => void = () => {
|
|
1594
|
+
if (this.props.graph.type !== "segment") {
|
|
1595
|
+
throw makeInvalidTypeError("addSegmentControls", "segment");
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const self = this;
|
|
1599
|
+
const graphie = this.graphie;
|
|
1600
|
+
|
|
1601
|
+
const coords = InteractiveGraph.getSegmentCoords(
|
|
1602
|
+
this.props.graph,
|
|
1603
|
+
this.props,
|
|
1604
|
+
);
|
|
1605
|
+
|
|
1606
|
+
const createPoint = (options) =>
|
|
1607
|
+
Interactive2.addMaybeMobileMovablePoint(this, options);
|
|
1608
|
+
|
|
1609
|
+
this.points = [];
|
|
1610
|
+
this.lines = _.map(
|
|
1611
|
+
coords,
|
|
1612
|
+
function (segment, i) {
|
|
1613
|
+
const updateCoordProps = function () {
|
|
1614
|
+
const graph = _.extend({}, self.props.graph, {
|
|
1615
|
+
coords: _.invoke(self.lines, "coords"),
|
|
1616
|
+
});
|
|
1617
|
+
self.onChange({graph: graph});
|
|
1618
|
+
};
|
|
1619
|
+
|
|
1620
|
+
// NOTE(jeresig): This code attempts to access the variable
|
|
1621
|
+
// while it's still being defined, it should be refactored!
|
|
1622
|
+
// The callbacks appear to be executed synchronously, which
|
|
1623
|
+
// causes this issue.
|
|
1624
|
+
const points = [];
|
|
1625
|
+
|
|
1626
|
+
for (let i = 0; i < segment.length; i += 1) {
|
|
1627
|
+
const coord = segment[i];
|
|
1628
|
+
points.push(
|
|
1629
|
+
createPoint({
|
|
1630
|
+
coord: coord,
|
|
1631
|
+
constraints: [
|
|
1632
|
+
// $FlowFixMe[prop-missing]
|
|
1633
|
+
Interactive2.MovablePoint.constraints.bound(),
|
|
1634
|
+
// $FlowFixMe[prop-missing]
|
|
1635
|
+
Interactive2.MovablePoint.constraints.snap(),
|
|
1636
|
+
(coord) => {
|
|
1637
|
+
const otherPoint = points[1 - i];
|
|
1638
|
+
if (!otherPoint) {
|
|
1639
|
+
// point hasn't been defined yet
|
|
1640
|
+
// because we're still creating them
|
|
1641
|
+
// We return false as the points aren't
|
|
1642
|
+
// going to be in the same position at
|
|
1643
|
+
// this point.
|
|
1644
|
+
return false;
|
|
1645
|
+
}
|
|
1646
|
+
return !kpoint.equal(
|
|
1647
|
+
coord,
|
|
1648
|
+
otherPoint.coord(),
|
|
1649
|
+
);
|
|
1650
|
+
},
|
|
1651
|
+
],
|
|
1652
|
+
onMove: updateCoordProps,
|
|
1653
|
+
}),
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
self.points = self.points.concat(points);
|
|
1658
|
+
const line = Interactive2.addMovableLine(graphie, {
|
|
1659
|
+
points: points,
|
|
1660
|
+
static: false,
|
|
1661
|
+
constraints: [
|
|
1662
|
+
// $FlowFixMe[prop-missing]
|
|
1663
|
+
Interactive2.MovableLine.constraints.bound(),
|
|
1664
|
+
// $FlowFixMe[prop-missing]
|
|
1665
|
+
Interactive2.MovableLine.constraints.snap(),
|
|
1666
|
+
],
|
|
1667
|
+
onMove: [
|
|
1668
|
+
// $FlowFixMe[prop-missing]
|
|
1669
|
+
Interactive2.MovableLine.onMove.updatePoints,
|
|
1670
|
+
updateCoordProps,
|
|
1671
|
+
],
|
|
1672
|
+
normalStyle: {
|
|
1673
|
+
stroke: this.props.apiOptions.isMobile
|
|
1674
|
+
? KhanColors.BLUE_C
|
|
1675
|
+
: KhanColors.INTERACTIVE,
|
|
1676
|
+
...this._lineStroke(),
|
|
1677
|
+
},
|
|
1678
|
+
highlightStyle: {
|
|
1679
|
+
stroke: this.props.apiOptions.isMobile
|
|
1680
|
+
? KhanColors.BLUE_C
|
|
1681
|
+
: KhanColors.INTERACTING,
|
|
1682
|
+
...this._lineStroke(),
|
|
1683
|
+
},
|
|
1684
|
+
});
|
|
1685
|
+
_.invoke(points, "toFront");
|
|
1686
|
+
|
|
1687
|
+
return line;
|
|
1688
|
+
},
|
|
1689
|
+
this,
|
|
1690
|
+
);
|
|
1691
|
+
};
|
|
1692
|
+
|
|
1693
|
+
removeSegmentControls: () => void = () => {
|
|
1694
|
+
_.invoke(this.points, "remove");
|
|
1695
|
+
_.invoke(this.lines, "remove");
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1698
|
+
addRayControls: () => void = () => {
|
|
1699
|
+
this.addLine("ray");
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
removeRayControls: () => void = () => {
|
|
1703
|
+
this.removeLine();
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
addPolygonControls: () => void = () => {
|
|
1707
|
+
this.polygon = null;
|
|
1708
|
+
const coords = InteractiveGraph.getPolygonCoords(
|
|
1709
|
+
this.props.graph,
|
|
1710
|
+
this.props,
|
|
1711
|
+
);
|
|
1712
|
+
// Clear out our old points so that newly added points don't
|
|
1713
|
+
// "collide", as in `addPointControls`
|
|
1714
|
+
this.points = [];
|
|
1715
|
+
this.points = _.map(coords, this.createPointForPolygonType, this);
|
|
1716
|
+
this.updatePolygon();
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
updatePolygon: () => void = () => {
|
|
1720
|
+
if (this.props.graph.type !== "polygon") {
|
|
1721
|
+
throw makeInvalidTypeError("updatePolygon", "polygon");
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
let closed;
|
|
1725
|
+
if (this.polygon) {
|
|
1726
|
+
closed = this.polygon.closed();
|
|
1727
|
+
} else if (this.points.length >= 3) {
|
|
1728
|
+
closed = true;
|
|
1729
|
+
} else {
|
|
1730
|
+
// There will only be fewer than 3 points in click-to-add-vertices
|
|
1731
|
+
// mode, so we don't need to explicitly check for that here.
|
|
1732
|
+
closed = false;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const graphie = this.graphie;
|
|
1736
|
+
const n = this.points.length;
|
|
1737
|
+
|
|
1738
|
+
// TODO(alex): check against "grid" instead, use constants
|
|
1739
|
+
const snapToGrid = !_.contains(
|
|
1740
|
+
["angles", "sides"],
|
|
1741
|
+
this.props.graph.snapTo,
|
|
1742
|
+
);
|
|
1743
|
+
|
|
1744
|
+
const angleLabels = _.times(
|
|
1745
|
+
n,
|
|
1746
|
+
function (i) {
|
|
1747
|
+
if (
|
|
1748
|
+
!this.props.graph.showAngles ||
|
|
1749
|
+
(!closed && (i === 0 || i === n - 1))
|
|
1750
|
+
) {
|
|
1751
|
+
return "";
|
|
1752
|
+
}
|
|
1753
|
+
if (this.props.graph.snapTo === "angles") {
|
|
1754
|
+
return "$deg0";
|
|
1755
|
+
}
|
|
1756
|
+
return "$deg1";
|
|
1757
|
+
},
|
|
1758
|
+
this,
|
|
1759
|
+
);
|
|
1760
|
+
|
|
1761
|
+
const showRightAngleMarkers = _.times(
|
|
1762
|
+
n,
|
|
1763
|
+
function (i) {
|
|
1764
|
+
return closed || (i !== 0 && i !== n - 1);
|
|
1765
|
+
},
|
|
1766
|
+
this,
|
|
1767
|
+
);
|
|
1768
|
+
|
|
1769
|
+
const numArcs = _.times(
|
|
1770
|
+
n,
|
|
1771
|
+
function (i) {
|
|
1772
|
+
if (
|
|
1773
|
+
this.props.graph.showAngles &&
|
|
1774
|
+
(closed || (i !== 0 && i !== n - 1))
|
|
1775
|
+
) {
|
|
1776
|
+
return 1;
|
|
1777
|
+
}
|
|
1778
|
+
return 0;
|
|
1779
|
+
},
|
|
1780
|
+
this,
|
|
1781
|
+
);
|
|
1782
|
+
|
|
1783
|
+
const sideLabels = _.times(
|
|
1784
|
+
n,
|
|
1785
|
+
function (i) {
|
|
1786
|
+
if (!this.props.graph.showSides || (!closed && i === n - 1)) {
|
|
1787
|
+
return "";
|
|
1788
|
+
}
|
|
1789
|
+
if (this.props.graph.snapTo === "sides") {
|
|
1790
|
+
return "$len0";
|
|
1791
|
+
}
|
|
1792
|
+
return "$len1";
|
|
1793
|
+
},
|
|
1794
|
+
this,
|
|
1795
|
+
);
|
|
1796
|
+
|
|
1797
|
+
if (this.polygon == null) {
|
|
1798
|
+
const self = this;
|
|
1799
|
+
self.polygon = Interactive2.addMovablePolygon(graphie, {
|
|
1800
|
+
constraints: [
|
|
1801
|
+
// $FlowFixMe[prop-missing]
|
|
1802
|
+
Interactive2.MovablePolygon.constraints.bound(),
|
|
1803
|
+
snapToGrid
|
|
1804
|
+
? // $FlowFixMe[prop-missing]
|
|
1805
|
+
Interactive2.MovablePolygon.constraints.snap()
|
|
1806
|
+
: null,
|
|
1807
|
+
],
|
|
1808
|
+
closed: closed,
|
|
1809
|
+
points: self.points,
|
|
1810
|
+
angleLabels: angleLabels,
|
|
1811
|
+
showRightAngleMarkers: showRightAngleMarkers,
|
|
1812
|
+
numArcs: numArcs,
|
|
1813
|
+
sideLabels: sideLabels,
|
|
1814
|
+
onMove: [
|
|
1815
|
+
// $FlowFixMe[prop-missing]
|
|
1816
|
+
Interactive2.MovablePolygon.onMove.updatePoints,
|
|
1817
|
+
function () {
|
|
1818
|
+
if (this.closed()) {
|
|
1819
|
+
self.updateCoordsFromPoints();
|
|
1820
|
+
}
|
|
1821
|
+
},
|
|
1822
|
+
],
|
|
1823
|
+
normalStyle: {
|
|
1824
|
+
stroke: this.props.apiOptions.isMobile
|
|
1825
|
+
? KhanColors.BLUE_C
|
|
1826
|
+
: KhanColors.INTERACTIVE,
|
|
1827
|
+
...this._lineStroke(),
|
|
1828
|
+
},
|
|
1829
|
+
});
|
|
1830
|
+
} else {
|
|
1831
|
+
// We only need to pass in the properties that might've changed
|
|
1832
|
+
this.polygon.update({
|
|
1833
|
+
closed: closed,
|
|
1834
|
+
points: this.points,
|
|
1835
|
+
angleLabels: angleLabels,
|
|
1836
|
+
showRightAngleMarkers: showRightAngleMarkers,
|
|
1837
|
+
numArcs: numArcs,
|
|
1838
|
+
sideLabels: sideLabels,
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
|
|
1843
|
+
removePolygonControls: () => void = () => {
|
|
1844
|
+
_.invoke(this.points, "remove");
|
|
1845
|
+
this.polygon?.remove();
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
addAngleControls: () => void = () => {
|
|
1849
|
+
if (this.props.graph.type !== "angle") {
|
|
1850
|
+
throw makeInvalidTypeError("addAngleControls", "angle");
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
const graph = this.props.graph;
|
|
1854
|
+
const graphie = this.graphie;
|
|
1855
|
+
|
|
1856
|
+
const coords = InteractiveGraph.getAngleCoords(
|
|
1857
|
+
this.props.graph,
|
|
1858
|
+
this.props,
|
|
1859
|
+
);
|
|
1860
|
+
|
|
1861
|
+
// The vertex snaps to the grid, but the rays don't...
|
|
1862
|
+
this.points = coords.map(function (coord, i) {
|
|
1863
|
+
return graphie?.addMovablePoint(
|
|
1864
|
+
_.extend(
|
|
1865
|
+
{
|
|
1866
|
+
coord: coord,
|
|
1867
|
+
normalStyle: {
|
|
1868
|
+
stroke: KhanColors.INTERACTIVE,
|
|
1869
|
+
fill: KhanColors.INTERACTIVE,
|
|
1870
|
+
},
|
|
1871
|
+
},
|
|
1872
|
+
i === 1
|
|
1873
|
+
? {
|
|
1874
|
+
snapX: graphie.snap[0],
|
|
1875
|
+
snapY: graphie.snap[1],
|
|
1876
|
+
}
|
|
1877
|
+
: {},
|
|
1878
|
+
),
|
|
1879
|
+
);
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1882
|
+
// ...they snap to whole-degree angles from the vertex.
|
|
1883
|
+
this.angle = graphie?.addMovableAngle({
|
|
1884
|
+
points: this.points,
|
|
1885
|
+
snapDegrees: graph.snapDegrees || 1,
|
|
1886
|
+
snapOffsetDeg: graph.angleOffsetDeg || 0,
|
|
1887
|
+
angleLabel: graph.showAngles ? "$deg0" : "",
|
|
1888
|
+
pushOut: 2,
|
|
1889
|
+
allowReflex: defaultVal(graph.allowReflexAngles, true),
|
|
1890
|
+
});
|
|
1891
|
+
|
|
1892
|
+
$(this.angle).on("move", () => {
|
|
1893
|
+
this.onChange({
|
|
1894
|
+
graph: {...graph, coords: this.angle?.getClockwiseCoords()},
|
|
1895
|
+
});
|
|
1896
|
+
});
|
|
1897
|
+
};
|
|
1898
|
+
|
|
1899
|
+
removeAngleControls: () => void = () => {
|
|
1900
|
+
_.invoke(this.points, "remove");
|
|
1901
|
+
this.angle?.remove();
|
|
1902
|
+
};
|
|
1903
|
+
|
|
1904
|
+
toggleShowAngles: () => void = () => {
|
|
1905
|
+
if (this.props.graph.type !== "polygon") {
|
|
1906
|
+
throw makeInvalidTypeError("toggleShowAngles", "polygon");
|
|
1907
|
+
}
|
|
1908
|
+
const graph = _.extend({}, this.props.graph, {
|
|
1909
|
+
showAngles: !this.props.graph.showAngles,
|
|
1910
|
+
});
|
|
1911
|
+
this.onChange({graph: graph});
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
toggleShowSides: () => void = () => {
|
|
1915
|
+
if (this.props.graph.type !== "polygon") {
|
|
1916
|
+
throw makeInvalidTypeError("toggleShowSides", "polygon");
|
|
1917
|
+
}
|
|
1918
|
+
const graph = _.extend({}, this.props.graph, {
|
|
1919
|
+
showSides: !this.props.graph.showSides,
|
|
1920
|
+
});
|
|
1921
|
+
this.onChange({graph: graph});
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1924
|
+
getUserInput: () => PerseusGraphType = () => {
|
|
1925
|
+
return InteractiveGraph.getUserInputFromProps(this.props);
|
|
1926
|
+
};
|
|
1927
|
+
|
|
1928
|
+
simpleValidate: (Rubric) => PerseusScore = (rubric) => {
|
|
1929
|
+
return InteractiveGraph.validate(this.getUserInput(), rubric, this);
|
|
1930
|
+
};
|
|
1931
|
+
|
|
1932
|
+
focus: () => void = $.noop;
|
|
1933
|
+
|
|
1934
|
+
render(): React.Node {
|
|
1935
|
+
const box = getInteractiveBoxFromSizeClass(
|
|
1936
|
+
this.props.containerSizeClass,
|
|
1937
|
+
);
|
|
1938
|
+
|
|
1939
|
+
let instructions;
|
|
1940
|
+
if (this.isClickToAddPoints() && this.state.shouldShowInstructions) {
|
|
1941
|
+
if (this.props.graph.type === "point") {
|
|
1942
|
+
instructions = i18n._("Click to add points");
|
|
1943
|
+
} else if (this.props.graph.type === "polygon") {
|
|
1944
|
+
instructions = i18n._("Click to add vertices");
|
|
1945
|
+
}
|
|
1946
|
+
} else {
|
|
1947
|
+
instructions = undefined;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
const onMouseDown = this.isClickToAddPoints()
|
|
1951
|
+
? this.handleAddPointsMouseDown
|
|
1952
|
+
: null;
|
|
1953
|
+
|
|
1954
|
+
const gridStep =
|
|
1955
|
+
this.props.gridStep ||
|
|
1956
|
+
Util.getGridStep(this.props.range, this.props.step, box[0]);
|
|
1957
|
+
const snapStep =
|
|
1958
|
+
this.props.snapStep || Util.snapStepFromGridStep(gridStep);
|
|
1959
|
+
|
|
1960
|
+
const isMobile = this.props.apiOptions.isMobile;
|
|
1961
|
+
|
|
1962
|
+
return (
|
|
1963
|
+
<div
|
|
1964
|
+
className={
|
|
1965
|
+
"perseus-widget " + "perseus-widget-interactive-graph"
|
|
1966
|
+
}
|
|
1967
|
+
style={{
|
|
1968
|
+
width: box[0],
|
|
1969
|
+
height: box[1],
|
|
1970
|
+
}}
|
|
1971
|
+
>
|
|
1972
|
+
<Graph
|
|
1973
|
+
instructions={instructions}
|
|
1974
|
+
// eslint-disable-next-line react/no-string-refs
|
|
1975
|
+
ref="graph"
|
|
1976
|
+
box={box}
|
|
1977
|
+
labels={this.props.labels}
|
|
1978
|
+
range={this.props.range}
|
|
1979
|
+
step={
|
|
1980
|
+
isMobile
|
|
1981
|
+
? Util.constrainedTickStepsFromTickSteps(
|
|
1982
|
+
this.props.step,
|
|
1983
|
+
this.props.range,
|
|
1984
|
+
)
|
|
1985
|
+
: this.props.step
|
|
1986
|
+
}
|
|
1987
|
+
gridStep={gridStep}
|
|
1988
|
+
snapStep={snapStep}
|
|
1989
|
+
markings={this.props.markings}
|
|
1990
|
+
backgroundImage={this.props.backgroundImage}
|
|
1991
|
+
showProtractor={this.props.showProtractor}
|
|
1992
|
+
showRuler={this.props.showRuler}
|
|
1993
|
+
rulerLabel={this.props.rulerLabel}
|
|
1994
|
+
rulerTicks={this.props.rulerTicks}
|
|
1995
|
+
onMouseDown={onMouseDown}
|
|
1996
|
+
onGraphieUpdated={this.setGraphie}
|
|
1997
|
+
setDrawingAreaAvailable={
|
|
1998
|
+
this.props.apiOptions.setDrawingAreaAvailable
|
|
1999
|
+
}
|
|
2000
|
+
isMobile={isMobile}
|
|
2001
|
+
/>
|
|
2002
|
+
</div>
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
static getQuadraticCoefficients(
|
|
2007
|
+
coords: $ReadOnlyArray<Coord>,
|
|
2008
|
+
): QuadraticCoefficient {
|
|
2009
|
+
const p1 = coords[0];
|
|
2010
|
+
const p2 = coords[1];
|
|
2011
|
+
const p3 = coords[2];
|
|
2012
|
+
|
|
2013
|
+
const denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]);
|
|
2014
|
+
if (denom === 0) {
|
|
2015
|
+
// Many of the callers assume that the return value is always defined.
|
|
2016
|
+
// $FlowFixMe[incompatible-return]
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
const a =
|
|
2020
|
+
(p3[0] * (p2[1] - p1[1]) +
|
|
2021
|
+
p2[0] * (p1[1] - p3[1]) +
|
|
2022
|
+
p1[0] * (p3[1] - p2[1])) /
|
|
2023
|
+
denom;
|
|
2024
|
+
const b =
|
|
2025
|
+
(p3[0] * p3[0] * (p1[1] - p2[1]) +
|
|
2026
|
+
p2[0] * p2[0] * (p3[1] - p1[1]) +
|
|
2027
|
+
p1[0] * p1[0] * (p2[1] - p3[1])) /
|
|
2028
|
+
denom;
|
|
2029
|
+
const c =
|
|
2030
|
+
(p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] +
|
|
2031
|
+
p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] +
|
|
2032
|
+
p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) /
|
|
2033
|
+
denom;
|
|
2034
|
+
return [a, b, c];
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
static getSinusoidCoefficients(
|
|
2038
|
+
coords: $ReadOnlyArray<Coord>,
|
|
2039
|
+
): SineCoefficient {
|
|
2040
|
+
// It's assumed that p1 is the root and p2 is the first peak
|
|
2041
|
+
const p1 = coords[0];
|
|
2042
|
+
const p2 = coords[1];
|
|
2043
|
+
|
|
2044
|
+
// Resulting coefficients are canonical for this sine curve
|
|
2045
|
+
const amplitude = p2[1] - p1[1];
|
|
2046
|
+
const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
|
|
2047
|
+
const phase = p1[0] * angularFrequency;
|
|
2048
|
+
const verticalOffset = p1[1];
|
|
2049
|
+
|
|
2050
|
+
return [amplitude, angularFrequency, phase, verticalOffset];
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/**
|
|
2054
|
+
* @param {object} graph Like props.graph or props.correct
|
|
2055
|
+
* @param {object} props of an InteractiveGraph instance
|
|
2056
|
+
*/
|
|
2057
|
+
static getLineCoords(
|
|
2058
|
+
graph: PerseusGraphType,
|
|
2059
|
+
props: Props,
|
|
2060
|
+
): $ReadOnlyArray<Coord> {
|
|
2061
|
+
return (
|
|
2062
|
+
// $FlowFixMe[incompatible-return]
|
|
2063
|
+
graph.coords ||
|
|
2064
|
+
InteractiveGraph.pointsFromNormalized(props, [
|
|
2065
|
+
[0.25, 0.75],
|
|
2066
|
+
[0.75, 0.75],
|
|
2067
|
+
])
|
|
2068
|
+
);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
/**
|
|
2072
|
+
* @param {object} graph Like props.graph or props.correct
|
|
2073
|
+
* @param {object} props of an InteractiveGraph instance
|
|
2074
|
+
*/
|
|
2075
|
+
static getPointCoords(
|
|
2076
|
+
graph: PerseusGraphTypePoint,
|
|
2077
|
+
props: Props,
|
|
2078
|
+
): $ReadOnlyArray<Coord> {
|
|
2079
|
+
const numPoints = graph.numPoints || 1;
|
|
2080
|
+
let coords = graph.coords;
|
|
2081
|
+
|
|
2082
|
+
if (coords) {
|
|
2083
|
+
return coords;
|
|
2084
|
+
}
|
|
2085
|
+
switch (numPoints) {
|
|
2086
|
+
case 1:
|
|
2087
|
+
// Back in the day, one point's coords were in graph.coord
|
|
2088
|
+
coords = [graph.coord || [0, 0]];
|
|
2089
|
+
break;
|
|
2090
|
+
case 2:
|
|
2091
|
+
coords = [
|
|
2092
|
+
[-5, 0],
|
|
2093
|
+
[5, 0],
|
|
2094
|
+
];
|
|
2095
|
+
break;
|
|
2096
|
+
case 3:
|
|
2097
|
+
coords = [
|
|
2098
|
+
[-5, 0],
|
|
2099
|
+
[0, 0],
|
|
2100
|
+
[5, 0],
|
|
2101
|
+
];
|
|
2102
|
+
break;
|
|
2103
|
+
case 4:
|
|
2104
|
+
coords = [
|
|
2105
|
+
[-6, 0],
|
|
2106
|
+
[-2, 0],
|
|
2107
|
+
[2, 0],
|
|
2108
|
+
[6, 0],
|
|
2109
|
+
];
|
|
2110
|
+
break;
|
|
2111
|
+
case 5:
|
|
2112
|
+
coords = [
|
|
2113
|
+
[-6, 0],
|
|
2114
|
+
[-3, 0],
|
|
2115
|
+
[0, 0],
|
|
2116
|
+
[3, 0],
|
|
2117
|
+
[6, 0],
|
|
2118
|
+
];
|
|
2119
|
+
break;
|
|
2120
|
+
case 6:
|
|
2121
|
+
coords = [
|
|
2122
|
+
[-5, 0],
|
|
2123
|
+
[-3, 0],
|
|
2124
|
+
[-1, 0],
|
|
2125
|
+
[1, 0],
|
|
2126
|
+
[3, 0],
|
|
2127
|
+
[5, 0],
|
|
2128
|
+
];
|
|
2129
|
+
break;
|
|
2130
|
+
case UNLIMITED:
|
|
2131
|
+
coords = [];
|
|
2132
|
+
break;
|
|
2133
|
+
}
|
|
2134
|
+
// Transform coords from their -10 to 10 space to 0 to 1
|
|
2135
|
+
// because of the old graph.coord, and also it's easier.
|
|
2136
|
+
const range = [
|
|
2137
|
+
[-10, 10],
|
|
2138
|
+
[-10, 10],
|
|
2139
|
+
];
|
|
2140
|
+
// $FlowFixMe[incompatible-call]
|
|
2141
|
+
const newCoords = InteractiveGraph.normalizeCoords(coords, range);
|
|
2142
|
+
|
|
2143
|
+
return InteractiveGraph.pointsFromNormalized(props, newCoords);
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
/**
|
|
2147
|
+
* @param {object} graph Like props.graph or props.correct
|
|
2148
|
+
* @param {object} props of an InteractiveGraph instance
|
|
2149
|
+
*/
|
|
2150
|
+
static getLinearSystemCoords(
|
|
2151
|
+
graph: PerseusGraphType,
|
|
2152
|
+
props: Props,
|
|
2153
|
+
): $ReadOnlyArray<$ReadOnlyArray<Coord>> {
|
|
2154
|
+
return (
|
|
2155
|
+
// The callers assume that we're return an array of points
|
|
2156
|
+
// $FlowFixMe[incompatible-return]
|
|
2157
|
+
graph.coords ||
|
|
2158
|
+
_.map(
|
|
2159
|
+
[
|
|
2160
|
+
[
|
|
2161
|
+
[0.25, 0.75],
|
|
2162
|
+
[0.75, 0.75],
|
|
2163
|
+
],
|
|
2164
|
+
[
|
|
2165
|
+
[0.25, 0.25],
|
|
2166
|
+
[0.75, 0.25],
|
|
2167
|
+
],
|
|
2168
|
+
],
|
|
2169
|
+
(coords) => {
|
|
2170
|
+
return InteractiveGraph.pointsFromNormalized(props, coords);
|
|
2171
|
+
},
|
|
2172
|
+
)
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
/**
|
|
2177
|
+
* @param {object} graph Like props.graph or props.correct
|
|
2178
|
+
* @param {object} props of an InteractiveGraph instance
|
|
2179
|
+
*/
|
|
2180
|
+
static getPolygonCoords(
|
|
2181
|
+
graph: PerseusGraphType,
|
|
2182
|
+
props: Props,
|
|
2183
|
+
): $ReadOnlyArray<Coord> {
|
|
2184
|
+
if (graph.type !== "polygon") {
|
|
2185
|
+
throw makeInvalidTypeError("toggleShowSides", "polygon");
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
let coords = graph.coords;
|
|
2189
|
+
if (coords) {
|
|
2190
|
+
return coords;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
const n = graph.numSides || 3;
|
|
2194
|
+
|
|
2195
|
+
if (n === UNLIMITED) {
|
|
2196
|
+
coords = [];
|
|
2197
|
+
} else {
|
|
2198
|
+
// $FlowFixMe[unsafe-addition]
|
|
2199
|
+
const angle = (2 * Math.PI) / n;
|
|
2200
|
+
// $FlowFixMe[unsafe-addition]
|
|
2201
|
+
const offset = (1 / n - 1 / 2) * Math.PI;
|
|
2202
|
+
|
|
2203
|
+
// TODO(alex): Generalize this to more than just triangles so that
|
|
2204
|
+
// all polygons have whole number side lengths if snapping to sides
|
|
2205
|
+
const radius =
|
|
2206
|
+
graph.snapTo === "sides" ? (Math.sqrt(3) / 3) * 7 : 4;
|
|
2207
|
+
|
|
2208
|
+
// Generate coords of a regular polygon with n sides
|
|
2209
|
+
coords = _.times(n, function (i) {
|
|
2210
|
+
return [
|
|
2211
|
+
radius * Math.cos(i * angle + offset),
|
|
2212
|
+
radius * Math.sin(i * angle + offset),
|
|
2213
|
+
];
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
const ranges: [Range, Range] = [
|
|
2218
|
+
[-10, 10],
|
|
2219
|
+
[-10, 10],
|
|
2220
|
+
];
|
|
2221
|
+
coords = InteractiveGraph.normalizeCoords(coords, ranges);
|
|
2222
|
+
|
|
2223
|
+
const snapToGrid = !_.contains(["angles", "sides"], graph.snapTo);
|
|
2224
|
+
coords = InteractiveGraph.pointsFromNormalized(
|
|
2225
|
+
props,
|
|
2226
|
+
coords,
|
|
2227
|
+
/* noSnap */ !snapToGrid,
|
|
2228
|
+
);
|
|
2229
|
+
|
|
2230
|
+
return coords;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* @param {object} graph Like props.graph or props.correct
|
|
2235
|
+
* @param {object} props of an InteractiveGraph instance
|
|
2236
|
+
*/
|
|
2237
|
+
static getSegmentCoords(
|
|
2238
|
+
graph: PerseusGraphTypeSegment,
|
|
2239
|
+
props: Props,
|
|
2240
|
+
): $ReadOnlyArray<$ReadOnlyArray<Coord>> {
|
|
2241
|
+
const coords = graph.coords;
|
|
2242
|
+
if (coords) {
|
|
2243
|
+
return coords;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
const n = graph.numSegments || 1;
|
|
2247
|
+
const ys: $ReadOnlyArray<number> = {
|
|
2248
|
+
// $FlowFixMe[unsupported-syntax]
|
|
2249
|
+
1: [5],
|
|
2250
|
+
// $FlowFixMe[unsupported-syntax]
|
|
2251
|
+
2: [5, -5],
|
|
2252
|
+
// $FlowFixMe[unsupported-syntax]
|
|
2253
|
+
3: [5, 0, -5],
|
|
2254
|
+
// $FlowFixMe[unsupported-syntax]
|
|
2255
|
+
4: [6, 2, -2, -6],
|
|
2256
|
+
// $FlowFixMe[unsupported-syntax]
|
|
2257
|
+
5: [6, 3, 0, -3, -6],
|
|
2258
|
+
// $FlowFixMe[unsupported-syntax]
|
|
2259
|
+
6: [5, 3, 1, -1, -3, -5],
|
|
2260
|
+
}[n];
|
|
2261
|
+
const range = [
|
|
2262
|
+
[-10, 10],
|
|
2263
|
+
[-10, 10],
|
|
2264
|
+
];
|
|
2265
|
+
|
|
2266
|
+
return ys.map(function (y) {
|
|
2267
|
+
let segment = [
|
|
2268
|
+
[-5, y],
|
|
2269
|
+
[5, y],
|
|
2270
|
+
];
|
|
2271
|
+
segment = InteractiveGraph.normalizeCoords(segment, range);
|
|
2272
|
+
segment = InteractiveGraph.pointsFromNormalized(props, segment);
|
|
2273
|
+
return segment;
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
/**
|
|
2278
|
+
* @param {object} graph Like props.graph or props.correct
|
|
2279
|
+
* @param {object} props of an InteractiveGraph instance
|
|
2280
|
+
*/
|
|
2281
|
+
static getAngleCoords(
|
|
2282
|
+
graph: PerseusGraphTypeAngle,
|
|
2283
|
+
props: Props,
|
|
2284
|
+
): $ReadOnlyArray<Coord> {
|
|
2285
|
+
let coords = graph.coords;
|
|
2286
|
+
if (coords) {
|
|
2287
|
+
return coords;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
const snap = graph.snapDegrees || 1;
|
|
2291
|
+
let angle = snap;
|
|
2292
|
+
while (angle < 20) {
|
|
2293
|
+
angle += snap;
|
|
2294
|
+
}
|
|
2295
|
+
angle = (angle * Math.PI) / 180;
|
|
2296
|
+
const offset = ((graph.angleOffsetDeg || 0) * Math.PI) / 180;
|
|
2297
|
+
|
|
2298
|
+
coords = InteractiveGraph.pointsFromNormalized(props, [
|
|
2299
|
+
[0.85, 0.5],
|
|
2300
|
+
[0.5, 0.5],
|
|
2301
|
+
]);
|
|
2302
|
+
|
|
2303
|
+
const radius = magnitude(vector(...coords));
|
|
2304
|
+
|
|
2305
|
+
// Adjust the lower point by angleOffsetDeg degrees
|
|
2306
|
+
// $FlowFixMe[cannot-write]
|
|
2307
|
+
coords[0] = [
|
|
2308
|
+
coords[1][0] + radius * Math.cos(offset),
|
|
2309
|
+
coords[1][1] + radius * Math.sin(offset),
|
|
2310
|
+
];
|
|
2311
|
+
// Position the upper point angle radians from the
|
|
2312
|
+
// lower point
|
|
2313
|
+
// $FlowFixMe[cannot-write]
|
|
2314
|
+
coords[2] = [
|
|
2315
|
+
coords[1][0] + radius * Math.cos(angle + offset),
|
|
2316
|
+
coords[1][1] + radius * Math.sin(angle + offset),
|
|
2317
|
+
];
|
|
2318
|
+
|
|
2319
|
+
return coords;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
static normalizeCoords(
|
|
2323
|
+
coordsList: $ReadOnlyArray<Coord>,
|
|
2324
|
+
ranges: [Range, Range],
|
|
2325
|
+
): $ReadOnlyArray<Coord> {
|
|
2326
|
+
return _.map(coordsList, function (coords) {
|
|
2327
|
+
return _.map(coords, function (coord, i) {
|
|
2328
|
+
const extent = ranges[i][1] - ranges[i][0];
|
|
2329
|
+
return (coord + ranges[i][1]) / extent;
|
|
2330
|
+
});
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
static getEquationString(props: Props): string {
|
|
2335
|
+
const type = props.graph.type;
|
|
2336
|
+
const funcName = "get" + capitalize(type) + "EquationString";
|
|
2337
|
+
// $FlowFixMe[incompatible-use]
|
|
2338
|
+
return InteractiveGraph[funcName](props);
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
static pointsFromNormalized(
|
|
2342
|
+
props: Props,
|
|
2343
|
+
coordsList: $ReadOnlyArray<Coord>,
|
|
2344
|
+
noSnap?: boolean,
|
|
2345
|
+
): $ReadOnlyArray<Coord> {
|
|
2346
|
+
return _.map(coordsList, function (coords) {
|
|
2347
|
+
return _.map(coords, function (coord, i) {
|
|
2348
|
+
const range: Range = props.range[i];
|
|
2349
|
+
if (noSnap) {
|
|
2350
|
+
return range[0] + (range[1] - range[0]) * coord;
|
|
2351
|
+
}
|
|
2352
|
+
const step = props.step[i];
|
|
2353
|
+
const nSteps = numSteps(range, step);
|
|
2354
|
+
const tick = Math.round(coord * nSteps);
|
|
2355
|
+
return range[0] + step * tick;
|
|
2356
|
+
});
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
static getLinearEquationString(props: Props): string {
|
|
2361
|
+
const coords = InteractiveGraph.getLineCoords(props.graph, props);
|
|
2362
|
+
if (eq(coords[0][0], coords[1][0])) {
|
|
2363
|
+
return "x = " + coords[0][0].toFixed(3);
|
|
2364
|
+
}
|
|
2365
|
+
const m = (coords[1][1] - coords[0][1]) / (coords[1][0] - coords[0][0]);
|
|
2366
|
+
const b = coords[0][1] - m * coords[0][0];
|
|
2367
|
+
if (eq(m, 0)) {
|
|
2368
|
+
return "y = " + b.toFixed(3);
|
|
2369
|
+
}
|
|
2370
|
+
return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
static getCurrentQuadraticCoefficients(props: Props): QuadraticCoefficient {
|
|
2374
|
+
// TODO(alpert): Don't duplicate
|
|
2375
|
+
const coords =
|
|
2376
|
+
props.graph.coords ||
|
|
2377
|
+
InteractiveGraph.defaultQuadraticCoords(props);
|
|
2378
|
+
// $FlowFixMe[incompatible-call]
|
|
2379
|
+
return InteractiveGraph.getQuadraticCoefficients(coords);
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
static defaultQuadraticCoords(props: Props): $ReadOnlyArray<Coord> {
|
|
2383
|
+
const coords = [
|
|
2384
|
+
[0.25, 0.75],
|
|
2385
|
+
[0.5, 0.25],
|
|
2386
|
+
[0.75, 0.75],
|
|
2387
|
+
];
|
|
2388
|
+
return InteractiveGraph.pointsFromNormalized(props, coords);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
static getQuadraticEquationString(props: Props): string {
|
|
2392
|
+
const coeffs = InteractiveGraph.getCurrentQuadraticCoefficients(props);
|
|
2393
|
+
return (
|
|
2394
|
+
"y = " +
|
|
2395
|
+
coeffs[0].toFixed(3) +
|
|
2396
|
+
"x^2 + " +
|
|
2397
|
+
coeffs[1].toFixed(3) +
|
|
2398
|
+
"x + " +
|
|
2399
|
+
coeffs[2].toFixed(3)
|
|
2400
|
+
);
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
static getCurrentSinusoidCoefficients(props: Props): SineCoefficient {
|
|
2404
|
+
const coords =
|
|
2405
|
+
props.graph.coords || InteractiveGraph.defaultSinusoidCoords(props);
|
|
2406
|
+
// $FlowFixMe[incompatible-call]
|
|
2407
|
+
return InteractiveGraph.getSinusoidCoefficients(coords);
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
static defaultSinusoidCoords(props: Props): $ReadOnlyArray<Coord> {
|
|
2411
|
+
const coords = [
|
|
2412
|
+
[0.5, 0.5],
|
|
2413
|
+
[0.65, 0.6],
|
|
2414
|
+
];
|
|
2415
|
+
return InteractiveGraph.pointsFromNormalized(props, coords);
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
static getSinusoidEquationString(props: Props): string {
|
|
2419
|
+
const coeffs = InteractiveGraph.getCurrentSinusoidCoefficients(props);
|
|
2420
|
+
return (
|
|
2421
|
+
"y = " +
|
|
2422
|
+
coeffs[0].toFixed(3) +
|
|
2423
|
+
"sin(" +
|
|
2424
|
+
coeffs[1].toFixed(3) +
|
|
2425
|
+
"x - " +
|
|
2426
|
+
coeffs[2].toFixed(3) +
|
|
2427
|
+
") + " +
|
|
2428
|
+
coeffs[3].toFixed(3)
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
static getCircleEquationString(props: Props): string {
|
|
2433
|
+
const graph = props.graph;
|
|
2434
|
+
// TODO(alpert): Don't duplicate
|
|
2435
|
+
const center = graph.center || [0, 0];
|
|
2436
|
+
const radius = graph.radius || 2;
|
|
2437
|
+
return (
|
|
2438
|
+
"center (" + center[0] + ", " + center[1] + "), radius " + radius
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
static getLinearSystemEquationString(props: Props): string {
|
|
2443
|
+
const coords = InteractiveGraph.getLinearSystemCoords(
|
|
2444
|
+
props.graph,
|
|
2445
|
+
props,
|
|
2446
|
+
);
|
|
2447
|
+
|
|
2448
|
+
return (
|
|
2449
|
+
"\n" +
|
|
2450
|
+
getLineEquation(coords[0][0], coords[0][1]) +
|
|
2451
|
+
"\n" +
|
|
2452
|
+
getLineEquation(coords[1][0], coords[1][1]) +
|
|
2453
|
+
"\n" +
|
|
2454
|
+
getLineIntersection(coords[0], coords[1])
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
static getPointEquationString(props: Props): string {
|
|
2459
|
+
if (props.graph.type !== "point") {
|
|
2460
|
+
throw makeInvalidTypeError("getPointEquationString", "point");
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
const coords = InteractiveGraph.getPointCoords(props.graph, props);
|
|
2464
|
+
return coords
|
|
2465
|
+
.map(function (coord) {
|
|
2466
|
+
return "(" + coord[0] + ", " + coord[1] + ")";
|
|
2467
|
+
})
|
|
2468
|
+
.join(", ");
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
static getSegmentEquationString(props: Props): string {
|
|
2472
|
+
if (props.graph.type !== "segment") {
|
|
2473
|
+
throw makeInvalidTypeError("getSegmentEquationString", "segment");
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
const segments = InteractiveGraph.getSegmentCoords(props.graph, props);
|
|
2477
|
+
return _.map(segments, function (segment) {
|
|
2478
|
+
return (
|
|
2479
|
+
"[" +
|
|
2480
|
+
_.map(segment, function (coord) {
|
|
2481
|
+
return "(" + coord.join(", ") + ")";
|
|
2482
|
+
}).join(" ") +
|
|
2483
|
+
"]"
|
|
2484
|
+
);
|
|
2485
|
+
}).join(" ");
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
static getRayEquationString(props: Props): string {
|
|
2489
|
+
if (props.graph.type !== "ray") {
|
|
2490
|
+
throw makeInvalidTypeError("createPointForPolygonType", "ray");
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
const coords = InteractiveGraph.getLineCoords(props.graph, props);
|
|
2494
|
+
const a = coords[0];
|
|
2495
|
+
const b = coords[1];
|
|
2496
|
+
let eq = InteractiveGraph.getLinearEquationString(props);
|
|
2497
|
+
|
|
2498
|
+
if (a[0] > b[0]) {
|
|
2499
|
+
eq += " (for x <= " + a[0].toFixed(3) + ")";
|
|
2500
|
+
} else if (a[0] < b[0]) {
|
|
2501
|
+
eq += " (for x >= " + a[0].toFixed(3) + ")";
|
|
2502
|
+
} else if (a[1] > b[1]) {
|
|
2503
|
+
eq += " (for y <= " + a[1].toFixed(3) + ")";
|
|
2504
|
+
} else {
|
|
2505
|
+
eq += " (for y >= " + a[1].toFixed(3) + ")";
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
return eq;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
static getPolygonEquationString(props: Props): string {
|
|
2512
|
+
if (props.graph.type !== "polygon") {
|
|
2513
|
+
throw makeInvalidTypeError("getPolygonEquationString", "polygon");
|
|
2514
|
+
}
|
|
2515
|
+
const coords = InteractiveGraph.getPolygonCoords(props.graph, props);
|
|
2516
|
+
return _.map(coords, function (coord) {
|
|
2517
|
+
return "(" + coord.join(", ") + ")";
|
|
2518
|
+
}).join(" ");
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
static getAngleEquationString(props: Props): string {
|
|
2522
|
+
if (props.graph.type !== "angle") {
|
|
2523
|
+
throw makeInvalidTypeError("getAngleEquationString", "angle");
|
|
2524
|
+
}
|
|
2525
|
+
const coords = InteractiveGraph.getAngleCoords(props.graph, props);
|
|
2526
|
+
const angle = GraphUtils.findAngle(coords[2], coords[0], coords[1]);
|
|
2527
|
+
return (
|
|
2528
|
+
angle.toFixed(0) +
|
|
2529
|
+
"\u00B0 angle" +
|
|
2530
|
+
" at (" +
|
|
2531
|
+
coords[1].join(", ") +
|
|
2532
|
+
")"
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
static validate(
|
|
2537
|
+
// TODO(kevinb): rename state to userInput
|
|
2538
|
+
state: PerseusGraphType, // state === userInput
|
|
2539
|
+
rubric: Rubric,
|
|
2540
|
+
component: $FlowFixMe,
|
|
2541
|
+
): PerseusScore {
|
|
2542
|
+
// When nothing has moved, there will neither be coords nor the
|
|
2543
|
+
// circle's center/radius fields. When those fields are absent, skip
|
|
2544
|
+
// all these checks; just go mark the answer as empty.
|
|
2545
|
+
const hasValue = !!(state.coords || (state.center && state.radius));
|
|
2546
|
+
|
|
2547
|
+
if (state.type === rubric.correct.type && hasValue) {
|
|
2548
|
+
if (
|
|
2549
|
+
state.type === "linear" &&
|
|
2550
|
+
rubric.correct.type === "linear" &&
|
|
2551
|
+
state.coords != null
|
|
2552
|
+
) {
|
|
2553
|
+
const guess = state.coords;
|
|
2554
|
+
const correct = rubric.correct.coords;
|
|
2555
|
+
|
|
2556
|
+
// If both of the guess points are on the correct line, it's
|
|
2557
|
+
// correct.
|
|
2558
|
+
if (
|
|
2559
|
+
// $FlowFixMe[incompatible-use] - Fix type so coords can't be omitted
|
|
2560
|
+
collinear(correct[0], correct[1], guess[0]) &&
|
|
2561
|
+
collinear(correct[0], correct[1], guess[1])
|
|
2562
|
+
) {
|
|
2563
|
+
return {
|
|
2564
|
+
type: "points",
|
|
2565
|
+
earned: 1,
|
|
2566
|
+
total: 1,
|
|
2567
|
+
message: null,
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
} else if (
|
|
2571
|
+
state.type === "linear-system" &&
|
|
2572
|
+
rubric.correct.type === "linear-system" &&
|
|
2573
|
+
state.coords != null
|
|
2574
|
+
) {
|
|
2575
|
+
const guess = state.coords;
|
|
2576
|
+
// $FlowFixMe[incompatible-cast] - Fix type so this can't be undefined
|
|
2577
|
+
const correct = (rubric.correct.coords: $ReadOnlyArray<
|
|
2578
|
+
$ReadOnlyArray<Coord>,
|
|
2579
|
+
>);
|
|
2580
|
+
|
|
2581
|
+
if (
|
|
2582
|
+
(collinear(correct[0][0], correct[0][1], guess[0][0]) &&
|
|
2583
|
+
collinear(correct[0][0], correct[0][1], guess[0][1]) &&
|
|
2584
|
+
collinear(correct[1][0], correct[1][1], guess[1][0]) &&
|
|
2585
|
+
collinear(correct[1][0], correct[1][1], guess[1][1])) ||
|
|
2586
|
+
(collinear(correct[0][0], correct[0][1], guess[1][0]) &&
|
|
2587
|
+
collinear(correct[0][0], correct[0][1], guess[1][1]) &&
|
|
2588
|
+
collinear(correct[1][0], correct[1][1], guess[0][0]) &&
|
|
2589
|
+
collinear(correct[1][0], correct[1][1], guess[0][1]))
|
|
2590
|
+
) {
|
|
2591
|
+
return {
|
|
2592
|
+
type: "points",
|
|
2593
|
+
earned: 1,
|
|
2594
|
+
total: 1,
|
|
2595
|
+
message: null,
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
} else if (
|
|
2599
|
+
state.type === "quadratic" &&
|
|
2600
|
+
rubric.correct.type === "quadratic" &&
|
|
2601
|
+
state.coords != null
|
|
2602
|
+
) {
|
|
2603
|
+
// If the parabola coefficients match, it's correct.
|
|
2604
|
+
const guessCoeffs = this.getQuadraticCoefficients(state.coords);
|
|
2605
|
+
const correctCoeffs = this.getQuadraticCoefficients(
|
|
2606
|
+
// $FlowFixMe[incompatible-call]
|
|
2607
|
+
// $FlowFixMe[prop-missing]
|
|
2608
|
+
rubric.correct.coords,
|
|
2609
|
+
);
|
|
2610
|
+
if (deepEq(guessCoeffs, correctCoeffs)) {
|
|
2611
|
+
return {
|
|
2612
|
+
type: "points",
|
|
2613
|
+
earned: 1,
|
|
2614
|
+
total: 1,
|
|
2615
|
+
message: null,
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
} else if (
|
|
2619
|
+
state.type === "sinusoid" &&
|
|
2620
|
+
rubric.correct.type === "sinusoid" &&
|
|
2621
|
+
state.coords != null
|
|
2622
|
+
) {
|
|
2623
|
+
const guessCoeffs = this.getSinusoidCoefficients(state.coords);
|
|
2624
|
+
const correctCoeffs = this.getSinusoidCoefficients(
|
|
2625
|
+
// $FlowFixMe[incompatible-call]
|
|
2626
|
+
// $FlowFixMe[prop-missing]
|
|
2627
|
+
rubric.correct.coords,
|
|
2628
|
+
);
|
|
2629
|
+
|
|
2630
|
+
const canonicalGuessCoeffs =
|
|
2631
|
+
canonicalSineCoefficients(guessCoeffs);
|
|
2632
|
+
const canonicalCorrectCoeffs =
|
|
2633
|
+
canonicalSineCoefficients(correctCoeffs);
|
|
2634
|
+
// If the canonical coefficients match, it's correct.
|
|
2635
|
+
if (deepEq(canonicalGuessCoeffs, canonicalCorrectCoeffs)) {
|
|
2636
|
+
return {
|
|
2637
|
+
type: "points",
|
|
2638
|
+
earned: 1,
|
|
2639
|
+
total: 1,
|
|
2640
|
+
message: null,
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
} else if (
|
|
2644
|
+
state.type === "circle" &&
|
|
2645
|
+
rubric.correct.type === "circle"
|
|
2646
|
+
) {
|
|
2647
|
+
if (
|
|
2648
|
+
deepEq(state.center, rubric.correct.center) &&
|
|
2649
|
+
// $FlowFixMe[prop-missing]
|
|
2650
|
+
eq(state.radius, rubric.correct.radius)
|
|
2651
|
+
) {
|
|
2652
|
+
return {
|
|
2653
|
+
type: "points",
|
|
2654
|
+
earned: 1,
|
|
2655
|
+
total: 1,
|
|
2656
|
+
message: null,
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
} else if (
|
|
2660
|
+
state.type === "point" &&
|
|
2661
|
+
rubric.correct.type === "point" &&
|
|
2662
|
+
state.coords != null
|
|
2663
|
+
) {
|
|
2664
|
+
let correct = InteractiveGraph.getPointCoords(
|
|
2665
|
+
rubric.correct,
|
|
2666
|
+
component,
|
|
2667
|
+
);
|
|
2668
|
+
const guess = state.coords?.slice();
|
|
2669
|
+
correct = correct.slice();
|
|
2670
|
+
// Everything's already rounded so we shouldn't need to do an
|
|
2671
|
+
// eq() comparison but _.isEqual(0, -0) is false, so we'll use
|
|
2672
|
+
// eq() anyway. The sort should be fine because it'll stringify
|
|
2673
|
+
// it and -0 converted to a string is "0"
|
|
2674
|
+
guess?.sort();
|
|
2675
|
+
correct.sort();
|
|
2676
|
+
if (deepEq(guess, correct)) {
|
|
2677
|
+
return {
|
|
2678
|
+
type: "points",
|
|
2679
|
+
earned: 1,
|
|
2680
|
+
total: 1,
|
|
2681
|
+
message: null,
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
} else if (
|
|
2685
|
+
state.type === "polygon" &&
|
|
2686
|
+
rubric.correct.type === "polygon" &&
|
|
2687
|
+
state.coords != null
|
|
2688
|
+
) {
|
|
2689
|
+
const guess: Array<Coord> = state.coords?.slice(); // eslint-disable-line flowtype/no-mutable-array
|
|
2690
|
+
// $FlowFixMe[incompatible-type]
|
|
2691
|
+
// $FlowFixMe[prop-missing]
|
|
2692
|
+
const correct: Array<Coord> = rubric.correct.coords?.slice(); // eslint-disable-line flowtype/no-mutable-array
|
|
2693
|
+
|
|
2694
|
+
let match;
|
|
2695
|
+
if (rubric.correct.match === "similar") {
|
|
2696
|
+
match = similar(guess, correct, Number.POSITIVE_INFINITY);
|
|
2697
|
+
} else if (rubric.correct.match === "congruent") {
|
|
2698
|
+
match = similar(guess, correct, knumber.DEFAULT_TOLERANCE);
|
|
2699
|
+
} else if (rubric.correct.match === "approx") {
|
|
2700
|
+
match = similar(guess, correct, 0.1);
|
|
2701
|
+
} else {
|
|
2702
|
+
/* exact */
|
|
2703
|
+
guess.sort();
|
|
2704
|
+
correct.sort();
|
|
2705
|
+
match = deepEq(guess, correct);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
if (match) {
|
|
2709
|
+
return {
|
|
2710
|
+
type: "points",
|
|
2711
|
+
earned: 1,
|
|
2712
|
+
total: 1,
|
|
2713
|
+
message: null,
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
} else if (
|
|
2717
|
+
state.type === "segment" &&
|
|
2718
|
+
rubric.correct.type === "segment" &&
|
|
2719
|
+
state.coords != null
|
|
2720
|
+
) {
|
|
2721
|
+
let guess = state.coords.slice();
|
|
2722
|
+
// $FlowFixMe[prop-missing]
|
|
2723
|
+
let correct = rubric.correct.coords?.slice();
|
|
2724
|
+
guess = _.invoke(guess, "sort").sort();
|
|
2725
|
+
correct = _.invoke(correct, "sort").sort();
|
|
2726
|
+
if (deepEq(guess, correct)) {
|
|
2727
|
+
return {
|
|
2728
|
+
type: "points",
|
|
2729
|
+
earned: 1,
|
|
2730
|
+
total: 1,
|
|
2731
|
+
message: null,
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
} else if (
|
|
2735
|
+
state.type === "ray" &&
|
|
2736
|
+
rubric.correct.type === "ray" &&
|
|
2737
|
+
state.coords != null
|
|
2738
|
+
) {
|
|
2739
|
+
const guess = state.coords;
|
|
2740
|
+
const correct = rubric.correct.coords;
|
|
2741
|
+
if (
|
|
2742
|
+
// $FlowFixMe[incompatible-use]
|
|
2743
|
+
deepEq(guess[0], correct[0]) &&
|
|
2744
|
+
// $FlowFixMe[incompatible-use]
|
|
2745
|
+
collinear(correct[0], correct[1], guess[1])
|
|
2746
|
+
) {
|
|
2747
|
+
return {
|
|
2748
|
+
type: "points",
|
|
2749
|
+
earned: 1,
|
|
2750
|
+
total: 1,
|
|
2751
|
+
message: null,
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
} else if (
|
|
2755
|
+
state.type === "angle" &&
|
|
2756
|
+
rubric.correct.type === "angle"
|
|
2757
|
+
) {
|
|
2758
|
+
const guess = state.coords;
|
|
2759
|
+
const correct = rubric.correct.coords;
|
|
2760
|
+
|
|
2761
|
+
let match;
|
|
2762
|
+
if (rubric.correct.match === "congruent") {
|
|
2763
|
+
const angles = _.map([guess, correct], function (coords) {
|
|
2764
|
+
const angle = GraphUtils.findAngle(
|
|
2765
|
+
coords[2],
|
|
2766
|
+
coords[0],
|
|
2767
|
+
coords[1],
|
|
2768
|
+
);
|
|
2769
|
+
return (angle + 360) % 360;
|
|
2770
|
+
});
|
|
2771
|
+
match = eq(...angles);
|
|
2772
|
+
} else {
|
|
2773
|
+
/* exact */
|
|
2774
|
+
match =
|
|
2775
|
+
// $FlowFixMe[incompatible-use]
|
|
2776
|
+
deepEq(guess[1], correct[1]) &&
|
|
2777
|
+
// $FlowFixMe[incompatible-use]
|
|
2778
|
+
collinear(correct[1], correct[0], guess[0]) &&
|
|
2779
|
+
// $FlowFixMe[incompatible-use]
|
|
2780
|
+
collinear(correct[1], correct[2], guess[2]);
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
if (match) {
|
|
2784
|
+
return {
|
|
2785
|
+
type: "points",
|
|
2786
|
+
earned: 1,
|
|
2787
|
+
total: 1,
|
|
2788
|
+
message: null,
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
// The input wasn't correct, so check if it's a blank input or if it's
|
|
2795
|
+
// actually just wrong
|
|
2796
|
+
if (!hasValue || _.isEqual(state, rubric.graph)) {
|
|
2797
|
+
// We're where we started.
|
|
2798
|
+
return {
|
|
2799
|
+
type: "invalid",
|
|
2800
|
+
message: null,
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
return {
|
|
2804
|
+
type: "points",
|
|
2805
|
+
earned: 0,
|
|
2806
|
+
total: 1,
|
|
2807
|
+
message: null,
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
static getUserInputFromProps(props: Props): PerseusGraphType {
|
|
2812
|
+
return props.graph;
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
export default ({
|
|
2817
|
+
name: "interactive-graph",
|
|
2818
|
+
displayName: "Interactive graph",
|
|
2819
|
+
widget: InteractiveGraph,
|
|
2820
|
+
}: WidgetExports<typeof InteractiveGraph>);
|