@tldraw/editor 3.16.0-next.df90ce0ff566 → 3.16.0-next.e57e478c23e0
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 +134 -110
- package/dist-cjs/index.js +3 -5
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +8 -6
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +0 -5
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/Shape.js +7 -10
- package/dist-cjs/lib/components/Shape.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -23
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultScribble.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +9 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
- package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +77 -133
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +9 -4
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +13 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
- package/dist-cjs/lib/exports/getSvgJsx.js +35 -16
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +31 -25
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +4 -1
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/{utils/nearestMultiple.js → hooks/useStateAttribute.js} +15 -14
- package/dist-cjs/lib/hooks/useStateAttribute.js.map +7 -0
- package/dist-cjs/lib/license/LicenseManager.js +17 -22
- package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
- package/dist-cjs/lib/license/LicenseProvider.js +5 -0
- package/dist-cjs/lib/license/LicenseProvider.js.map +2 -2
- package/dist-cjs/lib/license/Watermark.js +8 -8
- package/dist-cjs/lib/license/Watermark.js.map +1 -1
- package/dist-cjs/lib/license/useLicenseManagerState.js.map +2 -2
- package/dist-cjs/lib/options.js +7 -0
- package/dist-cjs/lib/options.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +3 -0
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +0 -4
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +26 -18
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +3 -0
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/utils/EditorAtom.js +45 -0
- package/dist-cjs/lib/utils/EditorAtom.js.map +7 -0
- package/dist-cjs/lib/utils/reparenting.js +2 -35
- package/dist-cjs/lib/utils/reparenting.js.map +3 -3
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +134 -110
- package/dist-esm/index.mjs +3 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +8 -6
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +0 -5
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/Shape.mjs +7 -10
- package/dist-esm/lib/components/Shape.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -23
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultScribble.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +9 -1
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +77 -133
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +9 -4
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +13 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs +36 -16
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +32 -26
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +4 -1
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useStateAttribute.mjs +15 -0
- package/dist-esm/lib/hooks/useStateAttribute.mjs.map +7 -0
- package/dist-esm/lib/license/LicenseManager.mjs +17 -22
- package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
- package/dist-esm/lib/license/LicenseProvider.mjs +5 -0
- package/dist-esm/lib/license/LicenseProvider.mjs.map +2 -2
- package/dist-esm/lib/license/Watermark.mjs +8 -8
- package/dist-esm/lib/license/Watermark.mjs.map +1 -1
- package/dist-esm/lib/license/useLicenseManagerState.mjs.map +2 -2
- package/dist-esm/lib/options.mjs +7 -0
- package/dist-esm/lib/options.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +4 -1
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +0 -4
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +29 -19
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +3 -0
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/utils/EditorAtom.mjs +25 -0
- package/dist-esm/lib/utils/EditorAtom.mjs.map +7 -0
- package/dist-esm/lib/utils/reparenting.mjs +3 -40
- package/dist-esm/lib/utils/reparenting.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +301 -288
- package/package.json +14 -37
- package/src/index.ts +3 -9
- package/src/lib/TldrawEditor.tsx +13 -17
- package/src/lib/components/MenuClickCapture.tsx +0 -8
- package/src/lib/components/Shape.tsx +6 -12
- package/src/lib/components/default-components/DefaultCanvas.tsx +5 -22
- package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
- package/src/lib/components/default-components/DefaultScribble.tsx +1 -1
- package/src/lib/components/default-components/DefaultShapeIndicator.tsx +5 -1
- package/src/lib/config/TLUserPreferences.ts +8 -1
- package/src/lib/editor/Editor.test.ts +12 -11
- package/src/lib/editor/Editor.ts +108 -193
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
- package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
- package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +34 -26
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +6 -1
- package/src/lib/editor/shapes/ShapeUtil.ts +35 -0
- package/src/lib/editor/types/misc-types.ts +54 -7
- package/src/lib/exports/getSvgJsx.test.ts +868 -0
- package/src/lib/exports/getSvgJsx.tsx +78 -21
- package/src/lib/hooks/useCanvasEvents.ts +45 -38
- package/src/lib/hooks/usePassThroughMouseOverEvents.ts +4 -1
- package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
- package/src/lib/hooks/useStateAttribute.ts +15 -0
- package/src/lib/license/LicenseManager.test.ts +61 -52
- package/src/lib/license/LicenseManager.ts +32 -24
- package/src/lib/license/LicenseProvider.tsx +8 -0
- package/src/lib/license/Watermark.test.tsx +2 -1
- package/src/lib/license/Watermark.tsx +8 -8
- package/src/lib/license/useLicenseManagerState.ts +2 -2
- package/src/lib/options.ts +8 -0
- package/src/lib/primitives/Box.test.ts +126 -0
- package/src/lib/primitives/Box.ts +10 -1
- package/src/lib/primitives/Vec.ts +0 -5
- package/src/lib/primitives/geometry/Geometry2d.ts +49 -19
- package/src/lib/primitives/geometry/Group2d.ts +4 -0
- package/src/lib/utils/EditorAtom.ts +37 -0
- package/src/lib/utils/reparenting.ts +3 -69
- package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
- package/src/version.ts +3 -3
- package/dist-cjs/lib/utils/nearestMultiple.js.map +0 -7
- package/dist-esm/lib/utils/nearestMultiple.mjs +0 -14
- package/dist-esm/lib/utils/nearestMultiple.mjs.map +0 -7
- package/src/lib/utils/nearestMultiple.ts +0 -13
|
@@ -86,15 +86,15 @@ To remove the watermark, please purchase a license at tldraw.dev.
|
|
|
86
86
|
|
|
87
87
|
.${className} {
|
|
88
88
|
position: absolute;
|
|
89
|
-
bottom: var(--space-2);
|
|
90
|
-
right: var(--space-2);
|
|
89
|
+
bottom: max(var(--tl-space-2), env(safe-area-inset-bottom));
|
|
90
|
+
right: max(var(--tl-space-2), env(safe-area-inset-right));
|
|
91
91
|
width: 96px;
|
|
92
92
|
height: 32px;
|
|
93
93
|
display: flex;
|
|
94
94
|
align-items: center;
|
|
95
95
|
justify-content: center;
|
|
96
|
-
z-index: var(--layer-watermark) !important;
|
|
97
|
-
background-color: color-mix(in srgb, var(--color-background) 62%, transparent);
|
|
96
|
+
z-index: var(--tl-layer-watermark) !important;
|
|
97
|
+
background-color: color-mix(in srgb, var(--tl-color-background) 62%, transparent);
|
|
98
98
|
opacity: 1;
|
|
99
99
|
border-radius: 5px;
|
|
100
100
|
pointer-events: all;
|
|
@@ -108,7 +108,7 @@ To remove the watermark, please purchase a license at tldraw.dev.
|
|
|
108
108
|
height: 32px;
|
|
109
109
|
pointer-events: all;
|
|
110
110
|
cursor: inherit;
|
|
111
|
-
color: var(--color-text);
|
|
111
|
+
color: var(--tl-color-text);
|
|
112
112
|
opacity: .38;
|
|
113
113
|
border: 0;
|
|
114
114
|
padding: 0;
|
|
@@ -116,12 +116,12 @@ To remove the watermark, please purchase a license at tldraw.dev.
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
.${className}[data-debug='true'] {
|
|
119
|
-
bottom: 46px;
|
|
119
|
+
bottom: max(46px, env(safe-area-inset-bottom));
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
.${className}[data-mobile='true'] {
|
|
123
123
|
border-radius: 4px 0px 0px 4px;
|
|
124
|
-
right: -2px;
|
|
124
|
+
right: max(-2px, calc(env(safe-area-inset-right) - 2px));
|
|
125
125
|
width: 8px;
|
|
126
126
|
height: 48px;
|
|
127
127
|
}
|
|
@@ -137,7 +137,7 @@ To remove the watermark, please purchase a license at tldraw.dev.
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
.${className}:hover {
|
|
140
|
-
background-color: var(--color-background);
|
|
140
|
+
background-color: var(--tl-color-background);
|
|
141
141
|
transition: background-color 0.2s ease-in-out;
|
|
142
142
|
transition-delay: 0.32s;
|
|
143
143
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useValue } from '@tldraw/state-react'
|
|
2
|
-
import { LicenseManager } from './LicenseManager'
|
|
2
|
+
import { LicenseManager, LicenseState } from './LicenseManager'
|
|
3
3
|
|
|
4
4
|
/** @internal */
|
|
5
|
-
export function useLicenseManagerState(licenseManager: LicenseManager) {
|
|
5
|
+
export function useLicenseManagerState(licenseManager: LicenseManager): LicenseState {
|
|
6
6
|
return useValue('watermarkState', () => licenseManager.state.get(), [licenseManager])
|
|
7
7
|
}
|
package/src/lib/options.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface TldrawOptions {
|
|
|
27
27
|
readonly multiClickDurationMs: number
|
|
28
28
|
readonly coarseDragDistanceSquared: number
|
|
29
29
|
readonly dragDistanceSquared: number
|
|
30
|
+
readonly uiDragDistanceSquared: number
|
|
31
|
+
readonly uiCoarseDragDistanceSquared: number
|
|
30
32
|
readonly defaultSvgPadding: number
|
|
31
33
|
readonly cameraSlideFriction: number
|
|
32
34
|
readonly gridSteps: readonly {
|
|
@@ -53,6 +55,7 @@ export interface TldrawOptions {
|
|
|
53
55
|
readonly flattenImageBoundsPadding: number
|
|
54
56
|
readonly laserDelayMs: number
|
|
55
57
|
readonly maxExportDelayMs: number
|
|
58
|
+
readonly tooltipDelayMs: number
|
|
56
59
|
/**
|
|
57
60
|
* How long should previews created by {@link Editor.createTemporaryAssetPreview} last before
|
|
58
61
|
* they expire? Defaults to 3 minutes.
|
|
@@ -97,6 +100,10 @@ export const defaultTldrawOptions = {
|
|
|
97
100
|
multiClickDurationMs: 200,
|
|
98
101
|
coarseDragDistanceSquared: 36, // 6 squared
|
|
99
102
|
dragDistanceSquared: 16, // 4 squared
|
|
103
|
+
uiDragDistanceSquared: 16, // 4 squared
|
|
104
|
+
// it's really easy to accidentally drag from the toolbar on mobile, so we use a much larger
|
|
105
|
+
// threshold than usual here to try and prevent accidental drags.
|
|
106
|
+
uiCoarseDragDistanceSquared: 625, // 25 squared
|
|
100
107
|
defaultSvgPadding: 32,
|
|
101
108
|
cameraSlideFriction: 0.09,
|
|
102
109
|
gridSteps: [
|
|
@@ -124,6 +131,7 @@ export const defaultTldrawOptions = {
|
|
|
124
131
|
flattenImageBoundsPadding: 16,
|
|
125
132
|
laserDelayMs: 1200,
|
|
126
133
|
maxExportDelayMs: 5000,
|
|
134
|
+
tooltipDelayMs: 700,
|
|
127
135
|
temporaryAssetPreviewLifetimeMs: 180000,
|
|
128
136
|
actionShortcutsLocation: 'swap',
|
|
129
137
|
createTextOnCanvasDoubleClick: true,
|
|
@@ -510,6 +510,132 @@ describe('Box', () => {
|
|
|
510
510
|
})
|
|
511
511
|
})
|
|
512
512
|
|
|
513
|
+
describe('Box.ContainsApproximately', () => {
|
|
514
|
+
it('returns true when first box exactly contains second', () => {
|
|
515
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
516
|
+
const boxB = new Box(10, 10, 50, 50)
|
|
517
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('returns false when first box clearly does not contain second', () => {
|
|
521
|
+
const boxA = new Box(0, 0, 50, 50)
|
|
522
|
+
const boxB = new Box(10, 10, 100, 100)
|
|
523
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(false)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it('returns true when containment is within default precision tolerance', () => {
|
|
527
|
+
// Box B extends very slightly outside A (within floating-point precision)
|
|
528
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
529
|
+
const boxB = new Box(10, 10, 80, 80)
|
|
530
|
+
// Move B's max edges just slightly outside A's bounds
|
|
531
|
+
boxB.w = 90.000000000001 // maxX = 100.000000000001 (slightly beyond 100)
|
|
532
|
+
boxB.h = 90.000000000001 // maxY = 100.000000000001 (slightly beyond 100)
|
|
533
|
+
|
|
534
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
|
|
535
|
+
expect(Box.Contains(boxA, boxB)).toBe(false) // strict contains would fail
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it('returns false when containment exceeds default precision tolerance', () => {
|
|
539
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
540
|
+
const boxB = new Box(10, 10, 80, 80)
|
|
541
|
+
// Move B's max edges clearly outside A's bounds
|
|
542
|
+
boxB.w = 95 // maxX = 105 (clearly beyond 100)
|
|
543
|
+
boxB.h = 95 // maxY = 105 (clearly beyond 100)
|
|
544
|
+
|
|
545
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(false)
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
it('respects custom precision parameter', () => {
|
|
549
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
550
|
+
const boxB = new Box(10, 10, 85, 85) // maxX=95, maxY=95
|
|
551
|
+
|
|
552
|
+
// With loose precision (10), should contain (95 is within 100-10=90 tolerance)
|
|
553
|
+
expect(Box.ContainsApproximately(boxA, boxB, 10)).toBe(true)
|
|
554
|
+
|
|
555
|
+
// With tight precision (4), should still contain (95 is within 100-4=96)
|
|
556
|
+
expect(Box.ContainsApproximately(boxA, boxB, 4)).toBe(true)
|
|
557
|
+
|
|
558
|
+
// Since 95 < 100, the precision parameter doesn't affect containment here
|
|
559
|
+
expect(Box.ContainsApproximately(boxA, boxB, 4.9)).toBe(true)
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it('handles negative coordinates correctly', () => {
|
|
563
|
+
const boxA = new Box(-50, -50, 100, 100) // bounds: (-50,-50) to (50,50)
|
|
564
|
+
const boxB = new Box(-40, -40, 79.999999999, 79.999999999) // bounds: (-40,-40) to (39.999999999, 39.999999999)
|
|
565
|
+
|
|
566
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
it('handles edge case where boxes are identical', () => {
|
|
570
|
+
const boxA = new Box(10, 20, 100, 200)
|
|
571
|
+
const boxB = new Box(10, 20, 100, 200)
|
|
572
|
+
|
|
573
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('handles edge case where inner box touches outer box edges', () => {
|
|
577
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
578
|
+
const boxB = new Box(0, 0, 100, 100) // exactly the same
|
|
579
|
+
|
|
580
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
|
|
581
|
+
|
|
582
|
+
// Slightly smaller inner box
|
|
583
|
+
const boxC = new Box(0.000001, 0.000001, 99.999998, 99.999998)
|
|
584
|
+
expect(Box.ContainsApproximately(boxA, boxC)).toBe(true)
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
it('handles floating-point precision issues in real-world scenarios', () => {
|
|
588
|
+
// Simulate common floating-point arithmetic issues
|
|
589
|
+
const containerBox = new Box(0, 0, 100, 100)
|
|
590
|
+
|
|
591
|
+
// Box that should be contained but has floating-point errors
|
|
592
|
+
const innerBox = new Box(10, 10, 80, 80)
|
|
593
|
+
// Simulate floating-point arithmetic that results in tiny overruns
|
|
594
|
+
innerBox.w = 90.00000000000001 // maxX = 100.00000000000001 (tiny overrun)
|
|
595
|
+
innerBox.h = 90.00000000000001 // maxY = 100.00000000000001 (tiny overrun)
|
|
596
|
+
|
|
597
|
+
expect(Box.ContainsApproximately(containerBox, innerBox)).toBe(true)
|
|
598
|
+
expect(Box.Contains(containerBox, innerBox)).toBe(false) // strict contains fails due to precision
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('fails when any edge exceeds tolerance', () => {
|
|
602
|
+
const boxA = new Box(10, 10, 100, 100) // bounds: (10,10) to (110,110)
|
|
603
|
+
|
|
604
|
+
// Test each edge exceeding tolerance
|
|
605
|
+
const testCases = [
|
|
606
|
+
{ name: 'left edge', box: new Box(5, 20, 80, 80) }, // minX too small
|
|
607
|
+
{ name: 'top edge', box: new Box(20, 5, 80, 80) }, // minY too small
|
|
608
|
+
{ name: 'right edge', box: new Box(20, 20, 95, 80) }, // maxX too large (20+95=115 > 110)
|
|
609
|
+
{ name: 'bottom edge', box: new Box(20, 20, 80, 95) }, // maxY too large (20+95=115 > 110)
|
|
610
|
+
]
|
|
611
|
+
|
|
612
|
+
testCases.forEach(({ box }) => {
|
|
613
|
+
expect(Box.ContainsApproximately(boxA, box, 1)).toBe(false) // tight precision
|
|
614
|
+
})
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
it('works with zero-sized dimensions', () => {
|
|
618
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
619
|
+
const boxB = new Box(50, 50, 0, 0) // zero-sized box (point)
|
|
620
|
+
|
|
621
|
+
expect(Box.ContainsApproximately(boxA, boxB)).toBe(true)
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
it('handles precision parameter edge cases', () => {
|
|
625
|
+
const boxA = new Box(0, 0, 100, 100)
|
|
626
|
+
const boxB = new Box(10, 10, 91, 91) // maxX=101, maxY=101 (clearly outside)
|
|
627
|
+
|
|
628
|
+
// Zero precision should work like strict Contains
|
|
629
|
+
expect(Box.ContainsApproximately(boxA, boxB, 0)).toBe(false)
|
|
630
|
+
|
|
631
|
+
// Small precision should still fail (101 > 100)
|
|
632
|
+
expect(Box.ContainsApproximately(boxA, boxB, 0.5)).toBe(false)
|
|
633
|
+
|
|
634
|
+
// Sufficient precision should succeed (101 <= 100 + 2)
|
|
635
|
+
expect(Box.ContainsApproximately(boxA, boxB, 2)).toBe(true)
|
|
636
|
+
})
|
|
637
|
+
})
|
|
638
|
+
|
|
513
639
|
describe('Box.Includes', () => {
|
|
514
640
|
it('returns true when boxes collide or contain', () => {
|
|
515
641
|
const boxA = new Box(0, 0, 50, 50)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BoxModel } from '@tldraw/tlschema'
|
|
2
2
|
import { Vec, VecLike } from './Vec'
|
|
3
|
-
import { PI, PI2, toPrecision } from './utils'
|
|
3
|
+
import { approximatelyLte, PI, PI2, toPrecision } from './utils'
|
|
4
4
|
|
|
5
5
|
/** @public */
|
|
6
6
|
export type BoxLike = BoxModel | Box
|
|
@@ -417,6 +417,15 @@ export class Box {
|
|
|
417
417
|
return A.minX < B.minX && A.minY < B.minY && A.maxY > B.maxY && A.maxX > B.maxX
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
static ContainsApproximately(A: Box, B: Box, precision?: number) {
|
|
421
|
+
return (
|
|
422
|
+
approximatelyLte(A.minX, B.minX, precision) &&
|
|
423
|
+
approximatelyLte(A.minY, B.minY, precision) &&
|
|
424
|
+
approximatelyLte(B.maxX, A.maxX, precision) &&
|
|
425
|
+
approximatelyLte(B.maxY, A.maxY, precision)
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
420
429
|
static Includes(A: Box, B: Box) {
|
|
421
430
|
return Box.Collides(A, B) || Box.Contains(A, B)
|
|
422
431
|
}
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
intersectLineSegmentPolyline,
|
|
10
10
|
intersectPolys,
|
|
11
11
|
linesIntersect,
|
|
12
|
+
polygonIntersectsPolyline,
|
|
13
|
+
polygonsIntersect,
|
|
12
14
|
} from '../intersect'
|
|
13
15
|
import { approximately, pointInPolygon } from '../utils'
|
|
14
16
|
|
|
@@ -227,25 +229,6 @@ export abstract class Geometry2d {
|
|
|
227
229
|
return distanceAlongRoute / length
|
|
228
230
|
}
|
|
229
231
|
|
|
230
|
-
/** @deprecated Iterate the vertices instead. */
|
|
231
|
-
nearestPointOnLineSegment(A: VecLike, B: VecLike): Vec {
|
|
232
|
-
const { vertices } = this
|
|
233
|
-
let nearest: Vec | undefined
|
|
234
|
-
let dist = Infinity
|
|
235
|
-
let d: number, p: Vec, q: Vec
|
|
236
|
-
for (let i = 0; i < vertices.length; i++) {
|
|
237
|
-
p = vertices[i]
|
|
238
|
-
q = Vec.NearestPointOnLineSegment(A, B, p, true)
|
|
239
|
-
d = Vec.Dist2(p, q)
|
|
240
|
-
if (d < dist) {
|
|
241
|
-
dist = d
|
|
242
|
-
nearest = q
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
if (!nearest) throw Error('nearest point not found')
|
|
246
|
-
return nearest
|
|
247
|
-
}
|
|
248
|
-
|
|
249
232
|
isPointInBounds(point: VecLike, margin = 0) {
|
|
250
233
|
const { bounds } = this
|
|
251
234
|
return !(
|
|
@@ -256,6 +239,53 @@ export abstract class Geometry2d {
|
|
|
256
239
|
)
|
|
257
240
|
}
|
|
258
241
|
|
|
242
|
+
overlapsPolygon(_polygon: VecLike[]): boolean {
|
|
243
|
+
const polygon = _polygon.map((v) => Vec.From(v))
|
|
244
|
+
|
|
245
|
+
// Otherwise, check if the geometry itself overlaps the polygon
|
|
246
|
+
const { vertices, center, isFilled, isEmptyLabel, isClosed } = this
|
|
247
|
+
|
|
248
|
+
// We'll do things in order of cheapest to most expensive checks
|
|
249
|
+
|
|
250
|
+
// Skip empty labels
|
|
251
|
+
if (isEmptyLabel) return false
|
|
252
|
+
|
|
253
|
+
// If any of the geometry's vertices are inside the polygon, it's inside
|
|
254
|
+
if (vertices.some((v) => pointInPolygon(v, polygon))) {
|
|
255
|
+
return true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// If the geometry is filled and closed and its center is inside the polygon, it's inside
|
|
259
|
+
if (isClosed) {
|
|
260
|
+
if (isFilled) {
|
|
261
|
+
// If closed and filled, check if the center is inside the polygon
|
|
262
|
+
if (pointInPolygon(center, polygon)) {
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ..then, slightly more expensive check, see the geometry covers the entire polygon but not its center
|
|
267
|
+
if (polygon.every((v) => pointInPolygon(v, vertices))) {
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// If any the geometry's vertices intersect the edge of the polygon, it's inside.
|
|
273
|
+
// for example when a rotated rectangle is moved over the corner of a parent rectangle
|
|
274
|
+
// If the geometry is closed, intersect as a polygon
|
|
275
|
+
if (polygonsIntersect(polygon, vertices)) {
|
|
276
|
+
return true
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
// If the geometry is not closed, intersect as a polyline
|
|
280
|
+
if (polygonIntersectsPolyline(polygon, vertices)) {
|
|
281
|
+
return true
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// If none of the above checks passed, the geometry is outside the polygon
|
|
286
|
+
return false
|
|
287
|
+
}
|
|
288
|
+
|
|
259
289
|
transform(transform: MatModel, opts?: TransformedGeometry2dOptions): Geometry2d {
|
|
260
290
|
return new TransformedGeometry2d(this, transform, opts)
|
|
261
291
|
}
|
|
@@ -236,4 +236,8 @@ export class Group2d extends Geometry2d {
|
|
|
236
236
|
getSvgPathData(): string {
|
|
237
237
|
return this.children.map((c, i) => (c.isLabel ? '' : c.getSvgPathData(i === 0))).join(' ')
|
|
238
238
|
}
|
|
239
|
+
|
|
240
|
+
overlapsPolygon(polygon: VecLike[]): boolean {
|
|
241
|
+
return this.children.some((child) => child.overlapsPolygon(polygon))
|
|
242
|
+
}
|
|
239
243
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { atom, Atom } from '@tldraw/state'
|
|
2
|
+
import { WeakCache } from '@tldraw/utils'
|
|
3
|
+
import { Editor } from '../editor/Editor'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An Atom that is scoped to the lifetime of an Editor.
|
|
7
|
+
*
|
|
8
|
+
* This is useful for storing UI state for tldraw applications. Keeping state scoped to an editor
|
|
9
|
+
* instead of stored in a global atom can prevent issues with state being shared between editors
|
|
10
|
+
* when navigating between pages, or when multiple editor instances are used on the same page.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
export class EditorAtom<T> {
|
|
15
|
+
private states = new WeakCache<Editor, Atom<T>>()
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private name: string,
|
|
19
|
+
private getInitialState: (editor: Editor) => T
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
getAtom(editor: Editor): Atom<T> {
|
|
23
|
+
return this.states.get(editor, () => atom(this.name, this.getInitialState(editor)))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get(editor: Editor): T {
|
|
27
|
+
return this.getAtom(editor).get()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
update(editor: Editor, update: (state: T) => T): T {
|
|
31
|
+
return this.getAtom(editor).update(update)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
set(editor: Editor, state: T): T {
|
|
35
|
+
return this.getAtom(editor).set(state)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -2,15 +2,7 @@ import { EMPTY_ARRAY } from '@tldraw/state'
|
|
|
2
2
|
import { TLGroupShape, TLParentId, TLShape, TLShapeId } from '@tldraw/tlschema'
|
|
3
3
|
import { IndexKey, compact, getIndexAbove, getIndexBetween } from '@tldraw/utils'
|
|
4
4
|
import { Editor } from '../editor/Editor'
|
|
5
|
-
import {
|
|
6
|
-
import { Geometry2d } from '../primitives/geometry/Geometry2d'
|
|
7
|
-
import { Group2d } from '../primitives/geometry/Group2d'
|
|
8
|
-
import {
|
|
9
|
-
intersectPolygonPolygon,
|
|
10
|
-
polygonIntersectsPolyline,
|
|
11
|
-
polygonsIntersect,
|
|
12
|
-
} from '../primitives/intersect'
|
|
13
|
-
import { pointInPolygon } from '../primitives/utils'
|
|
5
|
+
import { intersectPolygonPolygon } from '../primitives/intersect'
|
|
14
6
|
|
|
15
7
|
/**
|
|
16
8
|
* Reparents shapes that are no longer contained within their parent shapes.
|
|
@@ -189,68 +181,10 @@ function getOverlappingShapes<T extends TLShape[] | TLShapeId[]>(
|
|
|
189
181
|
|
|
190
182
|
const geometry = editor.getShapeGeometry(childId)
|
|
191
183
|
|
|
192
|
-
return
|
|
184
|
+
return geometry.overlapsPolygon(parentPolygonInShapeShape)
|
|
193
185
|
})
|
|
194
186
|
}
|
|
195
187
|
|
|
196
|
-
/**
|
|
197
|
-
* @public
|
|
198
|
-
*/
|
|
199
|
-
export function doesGeometryOverlapPolygon(
|
|
200
|
-
geometry: Geometry2d,
|
|
201
|
-
parentCornersInShapeSpace: Vec[]
|
|
202
|
-
): boolean {
|
|
203
|
-
// If the child is a group, check if any of its children overlap the box
|
|
204
|
-
if (geometry instanceof Group2d) {
|
|
205
|
-
return geometry.children.some((childGeometry) =>
|
|
206
|
-
doesGeometryOverlapPolygon(childGeometry, parentCornersInShapeSpace)
|
|
207
|
-
)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Otherwise, check if the geometry overlaps the box
|
|
211
|
-
const { vertices, center, isFilled, isEmptyLabel, isClosed } = geometry
|
|
212
|
-
|
|
213
|
-
// We'll do things in order of cheapest to most expensive checks
|
|
214
|
-
|
|
215
|
-
// Skip empty labels
|
|
216
|
-
if (isEmptyLabel) return false
|
|
217
|
-
|
|
218
|
-
// If any of the shape's vertices are inside the occluder, it's inside
|
|
219
|
-
if (vertices.some((v) => pointInPolygon(v, parentCornersInShapeSpace))) {
|
|
220
|
-
return true
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// If the shape is filled and closed and its center is inside the parent, it's inside
|
|
224
|
-
if (isClosed) {
|
|
225
|
-
if (isFilled) {
|
|
226
|
-
// If closed and filled, check if the center is inside the parent
|
|
227
|
-
if (pointInPolygon(center, parentCornersInShapeSpace)) {
|
|
228
|
-
return true
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ..then, slightly more expensive check, see the shape covers the entire parent but not its center
|
|
232
|
-
if (parentCornersInShapeSpace.every((v) => pointInPolygon(v, vertices))) {
|
|
233
|
-
return true
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// If any the shape's vertices intersect the edge of the occluder, it's inside.
|
|
238
|
-
// for example when a rotated rectangle is moved over the corner of a parent rectangle
|
|
239
|
-
// If the child shape is closed, intersect as a polygon
|
|
240
|
-
if (polygonsIntersect(parentCornersInShapeSpace, vertices)) {
|
|
241
|
-
return true
|
|
242
|
-
}
|
|
243
|
-
} else {
|
|
244
|
-
// if the child shape is not closed, intersect as a polyline
|
|
245
|
-
if (polygonIntersectsPolyline(parentCornersInShapeSpace, vertices)) {
|
|
246
|
-
return true
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// If none of the above checks passed, the shape is outside the parent
|
|
251
|
-
return false
|
|
252
|
-
}
|
|
253
|
-
|
|
254
188
|
/**
|
|
255
189
|
* Get the shapes that will be reparented to new parents when the shapes are dropped.
|
|
256
190
|
*
|
|
@@ -354,7 +288,7 @@ export function getDroppedShapesToNewParents(
|
|
|
354
288
|
.applyToPoints(parentPagePolygon)
|
|
355
289
|
|
|
356
290
|
// If the shape overlaps the parent polygon, reparent it to that parent
|
|
357
|
-
if (
|
|
291
|
+
if (editor.getShapeGeometry(shape).overlapsPolygon(parentPolygonInShapeSpace)) {
|
|
358
292
|
// Use the util to check if the shape can be reparented to the parent
|
|
359
293
|
if (
|
|
360
294
|
!editor.getShapeUtil(parentShape).canReceiveNewChildrenOfType?.(parentShape, shape.type)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { createTLSchema } from '@tldraw/tlschema'
|
|
2
2
|
import { openDB } from 'idb'
|
|
3
|
+
import { vi } from 'vitest'
|
|
3
4
|
import { hardReset } from './hardReset'
|
|
4
5
|
import { getAllIndexDbNames, LocalIndexedDb } from './LocalIndexedDb'
|
|
5
6
|
|
|
6
7
|
const schema = createTLSchema({ shapes: {}, bindings: {} })
|
|
7
8
|
describe('LocalIndexedDb', () => {
|
|
8
9
|
beforeEach(() => {
|
|
9
|
-
|
|
10
|
+
vi.useRealTimers()
|
|
10
11
|
})
|
|
11
12
|
afterEach(async () => {
|
|
12
13
|
await hardReset({ shouldReload: false })
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PageRecordType } from '@tldraw/tlschema'
|
|
2
2
|
import { IndexKey, promiseWithResolve } from '@tldraw/utils'
|
|
3
|
-
import {
|
|
3
|
+
import { Mock, vi } from 'vitest'
|
|
4
4
|
import { createTLStore } from '../../config/createTLStore'
|
|
5
5
|
import { TLLocalSyncClient } from './TLLocalSyncClient'
|
|
6
6
|
import { hardReset } from './hardReset'
|
|
@@ -10,20 +10,20 @@ class BroadcastChannelMock {
|
|
|
10
10
|
constructor(_name: string) {
|
|
11
11
|
// noop
|
|
12
12
|
}
|
|
13
|
-
postMessage =
|
|
13
|
+
postMessage = vi.fn((_msg: any) => {
|
|
14
14
|
// noop
|
|
15
15
|
})
|
|
16
|
-
close =
|
|
16
|
+
close = vi.fn(() => {
|
|
17
17
|
// noop
|
|
18
18
|
})
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function testClient(channel = new BroadcastChannelMock('test')) {
|
|
22
22
|
const store = createTLStore({ shapeUtils: [], bindingUtils: [] })
|
|
23
|
-
const onLoad =
|
|
23
|
+
const onLoad = vi.fn(() => {
|
|
24
24
|
return
|
|
25
25
|
})
|
|
26
|
-
const onLoadError =
|
|
26
|
+
const onLoadError = vi.fn(() => {
|
|
27
27
|
return
|
|
28
28
|
})
|
|
29
29
|
const client = new TLLocalSyncClient(
|
|
@@ -36,26 +36,26 @@ function testClient(channel = new BroadcastChannelMock('test')) {
|
|
|
36
36
|
channel
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
client.db.storeSnapshot =
|
|
40
|
-
client.db.storeChanges =
|
|
39
|
+
client.db.storeSnapshot = vi.fn(() => Promise.resolve())
|
|
40
|
+
client.db.storeChanges = vi.fn(() => Promise.resolve())
|
|
41
41
|
|
|
42
42
|
return {
|
|
43
|
-
client: client as { db: { storeSnapshot:
|
|
43
|
+
client: client as { db: { storeSnapshot: Mock; storeChanges: Mock } } & typeof client,
|
|
44
44
|
store,
|
|
45
45
|
onLoad,
|
|
46
46
|
onLoadError,
|
|
47
47
|
channel,
|
|
48
48
|
tick: async () => {
|
|
49
|
-
|
|
49
|
+
vi.advanceTimersByTime(500)
|
|
50
50
|
await Promise.resolve()
|
|
51
51
|
await client.db.pending()
|
|
52
|
-
|
|
52
|
+
vi.advanceTimersByTime(500)
|
|
53
53
|
await Promise.resolve()
|
|
54
54
|
},
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
const reloadMock =
|
|
58
|
+
const reloadMock = vi.fn()
|
|
59
59
|
|
|
60
60
|
beforeAll(() => {
|
|
61
61
|
Object.defineProperty(window, 'location', {
|
|
@@ -65,14 +65,14 @@ beforeAll(() => {
|
|
|
65
65
|
})
|
|
66
66
|
|
|
67
67
|
beforeEach(() => {
|
|
68
|
-
|
|
68
|
+
vi.clearAllMocks()
|
|
69
69
|
})
|
|
70
70
|
|
|
71
71
|
afterEach(async () => {
|
|
72
72
|
await hardReset({ shouldReload: false })
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
vi.useFakeTimers()
|
|
76
76
|
|
|
77
77
|
test('the client connects on instantiation, announcing its schema', async () => {
|
|
78
78
|
const { channel, tick } = testClient()
|
|
@@ -86,7 +86,7 @@ test('the client connects on instantiation, announcing its schema', async () =>
|
|
|
86
86
|
test('when a client receives an announce with a newer schema version it reloads itself', async () => {
|
|
87
87
|
const { client, channel, onLoadError, tick } = testClient()
|
|
88
88
|
await tick()
|
|
89
|
-
|
|
89
|
+
vi.advanceTimersByTime(10000)
|
|
90
90
|
expect(reloadMock).not.toHaveBeenCalled()
|
|
91
91
|
channel.onmessage?.({
|
|
92
92
|
data: {
|
|
@@ -104,7 +104,7 @@ test('when a client receives an announce with a newer schema version it reloads
|
|
|
104
104
|
test('when a client receives an announce with a newer schema version shortly after loading it does not reload but instead reports a loadError', async () => {
|
|
105
105
|
const { client, channel, onLoadError, tick } = testClient()
|
|
106
106
|
await tick()
|
|
107
|
-
|
|
107
|
+
vi.advanceTimersByTime(1000)
|
|
108
108
|
expect(reloadMock).not.toHaveBeenCalled()
|
|
109
109
|
channel.onmessage?.({
|
|
110
110
|
data: {
|
package/src/version.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// This file is automatically generated by internal/scripts/refresh-assets.ts.
|
|
2
2
|
// Do not edit manually. Or do, I'm a comment, not a cop.
|
|
3
3
|
|
|
4
|
-
export const version = '3.16.0-next.
|
|
4
|
+
export const version = '3.16.0-next.e57e478c23e0'
|
|
5
5
|
export const publishDates = {
|
|
6
6
|
major: '2024-09-13T14:36:29.063Z',
|
|
7
|
-
minor: '2025-
|
|
8
|
-
patch: '2025-
|
|
7
|
+
minor: '2025-09-08T07:48:02.447Z',
|
|
8
|
+
patch: '2025-09-08T07:48:02.447Z',
|
|
9
9
|
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../src/lib/utils/nearestMultiple.ts"],
|
|
4
|
-
"sourcesContent": ["// Euclidean algorithm to find the GCD\nfunction gcd(a: number, b: number): number {\n\treturn b === 0 ? a : gcd(b, a % b)\n}\n\n// Returns the lowest value that the given number can be multiplied by to reach an integer\nexport function nearestMultiple(float: number) {\n\tconst decimal = float.toString().split('.')[1]\n\tif (!decimal) return 1\n\tconst denominator = Math.pow(10, decimal.length)\n\tconst numerator = parseInt(decimal, 10)\n\treturn denominator / gcd(numerator, denominator)\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,SAAS,IAAI,GAAW,GAAmB;AAC1C,SAAO,MAAM,IAAI,IAAI,IAAI,GAAG,IAAI,CAAC;AAClC;AAGO,SAAS,gBAAgB,OAAe;AAC9C,QAAM,UAAU,MAAM,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC;AAC7C,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,cAAc,KAAK,IAAI,IAAI,QAAQ,MAAM;AAC/C,QAAM,YAAY,SAAS,SAAS,EAAE;AACtC,SAAO,cAAc,IAAI,WAAW,WAAW;AAChD;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
function gcd(a, b) {
|
|
2
|
-
return b === 0 ? a : gcd(b, a % b);
|
|
3
|
-
}
|
|
4
|
-
function nearestMultiple(float) {
|
|
5
|
-
const decimal = float.toString().split(".")[1];
|
|
6
|
-
if (!decimal) return 1;
|
|
7
|
-
const denominator = Math.pow(10, decimal.length);
|
|
8
|
-
const numerator = parseInt(decimal, 10);
|
|
9
|
-
return denominator / gcd(numerator, denominator);
|
|
10
|
-
}
|
|
11
|
-
export {
|
|
12
|
-
nearestMultiple
|
|
13
|
-
};
|
|
14
|
-
//# sourceMappingURL=nearestMultiple.mjs.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../../src/lib/utils/nearestMultiple.ts"],
|
|
4
|
-
"sourcesContent": ["// Euclidean algorithm to find the GCD\nfunction gcd(a: number, b: number): number {\n\treturn b === 0 ? a : gcd(b, a % b)\n}\n\n// Returns the lowest value that the given number can be multiplied by to reach an integer\nexport function nearestMultiple(float: number) {\n\tconst decimal = float.toString().split('.')[1]\n\tif (!decimal) return 1\n\tconst denominator = Math.pow(10, decimal.length)\n\tconst numerator = parseInt(decimal, 10)\n\treturn denominator / gcd(numerator, denominator)\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,IAAI,GAAW,GAAmB;AAC1C,SAAO,MAAM,IAAI,IAAI,IAAI,GAAG,IAAI,CAAC;AAClC;AAGO,SAAS,gBAAgB,OAAe;AAC9C,QAAM,UAAU,MAAM,SAAS,EAAE,MAAM,GAAG,EAAE,CAAC;AAC7C,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,cAAc,KAAK,IAAI,IAAI,QAAQ,MAAM;AAC/C,QAAM,YAAY,SAAS,SAAS,EAAE;AACtC,SAAO,cAAc,IAAI,WAAW,WAAW;AAChD;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|