@tldraw/editor 5.4.0-next.ef99ab47e5ce → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +114 -47
- 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/editor/Editor.js +181 -294
- 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/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/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/Group2d.js +63 -21
- package/dist-cjs/lib/primitives/geometry/Group2d.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 +114 -47
- 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/editor/Editor.mjs +189 -295
- 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/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/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/Group2d.mjs +63 -21
- package/dist-esm/lib/primitives/geometry/Group2d.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/editor/Editor.ts +266 -383
- 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/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/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 +226 -3
- package/src/lib/primitives/geometry/CubicBezier2d.test.ts +173 -3
- package/src/lib/primitives/geometry/CubicSpline2d.test.ts +160 -3
- 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 +398 -3
- package/src/lib/primitives/geometry/Group2d.ts +63 -27
- 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,175 @@
|
|
|
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')
|
|
4
174
|
})
|
|
5
175
|
})
|
|
@@ -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,245 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { perimeterOfEllipse } from '../utils'
|
|
2
|
+
import { Vec } from '../Vec'
|
|
3
|
+
import { Edge2d } from './Edge2d'
|
|
4
|
+
import { Ellipse2d } from './Ellipse2d'
|
|
5
|
+
import { getVerticesCountForArcLength } from './geometry-constants'
|
|
6
|
+
|
|
7
|
+
// A 100x50 ellipse: centre (50, 25), rx = 50, ry = 25
|
|
8
|
+
const ellipse = () => new Ellipse2d({ width: 100, height: 50, isFilled: false })
|
|
9
|
+
const filled = () => new Ellipse2d({ width: 100, height: 50, isFilled: true })
|
|
10
|
+
|
|
11
|
+
function expectOnEllipse(p: Vec, cx: number, cy: number, rx: number, ry: number) {
|
|
12
|
+
const dx = (p.x - cx) / rx
|
|
13
|
+
const dy = (p.y - cy) / ry
|
|
14
|
+
expect(dx * dx + dy * dy).toBeCloseTo(1, 6)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('Ellipse2d construction', () => {
|
|
18
|
+
it('is always closed and keeps the fill flag from config', () => {
|
|
19
|
+
expect(ellipse()).toMatchObject({ isClosed: true, isFilled: false })
|
|
20
|
+
expect(filled()).toMatchObject({ isClosed: true, isFilled: true })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('passes through the other geometry options', () => {
|
|
24
|
+
const e = new Ellipse2d({
|
|
25
|
+
width: 10,
|
|
26
|
+
height: 10,
|
|
27
|
+
isFilled: true,
|
|
28
|
+
isLabel: true,
|
|
29
|
+
isInternal: true,
|
|
30
|
+
debugColor: 'red',
|
|
31
|
+
ignore: true,
|
|
32
|
+
excludeFromShapeBounds: true,
|
|
33
|
+
})
|
|
34
|
+
expect(e).toMatchObject({
|
|
35
|
+
isLabel: true,
|
|
36
|
+
isInternal: true,
|
|
37
|
+
debugColor: 'red',
|
|
38
|
+
ignore: true,
|
|
39
|
+
excludeFromShapeBounds: true,
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('Ellipse2d.getVertices', () => {
|
|
45
|
+
it('samples one vertex per 20 units of perimeter, starting at the rightmost point', () => {
|
|
46
|
+
const e = ellipse()
|
|
47
|
+
const { vertices } = e
|
|
48
|
+
expect(vertices.length).toBe(getVerticesCountForArcLength(perimeterOfEllipse(50, 25)))
|
|
49
|
+
expect(vertices.length).toBe(13)
|
|
50
|
+
expect(vertices[0]).toMatchObject({ x: 100, y: 25 })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('places every vertex on the ellipse and inside the bounds', () => {
|
|
54
|
+
const { vertices, bounds } = ellipse()
|
|
55
|
+
for (const v of vertices) {
|
|
56
|
+
expectOnEllipse(v, 50, 25, 50, 25)
|
|
57
|
+
expect(bounds.containsPoint(v)).toBe(true)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('walks the ellipse in the positive angular direction', () => {
|
|
62
|
+
const { vertices } = ellipse()
|
|
63
|
+
// second vertex is one step (2π/13) clockwise in screen space, so below the first
|
|
64
|
+
expect(vertices[1].x).toBeLessThan(100)
|
|
65
|
+
expect(vertices[1].y).toBeGreaterThan(25)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('falls back to the minimum vertex count for a tiny ellipse', () => {
|
|
69
|
+
const { vertices } = new Ellipse2d({ width: 1, height: 1, isFilled: false })
|
|
70
|
+
expect(vertices.length).toBe(8)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('does not produce NaNs for a zero size ellipse', () => {
|
|
74
|
+
const e = new Ellipse2d({ width: 0, height: 0, isFilled: false })
|
|
75
|
+
expect(e.vertices.length).toBe(8)
|
|
76
|
+
for (const v of e.vertices) {
|
|
77
|
+
expect(Number.isFinite(v.x)).toBe(true)
|
|
78
|
+
expect(Number.isFinite(v.y)).toBe(true)
|
|
79
|
+
}
|
|
80
|
+
expect(e.bounds).toMatchObject({ x: 0, y: 0, w: 0, h: 0 })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('has a NaN length for a zero size ellipse (known quirk)', () => {
|
|
84
|
+
// perimeterOfEllipse divides by (rx + ry) ** 2, which is zero here
|
|
85
|
+
expect(new Ellipse2d({ width: 0, height: 0, isFilled: false }).getLength()).toBeNaN()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('Ellipse2d.edges', () => {
|
|
90
|
+
it('connects consecutive vertices and closes back to the first', () => {
|
|
91
|
+
const e = ellipse()
|
|
92
|
+
const { edges, vertices } = e
|
|
93
|
+
expect(edges.length).toBe(vertices.length)
|
|
94
|
+
expect(edges[0]).toBeInstanceOf(Edge2d)
|
|
95
|
+
expect(edges[0].vertices).toEqual([vertices[0], vertices[1]])
|
|
96
|
+
expect(edges[edges.length - 1].vertices).toEqual([vertices[vertices.length - 1], vertices[0]])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('caches the edges', () => {
|
|
100
|
+
const e = ellipse()
|
|
101
|
+
expect(e.edges).toBe(e.edges)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('Ellipse2d.bounds', () => {
|
|
106
|
+
it('is the width by height box at the origin', () => {
|
|
107
|
+
expect(ellipse().bounds).toMatchObject({ x: 0, y: 0, w: 100, h: 50 })
|
|
108
|
+
expect(ellipse().center).toMatchObject({ x: 50, y: 25 })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('is not affected by the polygon approximation', () => {
|
|
112
|
+
// the bounds come from the config, not from the sampled vertices
|
|
113
|
+
const e = new Ellipse2d({ width: 3, height: 7, isFilled: false })
|
|
114
|
+
expect(e.bounds).toMatchObject({ x: 0, y: 0, w: 3, h: 7 })
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('Ellipse2d.getLength', () => {
|
|
119
|
+
it("uses Ramanujan's perimeter approximation", () => {
|
|
120
|
+
expect(ellipse().length).toBeCloseTo(242.21, 1)
|
|
121
|
+
expect(ellipse().length).toBeCloseTo(perimeterOfEllipse(50, 25))
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('matches a circle circumference when width equals height', () => {
|
|
125
|
+
expect(new Ellipse2d({ width: 100, height: 100, isFilled: false }).length).toBeCloseTo(
|
|
126
|
+
Math.PI * 100
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('Ellipse2d.area', () => {
|
|
132
|
+
it('is the area of the inscribed polygon', () => {
|
|
133
|
+
const e = ellipse()
|
|
134
|
+
const n = e.vertices.length
|
|
135
|
+
expect(e.area).toBeCloseTo((n / 2) * 50 * 25 * Math.sin((2 * Math.PI) / n), 5)
|
|
136
|
+
expect(e.area).toBeLessThan(Math.PI * 50 * 25)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('Ellipse2d.nearestPoint', () => {
|
|
141
|
+
it('returns the vertex when it is the nearest point', () => {
|
|
142
|
+
expect(ellipse().nearestPoint(new Vec(150, 25))).toMatchObject({ x: 100, y: 25 })
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('returns a point on the edge polygon nearest to the point', () => {
|
|
146
|
+
const e = ellipse()
|
|
147
|
+
const p = e.nearestPoint(new Vec(50, -10))
|
|
148
|
+
expect(Vec.Dist(p, new Vec(50, 0))).toBeLessThan(1.5)
|
|
149
|
+
expect(e.hitTestPoint(p, 0.001)).toBe(true)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('accepts plain objects', () => {
|
|
153
|
+
expect(ellipse().nearestPoint({ x: 150, y: 25 })).toMatchObject({ x: 100, y: 25 })
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('Ellipse2d.distanceToPoint', () => {
|
|
158
|
+
it('is positive outside', () => {
|
|
159
|
+
expect(ellipse().distanceToPoint(new Vec(150, 25))).toBe(50)
|
|
160
|
+
expect(filled().distanceToPoint(new Vec(150, 25))).toBe(50)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('is zero on a vertex', () => {
|
|
164
|
+
expect(ellipse().distanceToPoint(new Vec(100, 25))).toBe(0)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('is negative inside only when filled or hitInside', () => {
|
|
168
|
+
const unfilledDist = ellipse().distanceToPoint(new Vec(50, 25))
|
|
169
|
+
expect(unfilledDist).toBeGreaterThan(0)
|
|
170
|
+
expect(filled().distanceToPoint(new Vec(50, 25))).toBe(-unfilledDist)
|
|
171
|
+
expect(ellipse().distanceToPoint(new Vec(50, 25), true)).toBe(-unfilledDist)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('Ellipse2d.hitTestPoint', () => {
|
|
176
|
+
it('hits the interior only when filled or hitInside', () => {
|
|
177
|
+
expect(filled().hitTestPoint(new Vec(50, 25))).toBe(true)
|
|
178
|
+
expect(ellipse().hitTestPoint(new Vec(50, 25))).toBe(false)
|
|
179
|
+
expect(ellipse().hitTestPoint(new Vec(50, 25), 0, true)).toBe(true)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('misses points outside', () => {
|
|
183
|
+
expect(filled().hitTestPoint(new Vec(150, 25))).toBe(false)
|
|
184
|
+
expect(ellipse().hitTestPoint(new Vec(150, 25))).toBe(false)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('misses the bounding box corners, which are outside the ellipse', () => {
|
|
188
|
+
expect(filled().hitTestPoint(new Vec(1, 1))).toBe(false)
|
|
189
|
+
expect(filled().hitTestPoint(new Vec(99, 49))).toBe(false)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('hits the edge', () => {
|
|
193
|
+
expect(ellipse().hitTestPoint(new Vec(100, 25))).toBe(true)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('respects the margin', () => {
|
|
197
|
+
expect(ellipse().hitTestPoint(new Vec(105, 25), 5)).toBe(true)
|
|
198
|
+
expect(ellipse().hitTestPoint(new Vec(105, 25), 4)).toBe(false)
|
|
199
|
+
expect(ellipse().hitTestPoint(new Vec(95, 25), 5)).toBe(true)
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe('Ellipse2d.hitTestLineSegment', () => {
|
|
204
|
+
it('hits a segment crossing the edge', () => {
|
|
205
|
+
expect(ellipse().hitTestLineSegment(new Vec(50, -10), new Vec(50, 60))).toBe(true)
|
|
206
|
+
expect(ellipse().hitTestLineSegment(new Vec(90, 25), new Vec(110, 25))).toBe(true)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('misses a segment entirely outside', () => {
|
|
210
|
+
expect(ellipse().hitTestLineSegment(new Vec(150, 0), new Vec(150, 50))).toBe(false)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('misses a segment entirely inside, even when filled', () => {
|
|
214
|
+
expect(ellipse().hitTestLineSegment(new Vec(40, 25), new Vec(60, 25))).toBe(false)
|
|
215
|
+
expect(filled().hitTestLineSegment(new Vec(40, 25), new Vec(60, 25))).toBe(false)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('misses a segment that cuts through a bounding box corner only', () => {
|
|
219
|
+
expect(ellipse().hitTestLineSegment(new Vec(0, 0), new Vec(5, 5))).toBe(false)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('Ellipse2d.intersectLineSegment', () => {
|
|
224
|
+
it('returns both crossings of a horizontal line through the centre', () => {
|
|
225
|
+
const hits = ellipse().intersectLineSegment(new Vec(-10, 25), new Vec(110, 25))
|
|
226
|
+
expect(hits.length).toBe(2)
|
|
227
|
+
expect(hits.map((h) => h.y)).toEqual([25, 25])
|
|
228
|
+
const xs = hits.map((h) => h.x).sort((a, b) => a - b)
|
|
229
|
+
// the leftmost point is not a vertex, so the polygon edge sits slightly inside the ellipse
|
|
230
|
+
expect(xs[0]).toBeGreaterThanOrEqual(0)
|
|
231
|
+
expect(xs[0]).toBeLessThan(2)
|
|
232
|
+
expect(xs[1]).toBe(100)
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
describe('Ellipse2d.getSvgPathData', () => {
|
|
237
|
+
it('draws two arcs starting from the leftmost point', () => {
|
|
238
|
+
expect(ellipse().getSvgPathData(true)).toBe('M0,25 a50,25,0,1,1,100,0a50,25,0,1,1,-100,0')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('omits the move command when not first', () => {
|
|
242
|
+
expect(ellipse().getSvgPathData()).toBe(' a50,25,0,1,1,100,0a50,25,0,1,1,-100,0')
|
|
243
|
+
expect(ellipse().getSvgPathData(false)).toBe(ellipse().getSvgPathData())
|
|
4
244
|
})
|
|
5
245
|
})
|