@tldraw/editor 5.4.0-next.ebc95cf01337 → 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 +67 -1
- 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 +164 -280
- package/dist-cjs/lib/editor/Editor.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/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/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 +67 -1
- 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 +172 -281
- package/dist-esm/lib/editor/Editor.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/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/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 +241 -359
- 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/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/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,400 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { Mat } from '../Mat'
|
|
2
|
+
import { Vec, VecLike } from '../Vec'
|
|
3
|
+
import { Edge2d } from './Edge2d'
|
|
4
|
+
import { Geometry2dFilters, TransformedGeometry2d } from './Geometry2d'
|
|
5
|
+
import { Group2d } from './Group2d'
|
|
6
|
+
import { Rectangle2d } from './Rectangle2d'
|
|
7
|
+
|
|
8
|
+
// Two rectangles side by side with a gap between them
|
|
9
|
+
const rectA = () => new Rectangle2d({ width: 100, height: 50, isFilled: true })
|
|
10
|
+
const rectB = () => new Rectangle2d({ x: 200, width: 50, height: 50, isFilled: false })
|
|
11
|
+
// A small filled label to the right of both, and an internal geometry further right
|
|
12
|
+
const label = () =>
|
|
13
|
+
new Rectangle2d({ x: 300, width: 20, height: 20, isFilled: true, isLabel: true })
|
|
14
|
+
const internal = () =>
|
|
15
|
+
new Rectangle2d({ x: 400, width: 10, height: 10, isFilled: true, isInternal: true })
|
|
16
|
+
|
|
17
|
+
const group = () => new Group2d({ children: [rectA(), rectB()] })
|
|
18
|
+
const groupWithLabel = () => new Group2d({ children: [rectA(), label(), rectB()] })
|
|
19
|
+
|
|
20
|
+
// An L shaped path made of two edges, 200 units long in total
|
|
21
|
+
const path = () =>
|
|
22
|
+
new Group2d({
|
|
23
|
+
children: [
|
|
24
|
+
new Edge2d({ start: new Vec(0, 0), end: new Vec(100, 0) }),
|
|
25
|
+
new Edge2d({ start: new Vec(100, 0), end: new Vec(100, 100) }),
|
|
26
|
+
],
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
function sortByX(points: VecLike[]) {
|
|
30
|
+
const round = (n: number) => Math.round(n * 1e6) / 1e6
|
|
31
|
+
return [...points]
|
|
32
|
+
.map((p) => ({ x: round(p.x), y: round(p.y) }))
|
|
33
|
+
.sort((a, b) => a.x - b.x || a.y - b.y)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('Group2d construction', () => {
|
|
37
|
+
it('is closed and unfilled and passes through the other options', () => {
|
|
38
|
+
const g = new Group2d({
|
|
39
|
+
children: [rectA()],
|
|
40
|
+
isLabel: true,
|
|
41
|
+
debugColor: 'red',
|
|
42
|
+
excludeFromShapeBounds: true,
|
|
43
|
+
})
|
|
44
|
+
expect(g).toMatchObject({
|
|
45
|
+
isClosed: true,
|
|
46
|
+
isFilled: false,
|
|
47
|
+
isLabel: true,
|
|
48
|
+
debugColor: 'red',
|
|
49
|
+
excludeFromShapeBounds: true,
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('keeps the children in order', () => {
|
|
54
|
+
const a = rectA()
|
|
55
|
+
const b = rectB()
|
|
56
|
+
const g = new Group2d({ children: [a, b] })
|
|
57
|
+
expect(g.children).toEqual([a, b])
|
|
58
|
+
expect(g.ignoredChildren).toEqual([])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('flattens nested groups', () => {
|
|
62
|
+
const a = rectA()
|
|
63
|
+
const b = rectB()
|
|
64
|
+
const c = label()
|
|
65
|
+
const g = new Group2d({ children: [a, new Group2d({ children: [b, c] })] })
|
|
66
|
+
expect(g.children).toEqual([a, b, c])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('separates ignored children', () => {
|
|
70
|
+
const a = rectA()
|
|
71
|
+
const ignored = new Rectangle2d({ width: 10, height: 10, isFilled: true, ignore: true })
|
|
72
|
+
const g = new Group2d({ children: [a, ignored] })
|
|
73
|
+
expect(g.children).toEqual([a])
|
|
74
|
+
expect(g.ignoredChildren).toEqual([ignored])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('throws without any non-ignored children', () => {
|
|
78
|
+
expect(() => new Group2d({ children: [] })).toThrow('Group2d must have at least one child')
|
|
79
|
+
expect(
|
|
80
|
+
() =>
|
|
81
|
+
new Group2d({
|
|
82
|
+
children: [new Rectangle2d({ width: 10, height: 10, isFilled: true, ignore: true })],
|
|
83
|
+
})
|
|
84
|
+
).toThrow('Group2d must have at least one child')
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('Group2d.getVertices', () => {
|
|
89
|
+
it('concatenates the vertices of the children', () => {
|
|
90
|
+
expect(group().vertices).toMatchObject([
|
|
91
|
+
{ x: 0, y: 0 },
|
|
92
|
+
{ x: 100, y: 0 },
|
|
93
|
+
{ x: 100, y: 50 },
|
|
94
|
+
{ x: 0, y: 50 },
|
|
95
|
+
{ x: 200, y: 0 },
|
|
96
|
+
{ x: 250, y: 0 },
|
|
97
|
+
{ x: 250, y: 50 },
|
|
98
|
+
{ x: 200, y: 50 },
|
|
99
|
+
])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('excludes labels by default but keeps internal geometry', () => {
|
|
103
|
+
const g = new Group2d({ children: [rectA(), label(), internal()] })
|
|
104
|
+
expect(g.vertices.length).toBe(8)
|
|
105
|
+
expect(g.getVertices(Geometry2dFilters.EXCLUDE_LABELS).length).toBe(8)
|
|
106
|
+
expect(g.getVertices(Geometry2dFilters.INCLUDE_ALL).length).toBe(12)
|
|
107
|
+
expect(g.getVertices(Geometry2dFilters.EXCLUDE_INTERNAL).length).toBe(8)
|
|
108
|
+
expect(g.getVertices(Geometry2dFilters.EXCLUDE_NON_STANDARD).length).toBe(4)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('returns nothing when the group itself is excluded by the filter', () => {
|
|
112
|
+
const g = new Group2d({ children: [rectA()], isLabel: true })
|
|
113
|
+
expect(g.getVertices(Geometry2dFilters.EXCLUDE_LABELS)).toEqual([])
|
|
114
|
+
expect(g.getVertices(Geometry2dFilters.INCLUDE_ALL).length).toBe(4)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('ignores ignored children', () => {
|
|
118
|
+
const ignored = new Rectangle2d({ x: 500, width: 10, height: 10, isFilled: true, ignore: true })
|
|
119
|
+
const g = new Group2d({ children: [rectA(), ignored] })
|
|
120
|
+
expect(g.getVertices(Geometry2dFilters.INCLUDE_ALL).length).toBe(4)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('Group2d.bounds', () => {
|
|
125
|
+
it('is the union of the children bounds', () => {
|
|
126
|
+
expect(group().bounds).toMatchObject({ x: 0, y: 0, w: 250, h: 50 })
|
|
127
|
+
expect(group().center).toMatchObject({ x: 125, y: 25 })
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('includes labels', () => {
|
|
131
|
+
expect(groupWithLabel().bounds).toMatchObject({ x: 0, y: 0, w: 320, h: 50 })
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('Group2d.getLength', () => {
|
|
136
|
+
it('sums the child lengths, excluding labels by default', () => {
|
|
137
|
+
expect(group().length).toBe(500)
|
|
138
|
+
expect(groupWithLabel().length).toBe(500)
|
|
139
|
+
expect(groupWithLabel().getLength(Geometry2dFilters.INCLUDE_ALL)).toBe(580)
|
|
140
|
+
expect(groupWithLabel().getLength(Geometry2dFilters.EXCLUDE_LABELS)).toBe(500)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('Group2d.getArea', () => {
|
|
145
|
+
it('uses the area of the first child', () => {
|
|
146
|
+
expect(group().area).toBe(5000)
|
|
147
|
+
expect(new Group2d({ children: [rectB(), rectA()] }).area).toBe(2500)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
describe('Group2d.nearestPoint', () => {
|
|
152
|
+
it('returns the nearest point across the children, preferring earlier children on ties', () => {
|
|
153
|
+
expect(group().nearestPoint(new Vec(190, 25))).toMatchObject({ x: 200, y: 25 })
|
|
154
|
+
expect(group().nearestPoint(new Vec(50, -10))).toMatchObject({ x: 50, y: 0 })
|
|
155
|
+
expect(group().nearestPoint(new Vec(150, 25))).toMatchObject({ x: 100, y: 25 })
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('respects the filters', () => {
|
|
159
|
+
const g = groupWithLabel()
|
|
160
|
+
expect(g.nearestPoint(new Vec(310, 30), Geometry2dFilters.INCLUDE_ALL)).toMatchObject({
|
|
161
|
+
x: 310,
|
|
162
|
+
y: 20,
|
|
163
|
+
})
|
|
164
|
+
expect(g.nearestPoint(new Vec(310, 30), Geometry2dFilters.EXCLUDE_LABELS)).toMatchObject({
|
|
165
|
+
x: 250,
|
|
166
|
+
y: 30,
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('uses the label when no filters are given', () => {
|
|
171
|
+
expect(groupWithLabel().nearestPoint(new Vec(310, 30))).toMatchObject({ x: 310, y: 20 })
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('Group2d.distanceToPoint', () => {
|
|
176
|
+
it('is the smallest child distance, negative inside a filled child', () => {
|
|
177
|
+
expect(group().distanceToPoint(new Vec(50, 25))).toBe(-25)
|
|
178
|
+
expect(group().distanceToPoint(new Vec(225, 25))).toBe(25)
|
|
179
|
+
expect(group().distanceToPoint(new Vec(225, 25), true)).toBe(-25)
|
|
180
|
+
expect(group().distanceToPoint(new Vec(150, 25))).toBe(50)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('respects the filters', () => {
|
|
184
|
+
const g = groupWithLabel()
|
|
185
|
+
expect(g.distanceToPoint(new Vec(310, 10), false, Geometry2dFilters.INCLUDE_ALL)).toBe(-10)
|
|
186
|
+
expect(g.distanceToPoint(new Vec(310, 10), false, Geometry2dFilters.EXCLUDE_LABELS)).toBe(60)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('Group2d.hitTestPoint', () => {
|
|
191
|
+
it('hits when any child hits', () => {
|
|
192
|
+
expect(group().hitTestPoint(new Vec(50, 25), 0, false)).toBe(true)
|
|
193
|
+
expect(group().hitTestPoint(new Vec(225, 25), 0, false)).toBe(false)
|
|
194
|
+
expect(group().hitTestPoint(new Vec(225, 25), 0, true)).toBe(true)
|
|
195
|
+
expect(group().hitTestPoint(new Vec(150, 25), 0, true)).toBe(false)
|
|
196
|
+
expect(group().hitTestPoint(new Vec(150, 25), 50, false)).toBe(true)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('excludes labels by default', () => {
|
|
200
|
+
const g = groupWithLabel()
|
|
201
|
+
expect(g.hitTestPoint(new Vec(310, 10), 0, false)).toBe(false)
|
|
202
|
+
expect(g.hitTestPoint(new Vec(310, 10), 0, false, Geometry2dFilters.INCLUDE_ALL)).toBe(true)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('never hits ignored children', () => {
|
|
206
|
+
const ignored = new Rectangle2d({ x: 500, width: 10, height: 10, isFilled: true, ignore: true })
|
|
207
|
+
const g = new Group2d({ children: [rectA(), ignored] })
|
|
208
|
+
expect(g.hitTestPoint(new Vec(505, 5), 0, true)).toBe(false)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('Group2d.hitTestLineSegment', () => {
|
|
213
|
+
it('hits when any child hits', () => {
|
|
214
|
+
expect(group().hitTestLineSegment(new Vec(90, 25), new Vec(110, 25), 0)).toBe(true)
|
|
215
|
+
expect(group().hitTestLineSegment(new Vec(240, 25), new Vec(260, 25), 0)).toBe(true)
|
|
216
|
+
expect(group().hitTestLineSegment(new Vec(150, -10), new Vec(150, 60), 0)).toBe(false)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('passes the distance to the children', () => {
|
|
220
|
+
expect(group().hitTestLineSegment(new Vec(150, 0), new Vec(150, 50), 50)).toBe(true)
|
|
221
|
+
expect(group().hitTestLineSegment(new Vec(150, 0), new Vec(150, 50), 49)).toBe(false)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('excludes labels by default', () => {
|
|
225
|
+
const g = groupWithLabel()
|
|
226
|
+
expect(g.hitTestLineSegment(new Vec(310, -10), new Vec(310, 30), 0)).toBe(false)
|
|
227
|
+
expect(
|
|
228
|
+
g.hitTestLineSegment(new Vec(310, -10), new Vec(310, 30), 0, Geometry2dFilters.INCLUDE_ALL)
|
|
229
|
+
).toBe(true)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('Group2d.intersectLineSegment', () => {
|
|
234
|
+
it('collects the intersections from every child', () => {
|
|
235
|
+
const hits = group().intersectLineSegment(new Vec(-10, 25), new Vec(300, 25))
|
|
236
|
+
expect(sortByX(hits)).toEqual([
|
|
237
|
+
{ x: 0, y: 25 },
|
|
238
|
+
{ x: 100, y: 25 },
|
|
239
|
+
{ x: 200, y: 25 },
|
|
240
|
+
{ x: 250, y: 25 },
|
|
241
|
+
])
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('respects the filters', () => {
|
|
245
|
+
const g = groupWithLabel()
|
|
246
|
+
const A = new Vec(-10, 10)
|
|
247
|
+
const B = new Vec(400, 10)
|
|
248
|
+
expect(g.intersectLineSegment(A, B).length).toBe(6)
|
|
249
|
+
expect(g.intersectLineSegment(A, B, Geometry2dFilters.EXCLUDE_LABELS).length).toBe(4)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('returns an empty array when nothing is hit', () => {
|
|
253
|
+
expect(group().intersectLineSegment(new Vec(150, -10), new Vec(150, 60))).toEqual([])
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
describe('Group2d.intersectCircle', () => {
|
|
258
|
+
it('collects the intersections from every child', () => {
|
|
259
|
+
const hits = group().intersectCircle(new Vec(0, 25), 10)
|
|
260
|
+
expect(sortByX(hits)).toEqual([
|
|
261
|
+
{ x: 0, y: 15 },
|
|
262
|
+
{ x: 0, y: 35 },
|
|
263
|
+
])
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('respects the filters', () => {
|
|
267
|
+
const g = groupWithLabel()
|
|
268
|
+
expect(g.intersectCircle(new Vec(300, 10), 5).length).toBe(2)
|
|
269
|
+
expect(g.intersectCircle(new Vec(300, 10), 5, Geometry2dFilters.EXCLUDE_LABELS)).toEqual([])
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe('Group2d.intersectPolygon and intersectPolyline', () => {
|
|
274
|
+
const polygon = [new Vec(50, -10), new Vec(110, -10), new Vec(110, 60), new Vec(50, 60)]
|
|
275
|
+
|
|
276
|
+
it('collects polygon intersections from every child', () => {
|
|
277
|
+
expect(sortByX(group().intersectPolygon(polygon))).toEqual([
|
|
278
|
+
{ x: 50, y: 0 },
|
|
279
|
+
{ x: 50, y: 50 },
|
|
280
|
+
])
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('collects polyline intersections from every child', () => {
|
|
284
|
+
const polyline = [new Vec(50, -10), new Vec(50, 60)]
|
|
285
|
+
expect(sortByX(group().intersectPolyline(polyline))).toEqual([
|
|
286
|
+
{ x: 50, y: 0 },
|
|
287
|
+
{ x: 50, y: 50 },
|
|
288
|
+
])
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('respects the filters', () => {
|
|
292
|
+
const g = groupWithLabel()
|
|
293
|
+
const labelPolygon = [new Vec(310, -10), new Vec(330, -10), new Vec(330, 30), new Vec(310, 30)]
|
|
294
|
+
expect(g.intersectPolygon(labelPolygon).length).toBe(2)
|
|
295
|
+
expect(g.intersectPolygon(labelPolygon, Geometry2dFilters.EXCLUDE_LABELS)).toEqual([])
|
|
296
|
+
const labelPolyline = [new Vec(310, -10), new Vec(310, 30)]
|
|
297
|
+
expect(g.intersectPolyline(labelPolyline).length).toBe(2)
|
|
298
|
+
expect(g.intersectPolyline(labelPolyline, Geometry2dFilters.EXCLUDE_LABELS)).toEqual([])
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
describe('Group2d.interpolateAlongEdge', () => {
|
|
303
|
+
it('walks through the children in order', () => {
|
|
304
|
+
const p = path()
|
|
305
|
+
expect(p.interpolateAlongEdge(0)).toMatchObject({ x: 0, y: 0 })
|
|
306
|
+
expect(p.interpolateAlongEdge(0.25)).toMatchObject({ x: 50, y: 0 })
|
|
307
|
+
expect(p.interpolateAlongEdge(0.5)).toMatchObject({ x: 100, y: 0 })
|
|
308
|
+
expect(p.interpolateAlongEdge(0.75)).toMatchObject({ x: 100, y: 50 })
|
|
309
|
+
expect(p.interpolateAlongEdge(1)).toMatchObject({ x: 100, y: 100 })
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('clamps to the end of the last child', () => {
|
|
313
|
+
expect(path().interpolateAlongEdge(1.5)).toMatchObject({ x: 100, y: 100 })
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
describe('Group2d.uninterpolateAlongEdge', () => {
|
|
318
|
+
it('is the inverse of interpolateAlongEdge', () => {
|
|
319
|
+
const p = path()
|
|
320
|
+
expect(p.uninterpolateAlongEdge(new Vec(0, 0))).toBe(0)
|
|
321
|
+
expect(p.uninterpolateAlongEdge(new Vec(50, 0))).toBe(0.25)
|
|
322
|
+
expect(p.uninterpolateAlongEdge(new Vec(100, 0))).toBe(0.5)
|
|
323
|
+
expect(p.uninterpolateAlongEdge(new Vec(100, 50))).toBe(0.75)
|
|
324
|
+
expect(p.uninterpolateAlongEdge(new Vec(100, 100))).toBe(1)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('snaps points off the path to the nearest child', () => {
|
|
328
|
+
expect(path().uninterpolateAlongEdge(new Vec(50, -20))).toBe(0.25)
|
|
329
|
+
expect(path().uninterpolateAlongEdge(new Vec(120, 50))).toBe(0.75)
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
describe('Group2d.transform', () => {
|
|
334
|
+
it('transforms every child', () => {
|
|
335
|
+
const t = group().transform(Mat.Translate(10, 20))
|
|
336
|
+
expect(t).toBeInstanceOf(Group2d)
|
|
337
|
+
expect((t as Group2d).children.every((c) => c instanceof TransformedGeometry2d)).toBe(true)
|
|
338
|
+
expect(t.bounds).toMatchObject({ x: 10, y: 20, w: 250, h: 50 })
|
|
339
|
+
expect(t.vertices[0]).toMatchObject({ x: 10, y: 20 })
|
|
340
|
+
expect(t.vertices.length).toBe(8)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('carries the label, debug colour and ignore flags forward', () => {
|
|
344
|
+
const g = new Group2d({
|
|
345
|
+
children: [rectA()],
|
|
346
|
+
isLabel: true,
|
|
347
|
+
debugColor: 'red',
|
|
348
|
+
ignore: true,
|
|
349
|
+
})
|
|
350
|
+
expect(g.transform(Mat.Identity())).toMatchObject({
|
|
351
|
+
isLabel: true,
|
|
352
|
+
debugColor: 'red',
|
|
353
|
+
ignore: true,
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('scales hit testing with the transform', () => {
|
|
358
|
+
const t = group().transform(Mat.Scale(2, 2))
|
|
359
|
+
expect(t.hitTestPoint(new Vec(100, 50), 0, false)).toBe(true)
|
|
360
|
+
expect(t.hitTestPoint(new Vec(300, 50), 0, false)).toBe(false)
|
|
361
|
+
expect(t.distanceToPoint(new Vec(300, 50))).toBe(100)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
// Locks in current behaviour, see #10562.
|
|
365
|
+
it('drops the ignored children', () => {
|
|
366
|
+
const ignored = new Rectangle2d({ x: 500, width: 10, height: 10, isFilled: true, ignore: true })
|
|
367
|
+
const g = new Group2d({ children: [rectA(), ignored] })
|
|
368
|
+
expect(g.ignoredChildren.length).toBe(1)
|
|
369
|
+
expect((g.transform(Mat.Identity()) as Group2d).ignoredChildren).toEqual([])
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
describe('Group2d.overlapsPolygon', () => {
|
|
374
|
+
it('overlaps when any child overlaps', () => {
|
|
375
|
+
const aroundB = [new Vec(190, -10), new Vec(260, -10), new Vec(260, 60), new Vec(190, 60)]
|
|
376
|
+
expect(group().overlapsPolygon(aroundB)).toBe(true)
|
|
377
|
+
const gap = [new Vec(120, 10), new Vec(180, 10), new Vec(180, 40), new Vec(120, 40)]
|
|
378
|
+
expect(group().overlapsPolygon(gap)).toBe(false)
|
|
379
|
+
})
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
describe('Group2d svg output', () => {
|
|
383
|
+
it('joins the children paths and skips labels', () => {
|
|
384
|
+
expect(group().getSvgPathData()).toBe('M0,0 h100 v50 h-100z M200,0 h50 v50 h-50z')
|
|
385
|
+
expect(groupWithLabel().getSvgPathData()).toBe('M0,0 h100 v50 h-100z M200,0 h50 v50 h-50z')
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
it('draws the children plus a small corner mark at each bounds corner', () => {
|
|
389
|
+
const g = new Group2d({
|
|
390
|
+
children: [new Rectangle2d({ width: 128, height: 64, isFilled: true })],
|
|
391
|
+
})
|
|
392
|
+
expect(g.toSimpleSvgPath()).toBe(
|
|
393
|
+
'M0,0L128,0L128,64L0,64Z' +
|
|
394
|
+
'M0,4 L0,0 L4,0 ' +
|
|
395
|
+
'M124,0 L128,0 L128,4 ' +
|
|
396
|
+
'M128,60 L128,64 L124,64 ' +
|
|
397
|
+
'M4,64 L0,64 L0,60 '
|
|
398
|
+
)
|
|
4
399
|
})
|
|
5
400
|
})
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Vec } from '../Vec'
|
|
2
|
+
import { Polygon2d } from './Polygon2d'
|
|
3
|
+
import { Polyline2d } from './Polyline2d'
|
|
4
|
+
|
|
5
|
+
// An L shape: right along the top for 100, then down for 100
|
|
6
|
+
const points = () => [new Vec(0, 0), new Vec(100, 0), new Vec(100, 100)]
|
|
7
|
+
const polyline = () => new Polyline2d({ points: points() })
|
|
8
|
+
|
|
9
|
+
describe('Polyline2d construction', () => {
|
|
10
|
+
it('is open and unfilled', () => {
|
|
11
|
+
expect(polyline()).toMatchObject({ isClosed: false, isFilled: false })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('passes through the other options', () => {
|
|
15
|
+
const p = new Polyline2d({ points: points(), isLabel: true, debugColor: 'red', ignore: true })
|
|
16
|
+
expect(p).toMatchObject({ isLabel: true, debugColor: 'red', ignore: true })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('throws with fewer than two points', () => {
|
|
20
|
+
expect(() => new Polyline2d({ points: [] })).toThrow(
|
|
21
|
+
'Polyline2d: points must be an array of at least 2 points'
|
|
22
|
+
)
|
|
23
|
+
expect(() => new Polyline2d({ points: [new Vec(0, 0)] })).toThrow(
|
|
24
|
+
'Polyline2d: points must be an array of at least 2 points'
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('Polyline2d.getVertices', () => {
|
|
30
|
+
it('returns the points as given', () => {
|
|
31
|
+
const pts = points()
|
|
32
|
+
expect(new Polyline2d({ points: pts }).vertices).toBe(pts)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('Polyline2d.bounds', () => {
|
|
37
|
+
it('spans the points', () => {
|
|
38
|
+
expect(polyline().bounds).toMatchObject({ x: 0, y: 0, w: 100, h: 100 })
|
|
39
|
+
expect(polyline().center).toMatchObject({ x: 50, y: 50 })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('handles offset points', () => {
|
|
43
|
+
const p = new Polyline2d({ points: [new Vec(10, 20), new Vec(30, -5)] })
|
|
44
|
+
expect(p.bounds).toMatchObject({ x: 10, y: -5, w: 20, h: 25 })
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('Polyline2d.getLength', () => {
|
|
49
|
+
it('sums the segment lengths without closing', () => {
|
|
50
|
+
expect(polyline().length).toBe(200)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('is zero for coincident points', () => {
|
|
54
|
+
expect(new Polyline2d({ points: [new Vec(5, 5), new Vec(5, 5)] }).length).toBe(0)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('Polyline2d.area', () => {
|
|
59
|
+
it('is zero because the shape is open', () => {
|
|
60
|
+
expect(polyline().area).toBe(0)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('Polyline2d.nearestPoint', () => {
|
|
65
|
+
it('projects onto the nearest segment', () => {
|
|
66
|
+
expect(polyline().nearestPoint(new Vec(50, 10))).toMatchObject({ x: 50, y: 0 })
|
|
67
|
+
expect(polyline().nearestPoint(new Vec(110, 50))).toMatchObject({ x: 100, y: 50 })
|
|
68
|
+
expect(polyline().nearestPoint(new Vec(90, 50))).toMatchObject({ x: 100, y: 50 })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('clamps to the end points', () => {
|
|
72
|
+
expect(polyline().nearestPoint(new Vec(-10, -10))).toMatchObject({ x: 0, y: 0 })
|
|
73
|
+
expect(polyline().nearestPoint(new Vec(150, 150))).toMatchObject({ x: 100, y: 100 })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('prefers the earlier segment on a tie', () => {
|
|
77
|
+
expect(polyline().nearestPoint(new Vec(50, 50))).toMatchObject({ x: 50, y: 0 })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('does not use the closing segment', () => {
|
|
81
|
+
// the closing segment (100, 100) -> (0, 0) would pass through (25, 25)
|
|
82
|
+
expect(polyline().nearestPoint(new Vec(20, 30))).toMatchObject({ x: 20, y: 0 })
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('handles zero length segments', () => {
|
|
86
|
+
const p = new Polyline2d({ points: [new Vec(5, 5), new Vec(5, 5)] })
|
|
87
|
+
expect(p.nearestPoint(new Vec(10, 10))).toMatchObject({ x: 5, y: 5 })
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('returns a new vector rather than one of the points', () => {
|
|
91
|
+
const pts = points()
|
|
92
|
+
const p = new Polyline2d({ points: pts })
|
|
93
|
+
const nearest = p.nearestPoint(new Vec(-10, -10))
|
|
94
|
+
expect(nearest).toEqual(pts[0])
|
|
95
|
+
expect(nearest).not.toBe(pts[0])
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('Polyline2d.distanceToPoint', () => {
|
|
100
|
+
it('is the distance to the nearest segment', () => {
|
|
101
|
+
expect(polyline().distanceToPoint(new Vec(50, 10))).toBe(10)
|
|
102
|
+
expect(polyline().distanceToPoint(new Vec(50, 50))).toBe(50)
|
|
103
|
+
expect(polyline().distanceToPoint(new Vec(-3, -4))).toBe(5)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('is never negative for an open polyline, even with hitInside', () => {
|
|
107
|
+
expect(polyline().distanceToPoint(new Vec(90, 10), true)).toBe(10)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('Polyline2d.hitTestPoint', () => {
|
|
112
|
+
it('hits points on the line', () => {
|
|
113
|
+
expect(polyline().hitTestPoint(new Vec(50, 0))).toBe(true)
|
|
114
|
+
expect(polyline().hitTestPoint(new Vec(100, 100))).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('respects the margin', () => {
|
|
118
|
+
expect(polyline().hitTestPoint(new Vec(50, 10), 10)).toBe(true)
|
|
119
|
+
expect(polyline().hitTestPoint(new Vec(50, 10), 9)).toBe(false)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('ignores hitInside because the shape is open', () => {
|
|
123
|
+
expect(polyline().hitTestPoint(new Vec(90, 10), 0, true)).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('Polyline2d.hitTestLineSegment', () => {
|
|
128
|
+
it('hits a segment crossing the line', () => {
|
|
129
|
+
expect(polyline().hitTestLineSegment(new Vec(50, -10), new Vec(50, 10))).toBe(true)
|
|
130
|
+
expect(polyline().hitTestLineSegment(new Vec(90, 50), new Vec(110, 50))).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('misses a segment inside the corner that does not cross', () => {
|
|
134
|
+
expect(polyline().hitTestLineSegment(new Vec(50, 10), new Vec(90, 90))).toBe(false)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('misses a segment crossing only the closing segment', () => {
|
|
138
|
+
expect(polyline().hitTestLineSegment(new Vec(20, 30), new Vec(30, 20))).toBe(false)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('respects the distance from a vertex', () => {
|
|
142
|
+
expect(polyline().hitTestLineSegment(new Vec(100, -10), new Vec(120, -10), 10)).toBe(true)
|
|
143
|
+
expect(polyline().hitTestLineSegment(new Vec(100, -10), new Vec(120, -10), 9)).toBe(false)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Locks in current behaviour, see #10556.
|
|
147
|
+
it('measures the distance from the vertices, not the edges (known quirk)', () => {
|
|
148
|
+
// the segment is 10 units from the middle of the top edge but 41 from the nearest vertex
|
|
149
|
+
expect(polyline().hitTestLineSegment(new Vec(40, 10), new Vec(60, 10), 10)).toBe(false)
|
|
150
|
+
expect(polyline().hitTestLineSegment(new Vec(40, 10), new Vec(60, 10), 42)).toBe(true)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('Polyline2d.intersectLineSegment', () => {
|
|
155
|
+
it('does not intersect the closing segment', () => {
|
|
156
|
+
expect(polyline().intersectLineSegment(new Vec(50, -10), new Vec(50, 10))).toEqual([
|
|
157
|
+
{ x: 50, y: 0, z: 1 },
|
|
158
|
+
])
|
|
159
|
+
expect(polyline().intersectLineSegment(new Vec(20, 30), new Vec(30, 20))).toEqual([])
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe('Polyline2d.interpolateAlongEdge', () => {
|
|
164
|
+
it('walks along the segments', () => {
|
|
165
|
+
expect(polyline().interpolateAlongEdge(0)).toMatchObject({ x: 0, y: 0 })
|
|
166
|
+
expect(polyline().interpolateAlongEdge(0.25)).toMatchObject({ x: 50, y: 0 })
|
|
167
|
+
expect(polyline().interpolateAlongEdge(0.75)).toMatchObject({ x: 100, y: 50 })
|
|
168
|
+
expect(polyline().interpolateAlongEdge(1)).toMatchObject({ x: 100, y: 100 })
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('is inverted by uninterpolateAlongEdge', () => {
|
|
172
|
+
expect(polyline().uninterpolateAlongEdge(new Vec(50, 0))).toBe(0.25)
|
|
173
|
+
expect(polyline().uninterpolateAlongEdge(new Vec(100, 50))).toBe(0.75)
|
|
174
|
+
expect(polyline().uninterpolateAlongEdge(new Vec(120, 50))).toBe(0.75)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('Polyline2d.getSvgPathData', () => {
|
|
179
|
+
it('emits a move followed by line commands', () => {
|
|
180
|
+
expect(polyline().getSvgPathData()).toBe('M 0 0 L 100 0 L 100 100')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('Polyline2d when closed (via Polygon2d)', () => {
|
|
185
|
+
const closed = (isFilled = false) => new Polygon2d({ points: points(), isFilled })
|
|
186
|
+
|
|
187
|
+
it('uses the closing segment for the nearest point', () => {
|
|
188
|
+
expect(closed().nearestPoint(new Vec(20, 30))).toMatchObject({ x: 25, y: 25 })
|
|
189
|
+
expect(closed().length).toBeCloseTo(200 + Math.hypot(100, 100))
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('returns a negative distance inside when filled or hitInside', () => {
|
|
193
|
+
expect(closed().distanceToPoint(new Vec(90, 10))).toBe(10)
|
|
194
|
+
expect(closed().distanceToPoint(new Vec(90, 10), true)).toBe(-10)
|
|
195
|
+
expect(closed(true).distanceToPoint(new Vec(90, 10))).toBe(-10)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('hits the interior when filled', () => {
|
|
199
|
+
expect(closed(true).hitTestPoint(new Vec(90, 10))).toBe(true)
|
|
200
|
+
expect(closed().hitTestPoint(new Vec(90, 10))).toBe(false)
|
|
201
|
+
expect(closed().hitTestPoint(new Vec(90, 10), 0, true)).toBe(true)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('hits segments crossing the closing segment', () => {
|
|
205
|
+
expect(closed().hitTestLineSegment(new Vec(20, 30), new Vec(30, 20))).toBe(true)
|
|
206
|
+
})
|
|
207
|
+
})
|