@tldraw/editor 5.4.0-next.ef99ab47e5ce → 5.5.0-next.6d9e51e9f3f7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +119 -49
- package/dist-cjs/index.js +4 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +7 -2
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +6 -8
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/default-components/CanvasOverlays.js +1 -3
- package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +1 -1
- package/dist-cjs/lib/config/TLUserPreferences.js +6 -2
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/config/createTLStore.js +1 -1
- package/dist-cjs/lib/config/createTLStore.js.map +2 -2
- package/dist-cjs/lib/constants.js +2 -0
- package/dist-cjs/lib/constants.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +184 -296
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +7 -3
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -3
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +8 -8
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js +10 -6
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +2 -1
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +79 -6
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +7 -0
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +0 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +1 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js +7 -3
- package/dist-cjs/lib/editor/managers/ThemeManager/ThemeManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +6 -3
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +12 -1
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/OverlayManager.js +3 -7
- package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +0 -1
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -1
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js +1 -0
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -1
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
- package/dist-cjs/lib/editor/tools/StateNode.js +3 -2
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/globals/editors.js +57 -0
- package/dist-cjs/lib/globals/editors.js.map +7 -0
- package/dist-cjs/lib/globals/menus.js +9 -4
- package/dist-cjs/lib/globals/menus.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -0
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +39 -9
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useLocalStore.js +10 -6
- package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +1 -0
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePeerIds.js +2 -1
- package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
- package/dist-cjs/lib/license/LicenseManager.js +9 -2
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +16 -3
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/license/useLicenseManagerState.js +5 -4
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +2 -1
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +4 -3
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Edge2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +71 -26
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Point2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Point2d.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +2 -1
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/lib/utils/deepLinks.js +5 -1
- package/dist-cjs/lib/utils/deepLinks.js.map +2 -2
- package/dist-cjs/lib/utils/normalizeWheel.js +2 -0
- package/dist-cjs/lib/utils/normalizeWheel.js.map +2 -2
- package/dist-cjs/lib/utils/rotation.js +1 -9
- package/dist-cjs/lib/utils/rotation.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +17 -0
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
- package/dist-cjs/lib/utils/sync/alerts.js +1 -1
- package/dist-cjs/lib/utils/sync/alerts.js.map +1 -1
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +119 -49
- package/dist-esm/index.mjs +9 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +9 -4
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +6 -8
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +1 -3
- package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +1 -1
- package/dist-esm/lib/config/TLUserPreferences.mjs +6 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/config/createTLStore.mjs +1 -1
- package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
- package/dist-esm/lib/constants.mjs +2 -0
- package/dist-esm/lib/constants.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +193 -297
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +7 -3
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -3
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +9 -9
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs +10 -6
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +2 -1
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +81 -8
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +7 -0
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +0 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -3
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs +7 -3
- package/dist-esm/lib/editor/managers/ThemeManager/ThemeManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +6 -3
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +12 -1
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs +3 -7
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +0 -1
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -1
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs +1 -0
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -1
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs +3 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/globals/editors.mjs +37 -0
- package/dist-esm/lib/globals/editors.mjs.map +7 -0
- package/dist-esm/lib/globals/menus.mjs +9 -4
- package/dist-esm/lib/globals/menus.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -0
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +40 -10
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useLocalStore.mjs +11 -7
- package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +1 -0
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePeerIds.mjs +2 -1
- package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseManager.mjs +9 -2
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +16 -3
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/license/useLicenseManagerState.mjs +5 -4
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +2 -1
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +4 -3
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Edge2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +75 -27
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Point2d.mjs +1 -1
- package/dist-esm/lib/primitives/geometry/Point2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +2 -1
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/lib/utils/deepLinks.mjs +5 -1
- package/dist-esm/lib/utils/deepLinks.mjs.map +2 -2
- package/dist-esm/lib/utils/normalizeWheel.mjs +2 -0
- package/dist-esm/lib/utils/normalizeWheel.mjs.map +2 -2
- package/dist-esm/lib/utils/rotation.mjs +1 -9
- package/dist-esm/lib/utils/rotation.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +17 -0
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/alerts.mjs +1 -1
- package/dist-esm/lib/utils/sync/alerts.mjs.map +1 -1
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +13 -7
- package/src/index.ts +6 -1
- package/src/lib/TldrawEditor.tsx +16 -4
- package/src/lib/components/MenuClickCapture.tsx +19 -18
- package/src/lib/components/default-components/CanvasOverlays.tsx +1 -3
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
- package/src/lib/config/TLUserPreferences.test.ts +18 -1
- package/src/lib/config/TLUserPreferences.ts +8 -4
- package/src/lib/config/createTLStore.ts +1 -1
- package/src/lib/constants.ts +8 -0
- package/src/lib/editor/Editor.ts +273 -386
- package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +7 -3
- package/src/lib/editor/managers/ClickManager/ClickManager.ts +5 -3
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +9 -9
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +35 -4
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.ts +13 -7
- package/src/lib/editor/managers/FontManager/FontManager.ts +2 -1
- package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +199 -3
- package/src/lib/editor/managers/InputsManager/InputsManager.ts +128 -9
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +12 -1
- package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +0 -2
- package/src/lib/editor/managers/TextManager/TextManager.ts +2 -6
- package/src/lib/editor/managers/ThemeManager/ThemeManager.test.ts +53 -0
- package/src/lib/editor/managers/ThemeManager/ThemeManager.ts +9 -3
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
- package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +64 -3
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +18 -1
- package/src/lib/editor/overlays/OverlayManager.ts +3 -7
- package/src/lib/editor/overlays/strokeShapeIndicators.ts +0 -1
- package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +5 -1
- package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +3 -0
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -1
- package/src/lib/editor/tools/StateNode.test.ts +64 -0
- package/src/lib/editor/tools/StateNode.ts +12 -2
- package/src/lib/globals/editors.test.ts +76 -0
- package/src/lib/globals/editors.ts +53 -0
- package/src/lib/globals/menus.test.ts +33 -0
- package/src/lib/globals/menus.ts +11 -4
- package/src/lib/hooks/useCanvasEvents.ts +2 -0
- package/src/lib/hooks/useDocumentEvents.ts +60 -16
- package/src/lib/hooks/useLocalStore.ts +16 -8
- package/src/lib/hooks/usePassThroughWheelEvents.ts +3 -0
- package/src/lib/hooks/usePeerIds.ts +2 -1
- package/src/lib/license/LicenseManager.test.ts +160 -0
- package/src/lib/license/LicenseManager.ts +13 -4
- package/src/lib/license/LicenseProvider.tsx +24 -3
- package/src/lib/license/useLicenseManagerState.ts +8 -6
- package/src/lib/primitives/Box.test.ts +9 -1
- package/src/lib/primitives/Box.ts +3 -1
- package/src/lib/primitives/Vec.test.ts +10 -0
- package/src/lib/primitives/Vec.ts +4 -4
- package/src/lib/primitives/geometry/Arc2d.test.ts +235 -3
- package/src/lib/primitives/geometry/Arc2d.ts +1 -1
- package/src/lib/primitives/geometry/CubicBezier2d.test.ts +186 -3
- package/src/lib/primitives/geometry/CubicBezier2d.ts +1 -1
- package/src/lib/primitives/geometry/CubicSpline2d.test.ts +160 -3
- package/src/lib/primitives/geometry/Edge2d.test.ts +21 -3
- package/src/lib/primitives/geometry/Edge2d.ts +1 -1
- package/src/lib/primitives/geometry/Ellipse2d.test.ts +243 -3
- package/src/lib/primitives/geometry/Geometry2d.test.ts +553 -1
- package/src/lib/primitives/geometry/Group2d.test.ts +419 -3
- package/src/lib/primitives/geometry/Group2d.ts +81 -34
- package/src/lib/primitives/geometry/Point2d.test.ts +14 -3
- package/src/lib/primitives/geometry/Point2d.ts +1 -1
- package/src/lib/primitives/geometry/Polyline2d.test.ts +207 -0
- package/src/lib/primitives/geometry/Stadium2d.test.ts +191 -3
- package/src/lib/primitives/utils.test.ts +10 -1
- package/src/lib/primitives/utils.ts +4 -1
- package/src/lib/test/useCanvasEvents.test.tsx +74 -0
- package/src/lib/test/useDocumentEvents.test.tsx +107 -0
- package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
- package/src/lib/utils/deepLinks.test.ts +15 -0
- package/src/lib/utils/deepLinks.ts +7 -1
- package/src/lib/utils/normalizeWheel.test.ts +39 -0
- package/src/lib/utils/normalizeWheel.ts +6 -0
- package/src/lib/utils/rotation.ts +4 -12
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +56 -0
- package/src/lib/utils/sync/TLLocalSyncClient.ts +24 -0
- package/src/lib/utils/sync/alerts.ts +1 -1
- package/src/version.ts +3 -3
- package/src/lib/primitives/geometry/Polyline.test.ts +0 -5
|
@@ -1,5 +1,237 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Vec } from '../Vec'
|
|
2
|
+
import { Arc2d } from './Arc2d'
|
|
3
|
+
|
|
4
|
+
// A quarter circle of radius 10 centred at the origin, from (10, 0) clockwise (in screen
|
|
5
|
+
// space, i.e. with increasing angle) to (0, 10).
|
|
6
|
+
const quarter = () =>
|
|
7
|
+
new Arc2d({
|
|
8
|
+
center: new Vec(0, 0),
|
|
9
|
+
start: new Vec(10, 0),
|
|
10
|
+
end: new Vec(0, 10),
|
|
11
|
+
sweepFlag: 1,
|
|
12
|
+
largeArcFlag: 0,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// The complementary three-quarter arc between the same points, going the other way round.
|
|
16
|
+
const threeQuarter = () =>
|
|
17
|
+
new Arc2d({
|
|
18
|
+
center: new Vec(0, 0),
|
|
19
|
+
start: new Vec(10, 0),
|
|
20
|
+
end: new Vec(0, 10),
|
|
21
|
+
sweepFlag: 0,
|
|
22
|
+
largeArcFlag: 1,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
function expectVec(actual: Vec, expected: { x: number; y: number }) {
|
|
26
|
+
expect(actual.x).toBeCloseTo(expected.x)
|
|
27
|
+
expect(actual.y).toBeCloseTo(expected.y)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('Arc2d construction', () => {
|
|
31
|
+
it('is never closed or filled', () => {
|
|
32
|
+
expect(quarter()).toMatchObject({ isClosed: false, isFilled: false })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('throws when start and end are the same point', () => {
|
|
36
|
+
expect(
|
|
37
|
+
() =>
|
|
38
|
+
new Arc2d({
|
|
39
|
+
center: new Vec(0, 0),
|
|
40
|
+
start: new Vec(10, 0),
|
|
41
|
+
end: new Vec(10, 0),
|
|
42
|
+
sweepFlag: 1,
|
|
43
|
+
largeArcFlag: 0,
|
|
44
|
+
})
|
|
45
|
+
).toThrow('Arc must have different start and end points.')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('Arc2d.getLength', () => {
|
|
50
|
+
it('is the arc measure times the radius', () => {
|
|
51
|
+
expect(quarter().length).toBeCloseTo((Math.PI / 2) * 10)
|
|
52
|
+
expect(threeQuarter().length).toBeCloseTo(((3 * Math.PI) / 2) * 10)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('is half a circumference for a semicircle in either direction', () => {
|
|
56
|
+
const semi = new Arc2d({
|
|
57
|
+
center: new Vec(25, 25),
|
|
58
|
+
start: new Vec(25, 50),
|
|
59
|
+
end: new Vec(25, 0),
|
|
60
|
+
sweepFlag: 1,
|
|
61
|
+
largeArcFlag: 1,
|
|
62
|
+
})
|
|
63
|
+
expect(semi.length).toBeCloseTo(Math.PI * 25)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('Arc2d.getVertices', () => {
|
|
68
|
+
it('samples at least eight segments from start to end', () => {
|
|
69
|
+
const { vertices } = quarter()
|
|
70
|
+
expect(vertices.length).toBe(9)
|
|
71
|
+
expectVec(vertices[0], { x: 10, y: 0 })
|
|
72
|
+
expectVec(vertices[4], { x: 10 / Math.SQRT2, y: 10 / Math.SQRT2 })
|
|
73
|
+
expectVec(vertices[8], { x: 0, y: 10 })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('keeps every vertex on the circle', () => {
|
|
77
|
+
for (const v of quarter().vertices) {
|
|
78
|
+
expect(Vec.Dist(v, new Vec(0, 0))).toBeCloseTo(10)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('walks the long way round for a large arc', () => {
|
|
83
|
+
const { vertices } = threeQuarter()
|
|
84
|
+
expect(vertices.length).toBe(9)
|
|
85
|
+
expectVec(vertices[0], { x: 10, y: 0 })
|
|
86
|
+
// the midpoint of the long arc is opposite the midpoint of the short arc
|
|
87
|
+
expectVec(vertices[4], { x: -10 / Math.SQRT2, y: -10 / Math.SQRT2 })
|
|
88
|
+
expectVec(vertices[8], { x: 0, y: 10 })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('samples more vertices for longer arcs', () => {
|
|
92
|
+
const big = new Arc2d({
|
|
93
|
+
center: new Vec(0, 0),
|
|
94
|
+
start: new Vec(100, 0),
|
|
95
|
+
end: new Vec(0, 100),
|
|
96
|
+
sweepFlag: 1,
|
|
97
|
+
largeArcFlag: 0,
|
|
98
|
+
})
|
|
99
|
+
// length is 50π ≈ 157, one vertex per 20 units -> 8 segments
|
|
100
|
+
expect(big.vertices.length).toBe(9)
|
|
101
|
+
const bigger = new Arc2d({
|
|
102
|
+
center: new Vec(0, 0),
|
|
103
|
+
start: new Vec(200, 0),
|
|
104
|
+
end: new Vec(0, 200),
|
|
105
|
+
sweepFlag: 1,
|
|
106
|
+
largeArcFlag: 0,
|
|
107
|
+
})
|
|
108
|
+
// length is 100π ≈ 314 -> 16 segments
|
|
109
|
+
expect(bigger.vertices.length).toBe(17)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('Arc2d.bounds', () => {
|
|
114
|
+
it('is the box around the sampled vertices', () => {
|
|
115
|
+
const { bounds } = quarter()
|
|
116
|
+
expect(bounds.x).toBeCloseTo(0)
|
|
117
|
+
expect(bounds.y).toBeCloseTo(0)
|
|
118
|
+
expect(bounds.w).toBeCloseTo(10)
|
|
119
|
+
expect(bounds.h).toBeCloseTo(10)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('Arc2d.nearestPoint', () => {
|
|
124
|
+
it('projects radially onto the arc when the angle is within the sweep', () => {
|
|
125
|
+
expectVec(quarter().nearestPoint(new Vec(5, 5)), { x: 10 / Math.SQRT2, y: 10 / Math.SQRT2 })
|
|
126
|
+
expectVec(quarter().nearestPoint(new Vec(20, 20)), {
|
|
127
|
+
x: 10 / Math.SQRT2,
|
|
128
|
+
y: 10 / Math.SQRT2,
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('returns the start point for angles before the arc', () => {
|
|
133
|
+
expect(quarter().nearestPoint(new Vec(20, -5))).toMatchObject({ x: 10, y: 0 })
|
|
134
|
+
expect(quarter().nearestPoint(new Vec(20, 0))).toMatchObject({ x: 10, y: 0 })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('returns the end point for angles after the arc', () => {
|
|
138
|
+
expect(quarter().nearestPoint(new Vec(-5, 20))).toMatchObject({ x: 0, y: 10 })
|
|
139
|
+
expect(quarter().nearestPoint(new Vec(0, 20))).toMatchObject({ x: 0, y: 10 })
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('returns the start point for the centre itself', () => {
|
|
143
|
+
expect(quarter().nearestPoint(new Vec(0, 0))).toMatchObject({ x: 10, y: 0 })
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('projects onto the long arc', () => {
|
|
147
|
+
expectVec(threeQuarter().nearestPoint(new Vec(-20, 0)), { x: -10, y: 0 })
|
|
148
|
+
expectVec(threeQuarter().nearestPoint(new Vec(0, -3)), { x: 0, y: -10 })
|
|
149
|
+
// a point in the gap of the long arc snaps to the nearer end point
|
|
150
|
+
expect(threeQuarter().nearestPoint(new Vec(20, 5))).toMatchObject({ x: 10, y: 0 })
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('Arc2d.distanceToPoint', () => {
|
|
155
|
+
it('measures to the arc or its end points', () => {
|
|
156
|
+
expect(quarter().distanceToPoint(new Vec(20, 20))).toBeCloseTo(Math.hypot(20, 20) - 10)
|
|
157
|
+
expect(quarter().distanceToPoint(new Vec(20, 0))).toBe(10)
|
|
158
|
+
expect(quarter().distanceToPoint(new Vec(0, 0))).toBe(10)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('is never negative because an arc has no interior', () => {
|
|
162
|
+
expect(quarter().distanceToPoint(new Vec(1, 1), true)).toBeGreaterThan(0)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('Arc2d.hitTestPoint', () => {
|
|
167
|
+
it('hits points on the arc within the margin', () => {
|
|
168
|
+
expect(quarter().hitTestPoint(new Vec(10 / Math.SQRT2, 10 / Math.SQRT2), 0.001)).toBe(true)
|
|
169
|
+
expect(quarter().hitTestPoint(new Vec(10, 0))).toBe(true)
|
|
170
|
+
expect(quarter().hitTestPoint(new Vec(5, 5), 3)).toBe(true)
|
|
171
|
+
expect(quarter().hitTestPoint(new Vec(5, 5), 2)).toBe(false)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('does not hit the sector inside the arc even with hitInside', () => {
|
|
175
|
+
expect(quarter().hitTestPoint(new Vec(5, 5), 0, true)).toBe(false)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('Arc2d.hitTestLineSegment', () => {
|
|
180
|
+
it('hits a segment crossing the arc', () => {
|
|
181
|
+
expect(quarter().hitTestLineSegment(new Vec(5, 0), new Vec(5, 20))).toBe(true)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('misses a segment crossing the circle outside the arc', () => {
|
|
185
|
+
// crosses the circle at (-5, 8.66), which is at 120° and not on the quarter arc
|
|
186
|
+
expect(quarter().hitTestLineSegment(new Vec(-5, 0), new Vec(-5, 20))).toBe(false)
|
|
187
|
+
// but it is on the complementary arc
|
|
188
|
+
expect(threeQuarter().hitTestLineSegment(new Vec(-5, 0), new Vec(-5, 20))).toBe(true)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('misses a segment that does not reach the circle', () => {
|
|
192
|
+
expect(quarter().hitTestLineSegment(new Vec(20, 20), new Vec(30, 30))).toBe(false)
|
|
193
|
+
expect(quarter().hitTestLineSegment(new Vec(1, 1), new Vec(2, 2))).toBe(false)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Locks in current behaviour, see #10555.
|
|
197
|
+
it('hits crossings just beyond the end points (known quirk)', () => {
|
|
198
|
+
// (5, -8.66) is at -60°, off the quarter arc, but getPointInArcT clamps it to the start
|
|
199
|
+
expect(quarter().hitTestLineSegment(new Vec(5, -20), new Vec(5, 0))).toBe(true)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('checks both directions of the long arc', () => {
|
|
203
|
+
expect(threeQuarter().hitTestLineSegment(new Vec(5, -20), new Vec(5, 0))).toBe(true)
|
|
204
|
+
expect(threeQuarter().hitTestLineSegment(new Vec(5, 0), new Vec(5, 20))).toBe(false)
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('Arc2d.getSvgPathData', () => {
|
|
209
|
+
it('emits a move and an arc command with the flags', () => {
|
|
210
|
+
expect(quarter().getSvgPathData()).toBe('M10, 0 A10 10 0 0 1 0, 10')
|
|
211
|
+
expect(threeQuarter().getSvgPathData()).toBe('M10, 0 A10 10 0 1 0 0, 10')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('omits the move command when not first', () => {
|
|
215
|
+
expect(quarter().getSvgPathData(false)).toBe(' A10 10 0 0 1 0, 10')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('rounds coordinates to two decimals', () => {
|
|
219
|
+
const arc = new Arc2d({
|
|
220
|
+
center: new Vec(0, 0),
|
|
221
|
+
start: new Vec(10, 0),
|
|
222
|
+
end: new Vec(10 / Math.SQRT2, 10 / Math.SQRT2),
|
|
223
|
+
sweepFlag: 1,
|
|
224
|
+
largeArcFlag: 0,
|
|
225
|
+
})
|
|
226
|
+
expect(arc.getSvgPathData()).toBe('M10, 0 A10 10 0 0 1 7.07, 7.07')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('does not mutate the stored points', () => {
|
|
230
|
+
const start = new Vec(10, 0)
|
|
231
|
+
const end = new Vec(10 / Math.SQRT2, 10 / Math.SQRT2)
|
|
232
|
+
const arc = new Arc2d({ center: new Vec(0, 0), start, end, sweepFlag: 1, largeArcFlag: 0 })
|
|
233
|
+
arc.getSvgPathData()
|
|
234
|
+
expect(start).toMatchObject({ x: 10, y: 0 })
|
|
235
|
+
expect(end).toMatchObject({ x: 10 / Math.SQRT2, y: 10 / Math.SQRT2 })
|
|
4
236
|
})
|
|
5
237
|
})
|
|
@@ -105,7 +105,7 @@ export class Arc2d extends Geometry2d {
|
|
|
105
105
|
_largeArcFlag: largeArcFlag,
|
|
106
106
|
_sweepFlag: sweepFlag,
|
|
107
107
|
} = this
|
|
108
|
-
return `${first ? `M${start
|
|
108
|
+
return `${first ? `M${start}` : ``} A${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${end}`
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
override getLength() {
|
|
@@ -1,5 +1,188 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Vec } from '../Vec'
|
|
2
|
+
import { CubicBezier2d } from './CubicBezier2d'
|
|
3
|
+
import { Polyline2d } from './Polyline2d'
|
|
4
|
+
|
|
5
|
+
// A symmetric arch from (0, 0) to (100, 0) whose control points pull it up to y = 75 at t = 0.5
|
|
6
|
+
const arch = (resolution?: number) =>
|
|
7
|
+
new CubicBezier2d({
|
|
8
|
+
start: new Vec(0, 0),
|
|
9
|
+
cp1: new Vec(0, 100),
|
|
10
|
+
cp2: new Vec(100, 100),
|
|
11
|
+
end: new Vec(100, 0),
|
|
12
|
+
resolution,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// Control points on the line y = x, so the curve is the straight segment (0,0) -> (30,30)
|
|
16
|
+
const straight = () =>
|
|
17
|
+
new CubicBezier2d({
|
|
18
|
+
start: new Vec(0, 0),
|
|
19
|
+
cp1: new Vec(10, 10),
|
|
20
|
+
cp2: new Vec(20, 20),
|
|
21
|
+
end: new Vec(30, 30),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('CubicBezier2d construction', () => {
|
|
25
|
+
it('is an open, unfilled polyline', () => {
|
|
26
|
+
const b = arch()
|
|
27
|
+
expect(b).toBeInstanceOf(Polyline2d)
|
|
28
|
+
expect(b).toMatchObject({ isClosed: false, isFilled: false })
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('CubicBezier2d.GetAtT', () => {
|
|
33
|
+
it('returns the end points at t = 0 and t = 1', () => {
|
|
34
|
+
expect(CubicBezier2d.GetAtT(arch(), 0)).toMatchObject({ x: 0, y: 0 })
|
|
35
|
+
expect(CubicBezier2d.GetAtT(arch(), 1)).toMatchObject({ x: 100, y: 0 })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('returns the weighted control point average in between', () => {
|
|
39
|
+
// 0.125 a + 0.375 b + 0.375 c + 0.125 d
|
|
40
|
+
expect(CubicBezier2d.GetAtT(arch(), 0.5)).toMatchObject({ x: 50, y: 75 })
|
|
41
|
+
const p = CubicBezier2d.GetAtT(arch(), 0.4)
|
|
42
|
+
expect(p.x).toBeCloseTo(35.2)
|
|
43
|
+
expect(p.y).toBeCloseTo(72)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('CubicBezier2d.getVertices', () => {
|
|
48
|
+
it('samples resolution + 1 points along the curve', () => {
|
|
49
|
+
const { vertices } = arch()
|
|
50
|
+
expect(vertices.length).toBe(11)
|
|
51
|
+
expect(vertices[0]).toMatchObject({ x: 0, y: 0 })
|
|
52
|
+
expect(vertices[5]).toMatchObject({ x: 50, y: 75 })
|
|
53
|
+
expect(vertices[10]).toMatchObject({ x: 100, y: 0 })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('honours a custom resolution', () => {
|
|
57
|
+
const { vertices } = arch(4)
|
|
58
|
+
expect(vertices.length).toBe(5)
|
|
59
|
+
expect(vertices[2]).toMatchObject({ x: 50, y: 75 })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('is symmetric for symmetric control points', () => {
|
|
63
|
+
const { vertices } = arch()
|
|
64
|
+
for (let i = 0; i < 5; i++) {
|
|
65
|
+
expect(vertices[i].x).toBeCloseTo(100 - vertices[10 - i].x)
|
|
66
|
+
expect(vertices[i].y).toBeCloseTo(vertices[10 - i].y)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('CubicBezier2d.bounds', () => {
|
|
72
|
+
it('spans the sampled vertices', () => {
|
|
73
|
+
expect(arch().bounds).toMatchObject({ x: 0, y: 0, w: 100, h: 75 })
|
|
74
|
+
expect(arch().center).toMatchObject({ x: 50, y: 37.5 })
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('CubicBezier2d.getLength', () => {
|
|
79
|
+
it('equals the chord for a straight curve', () => {
|
|
80
|
+
expect(straight().length).toBeCloseTo(Math.hypot(30, 30))
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('sums sampled chords at the given precision', () => {
|
|
84
|
+
const b = arch()
|
|
85
|
+
expect(b.getLength(undefined, 1)).toBe(100)
|
|
86
|
+
expect(b.getLength(undefined, 2)).toBeCloseTo(2 * Math.hypot(50, 75))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('gets longer with finer sampling of a curved path', () => {
|
|
90
|
+
const b = arch()
|
|
91
|
+
expect(b.getLength(undefined, 2)).toBeLessThan(b.getLength(undefined, 32))
|
|
92
|
+
expect(b.length).toBe(b.getLength(undefined, 32))
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('CubicBezier2d.nearestPoint', () => {
|
|
97
|
+
it('returns the peak vertex for a point above the arch', () => {
|
|
98
|
+
expect(arch().nearestPoint(new Vec(50, 100))).toMatchObject({ x: 50, y: 75 })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('clamps to the end points', () => {
|
|
102
|
+
expect(arch().nearestPoint(new Vec(-10, 0))).toMatchObject({ x: 0, y: 0 })
|
|
103
|
+
expect(arch().nearestPoint(new Vec(110, 0))).toMatchObject({ x: 100, y: 0 })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('projects onto the straight curve', () => {
|
|
107
|
+
const p = straight().nearestPoint(new Vec(20, 10))
|
|
108
|
+
expect(p.x).toBeCloseTo(15)
|
|
109
|
+
expect(p.y).toBeCloseTo(15)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe('CubicBezier2d.distanceToPoint', () => {
|
|
114
|
+
it('measures to the sampled polyline', () => {
|
|
115
|
+
expect(arch().distanceToPoint(new Vec(50, 100))).toBe(25)
|
|
116
|
+
expect(arch().distanceToPoint(new Vec(-10, 0))).toBe(10)
|
|
117
|
+
expect(straight().distanceToPoint(new Vec(20, 10))).toBeCloseTo(Math.hypot(5, 5))
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('is always positive, even under the arch with hitInside', () => {
|
|
121
|
+
expect(arch().distanceToPoint(new Vec(50, 30), true)).toBeGreaterThan(0)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('CubicBezier2d.hitTestPoint', () => {
|
|
126
|
+
it('hits points on the curve', () => {
|
|
127
|
+
expect(arch().hitTestPoint(new Vec(50, 75))).toBe(true)
|
|
128
|
+
expect(arch().hitTestPoint(new Vec(0, 0))).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('respects the margin', () => {
|
|
132
|
+
expect(arch().hitTestPoint(new Vec(50, 80), 5)).toBe(true)
|
|
133
|
+
expect(arch().hitTestPoint(new Vec(50, 80), 4)).toBe(false)
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('does not hit the area under the arch', () => {
|
|
137
|
+
expect(arch().hitTestPoint(new Vec(50, 30))).toBe(false)
|
|
138
|
+
expect(arch().hitTestPoint(new Vec(50, 30), 0, true)).toBe(false)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('CubicBezier2d.hitTestLineSegment', () => {
|
|
143
|
+
it('hits a segment crossing the curve', () => {
|
|
144
|
+
expect(arch().hitTestLineSegment(new Vec(50, 0), new Vec(50, 100))).toBe(true)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('misses a segment away from the curve', () => {
|
|
148
|
+
expect(arch().hitTestLineSegment(new Vec(200, 0), new Vec(200, 100))).toBe(false)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('respects the distance', () => {
|
|
152
|
+
expect(arch().hitTestLineSegment(new Vec(0, 80), new Vec(100, 80), 5)).toBe(true)
|
|
153
|
+
expect(arch().hitTestLineSegment(new Vec(0, 80), new Vec(100, 80), 4)).toBe(false)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('CubicBezier2d.getSvgPathData', () => {
|
|
158
|
+
it('emits a move and a cubic command', () => {
|
|
159
|
+
expect(arch().getSvgPathData()).toBe('M 0, 0 C0, 100 100, 100 100, 0')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('omits the move command when not first', () => {
|
|
163
|
+
expect(arch().getSvgPathData(false)).toBe(' C0, 100 100, 100 100, 0')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('rounds coordinates to two decimals', () => {
|
|
167
|
+
const b = new CubicBezier2d({
|
|
168
|
+
start: new Vec(0.123, 0),
|
|
169
|
+
cp1: new Vec(1.006, 2),
|
|
170
|
+
cp2: new Vec(3, 4.999),
|
|
171
|
+
end: new Vec(10, 10),
|
|
172
|
+
})
|
|
173
|
+
expect(b.getSvgPathData()).toBe('M 0.12, 0 C1.01, 2 3, 5 10, 10')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('does not mutate the stored points', () => {
|
|
177
|
+
const start = new Vec(0.123, 0)
|
|
178
|
+
const cp1 = new Vec(1.006, 2)
|
|
179
|
+
const cp2 = new Vec(3, 4.999)
|
|
180
|
+
const end = new Vec(10, 10.005)
|
|
181
|
+
const b = new CubicBezier2d({ start, cp1, cp2, end })
|
|
182
|
+
b.getSvgPathData()
|
|
183
|
+
expect(start).toMatchObject({ x: 0.123, y: 0 })
|
|
184
|
+
expect(cp1).toMatchObject({ x: 1.006, y: 2 })
|
|
185
|
+
expect(cp2).toMatchObject({ x: 3, y: 4.999 })
|
|
186
|
+
expect(end).toMatchObject({ x: 10, y: 10.005 })
|
|
4
187
|
})
|
|
5
188
|
})
|
|
@@ -81,7 +81,7 @@ export class CubicBezier2d extends Polyline2d {
|
|
|
81
81
|
|
|
82
82
|
getSvgPathData(first = true) {
|
|
83
83
|
const { _a: a, _b: b, _c: c, _d: d } = this
|
|
84
|
-
return `${first ? `M ${a
|
|
84
|
+
return `${first ? `M ${a} ` : ``} C${b} ${c} ${d}`
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
static GetAtT(segment: CubicBezier2d, t: number) {
|
|
@@ -1,5 +1,162 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Vec } from '../Vec'
|
|
2
|
+
import { CubicBezier2d } from './CubicBezier2d'
|
|
3
|
+
import { CubicSpline2d } from './CubicSpline2d'
|
|
4
|
+
|
|
5
|
+
// Three collinear points: the spline is the straight line from (0, 0) to (200, 0)
|
|
6
|
+
const line = () => new CubicSpline2d({ points: [new Vec(0, 0), new Vec(100, 0), new Vec(200, 0)] })
|
|
7
|
+
|
|
8
|
+
// A symmetric peak: (0, 0) up to (100, 100) and back down to (200, 0)
|
|
9
|
+
const peak = () =>
|
|
10
|
+
new CubicSpline2d({ points: [new Vec(0, 0), new Vec(100, 100), new Vec(200, 0)] })
|
|
11
|
+
|
|
12
|
+
function expectVec(actual: Vec, expected: { x: number; y: number }) {
|
|
13
|
+
expect(actual.x).toBeCloseTo(expected.x)
|
|
14
|
+
expect(actual.y).toBeCloseTo(expected.y)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('CubicSpline2d construction', () => {
|
|
18
|
+
it('is never closed or filled', () => {
|
|
19
|
+
expect(line()).toMatchObject({ isClosed: false, isFilled: false })
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('CubicSpline2d.segments', () => {
|
|
24
|
+
it('creates one bezier per pair of consecutive points', () => {
|
|
25
|
+
const { segments } = line()
|
|
26
|
+
expect(segments.length).toBe(2)
|
|
27
|
+
expect(segments[0]).toBeInstanceOf(CubicBezier2d)
|
|
28
|
+
expect(segments[0].vertices[0]).toMatchObject({ x: 0, y: 0 })
|
|
29
|
+
expect(segments[0].vertices[10]).toMatchObject({ x: 100, y: 0 })
|
|
30
|
+
expect(segments[1].vertices[0]).toMatchObject({ x: 100, y: 0 })
|
|
31
|
+
expect(segments[1].vertices[10]).toMatchObject({ x: 200, y: 0 })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('uses the end points as control points at the ends and Catmull-Rom style tangents between', () => {
|
|
35
|
+
const { segments } = peak()
|
|
36
|
+
// cp1 of the first segment is the first point; cp2 is pulled back from the second point
|
|
37
|
+
// along the chord (p3 - p1) / 6 * 1.25
|
|
38
|
+
expect(segments[0].getSvgPathData()).toBe('M 0, 0 C0, 0 58.33, 100 100, 100')
|
|
39
|
+
expect(segments[1].getSvgPathData()).toBe('M 100, 100 C141.67, 100 200, 0 200, 0')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('caches the segments', () => {
|
|
43
|
+
const s = line()
|
|
44
|
+
expect(s.segments).toBe(s.segments)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('creates a single straight segment for two points', () => {
|
|
48
|
+
const s = new CubicSpline2d({ points: [new Vec(0, 0), new Vec(100, 0)] })
|
|
49
|
+
expect(s.segments.length).toBe(1)
|
|
50
|
+
expect(s.segments[0].getSvgPathData()).toBe('M 0, 0 C0, 0 100, 0 100, 0')
|
|
51
|
+
expect(s.length).toBeCloseTo(100)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('has no segments for a single point', () => {
|
|
55
|
+
const s = new CubicSpline2d({ points: [new Vec(5, 5)] })
|
|
56
|
+
expect(s.segments).toEqual([])
|
|
57
|
+
expect(s.vertices).toEqual([new Vec(5, 5)])
|
|
58
|
+
expect(s.length).toBe(0)
|
|
59
|
+
expect(() => s.nearestPoint(new Vec(0, 0))).toThrow('nearest point not found')
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('CubicSpline2d.getVertices', () => {
|
|
64
|
+
it('concatenates the segment vertices and appends the final point', () => {
|
|
65
|
+
const { vertices } = line()
|
|
66
|
+
expect(vertices.length).toBe(11 + 11 + 1)
|
|
67
|
+
expect(vertices[0]).toMatchObject({ x: 0, y: 0 })
|
|
68
|
+
expect(vertices[10]).toMatchObject({ x: 100, y: 0 })
|
|
69
|
+
expect(vertices[11]).toMatchObject({ x: 100, y: 0 })
|
|
70
|
+
expect(vertices[22]).toMatchObject({ x: 200, y: 0 })
|
|
71
|
+
for (const v of vertices) expect(v.y).toBe(0)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('passes through every control point', () => {
|
|
75
|
+
const { vertices } = peak()
|
|
76
|
+
expect(vertices[0]).toMatchObject({ x: 0, y: 0 })
|
|
77
|
+
expect(vertices[10]).toMatchObject({ x: 100, y: 100 })
|
|
78
|
+
expect(vertices[22]).toMatchObject({ x: 200, y: 0 })
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('CubicSpline2d.bounds', () => {
|
|
83
|
+
it('spans the vertices', () => {
|
|
84
|
+
expect(line().bounds).toMatchObject({ x: 0, y: 0, w: 200, h: 0 })
|
|
85
|
+
expect(peak().bounds).toMatchObject({ x: 0, y: 0, w: 200, h: 100 })
|
|
86
|
+
expect(peak().center).toMatchObject({ x: 100, y: 50 })
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('CubicSpline2d.getLength', () => {
|
|
91
|
+
it('sums the segment lengths', () => {
|
|
92
|
+
const s = line()
|
|
93
|
+
expect(s.length).toBeCloseTo(200)
|
|
94
|
+
expect(s.length).toBeCloseTo(s.segments[0].length + s.segments[1].length)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('is longer than the chord for a curved spline', () => {
|
|
98
|
+
expect(peak().length).toBeGreaterThan(2 * Math.hypot(100, 100))
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('CubicSpline2d.nearestPoint', () => {
|
|
103
|
+
it('projects onto the nearest segment', () => {
|
|
104
|
+
expectVec(line().nearestPoint(new Vec(50, 10)), { x: 50, y: 0 })
|
|
105
|
+
expectVec(line().nearestPoint(new Vec(150, -10)), { x: 150, y: 0 })
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('clamps to the end points', () => {
|
|
109
|
+
expect(line().nearestPoint(new Vec(-10, 0))).toMatchObject({ x: 0, y: 0 })
|
|
110
|
+
expect(line().nearestPoint(new Vec(210, 5))).toMatchObject({ x: 200, y: 0 })
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('returns the shared point where two segments meet', () => {
|
|
114
|
+
expect(peak().nearestPoint(new Vec(100, 120))).toMatchObject({ x: 100, y: 100 })
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('CubicSpline2d.distanceToPoint', () => {
|
|
119
|
+
it('is the distance to the nearest segment', () => {
|
|
120
|
+
expect(line().distanceToPoint(new Vec(50, 10))).toBeCloseTo(10)
|
|
121
|
+
expect(line().distanceToPoint(new Vec(150, -10))).toBeCloseTo(10)
|
|
122
|
+
expect(peak().distanceToPoint(new Vec(100, 120))).toBe(20)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('is never negative', () => {
|
|
126
|
+
expect(peak().distanceToPoint(new Vec(100, 50), true)).toBeGreaterThan(0)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('CubicSpline2d.hitTestPoint', () => {
|
|
131
|
+
it('hits points on the spline within the margin', () => {
|
|
132
|
+
expect(line().hitTestPoint(new Vec(50, 0), 0.001)).toBe(true)
|
|
133
|
+
expect(line().hitTestPoint(new Vec(50, 10), 10)).toBe(true)
|
|
134
|
+
expect(line().hitTestPoint(new Vec(50, 10), 9)).toBe(false)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('does not hit the area under a peak', () => {
|
|
138
|
+
expect(peak().hitTestPoint(new Vec(100, 50), 0, true)).toBe(false)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('CubicSpline2d.hitTestLineSegment', () => {
|
|
143
|
+
it('hits a segment crossing either bezier', () => {
|
|
144
|
+
expect(line().hitTestLineSegment(new Vec(50, -10), new Vec(50, 10))).toBe(true)
|
|
145
|
+
expect(line().hitTestLineSegment(new Vec(150, -10), new Vec(150, 10))).toBe(true)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('misses a segment that does not reach the spline', () => {
|
|
149
|
+
expect(line().hitTestLineSegment(new Vec(150, 5), new Vec(150, 10))).toBe(false)
|
|
150
|
+
expect(line().hitTestLineSegment(new Vec(250, -10), new Vec(250, 10))).toBe(false)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('CubicSpline2d.getSvgPathData', () => {
|
|
155
|
+
it('chains the segment paths with a single move command', () => {
|
|
156
|
+
expect(line().getSvgPathData()).toBe('M 0, 0 C0, 0 58.33, 0 100, 0 C141.67, 0 200, 0 200, 0')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('is empty for a single point', () => {
|
|
160
|
+
expect(new CubicSpline2d({ points: [new Vec(5, 5)] }).getSvgPathData()).toBe('')
|
|
4
161
|
})
|
|
5
162
|
})
|
|
@@ -1,5 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Vec } from '../Vec'
|
|
2
|
+
import { Edge2d } from './Edge2d'
|
|
3
|
+
|
|
4
|
+
describe('Edge2d.getSvgPathData', () => {
|
|
5
|
+
it('emits a move and a line command, rounded to two decimals', () => {
|
|
6
|
+
const edge = new Edge2d({ start: new Vec(0.123456, 0), end: new Vec(10, 10.987654) })
|
|
7
|
+
expect(edge.getSvgPathData()).toBe('M0.12, 0 L10, 10.99')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('omits the move command when not first', () => {
|
|
11
|
+
const edge = new Edge2d({ start: new Vec(0.123456, 0), end: new Vec(10, 10.987654) })
|
|
12
|
+
expect(edge.getSvgPathData(false)).toBe(' L10, 10.99')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('does not mutate the stored points', () => {
|
|
16
|
+
const start = new Vec(0.123456, 0)
|
|
17
|
+
const end = new Vec(10, 10.987654)
|
|
18
|
+
const edge = new Edge2d({ start, end })
|
|
19
|
+
edge.getSvgPathData()
|
|
20
|
+
expect(start).toMatchObject({ x: 0.123456, y: 0 })
|
|
21
|
+
expect(end).toMatchObject({ x: 10, y: 10.987654 })
|
|
4
22
|
})
|
|
5
23
|
})
|