@@ -10,7 +10,42 @@ NOTE: This component assumes that when it is first mounted that elements
10
10
is actually what it will be for the initial load, so that it can properly
11
11
set the center position. Do not create with elements=[], then the real
12
12
elements.
13
+
14
+ COORDINATES:
15
+
16
+ Functions below that depend on the coordinate system should
17
+ be named ending with either "Data", "Window" or "Viewport",
18
+ depending on what coordinates they use. Those coordinate
19
+ systems are defined below.
20
+
21
+ data coordinates:
22
+ - what all the elements use in defining themselves.
23
+ - this is an x,y infinite plane, with of course the
24
+ x-axis going down (computer graphics, after all)
25
+ - objects also have an arbitrary z coordinate
26
+
27
+ window coordinates:
28
+ - this is the div we're drawing everything to the screen using
29
+ - when we draw an element on the screen, we used position absolute
30
+ with window coordinates.
31
+ - also x,y with x-axis going down. However, negative
32
+ coordinates can never be visible.
33
+ - scrolling the visible window does not change these coordinates.
34
+ - this is related to data coordinates by a translation followed
35
+ by scaling.
36
+ - we also translate all z-coordinates to be in an explicit interval [0,MAX]
37
+ via an increasing (but not necessarily linear!) function.
38
+
39
+ viewport coordinates:
40
+ - this is the coordinate system used when clicking with the mouse
41
+ and getting an event e.clientX, e.clientY. The upper left point (0,0)
42
+ is the upper left corner of the browser window.
43
+ - this is related to window coordinates by translation, where the parameters
44
+ are the position of the canvas div and its scrollTop, scrollLeft attributes.
45
+ Thus the transform back and forth between window and portal coordinates
46
+ is extra tricky, because it can change any time at any time!
13
47
*/
48
+
14
49
import {
15
50
ClipboardEvent ,
16
51
ReactNode ,
@@ -20,7 +55,7 @@ import {
20
55
useRef ,
21
56
useState ,
22
57
} from "react" ;
23
- import { Element , ElementType , Point } from "./types" ;
58
+ import { Element , ElementType , Point , Rect } from "./types" ;
24
59
import { Tool , TOOLS } from "./tools/spec" ;
25
60
import RenderElement from "./elements/render" ;
26
61
import Focused , {
@@ -34,14 +69,17 @@ import { useFrameContext } from "./hooks";
34
69
import usePinchToZoom from "@cocalc/frontend/frame-editors/frame-tree/pinch-to-zoom" ;
35
70
import Grid from "./elements/grid" ;
36
71
import {
72
+ compressPath ,
37
73
fontSizeToZoom ,
74
+ ZOOM100 ,
38
75
getPageSpan ,
39
76
getPosition ,
77
+ fitRectToRect ,
40
78
getOverlappingElements ,
41
79
pointEqual ,
42
80
pointRound ,
43
81
pointsToRect ,
44
- compressPath ,
82
+ rectSpan ,
45
83
MAX_ELEMENTS ,
46
84
} from "./math" ;
47
85
import { throttle } from "lodash" ;
@@ -70,7 +108,6 @@ interface Props {
70
108
margin ?: number ;
71
109
readOnly ?: boolean ;
72
110
tool ?: Tool ;
73
- fitToScreen ?: boolean ; // if set, compute data then set font_size to get zoom (plus offset) to everything is visible properly on the page; also set fitToScreen back to false in frame tree data
74
111
evtToDataRef ?: MutableRefObject < Function | null > ;
75
112
isNavigator ?: boolean ; // is the navigator, so hide the grid, don't save window, don't scroll, don't move
76
113
}
@@ -83,7 +120,6 @@ export default function Canvas({
83
120
margin,
84
121
readOnly,
85
122
selectedTool,
86
- fitToScreen,
87
123
evtToDataRef,
88
124
isNavigator,
89
125
} : Props ) {
@@ -128,7 +164,7 @@ export default function Canvas({
128
164
useEffect ( ( ) => {
129
165
if ( isNavigator ) return ;
130
166
if ( canvasScale == lastScale ) return ;
131
- const ctr = getCenterPosition ( ) ;
167
+ const ctr = getCenterPositionWindow ( ) ;
132
168
if ( ctr == null ) return ;
133
169
const { x, y } = ctr ;
134
170
const new_x = ( lastScale / canvasScale ) * x ;
@@ -163,26 +199,24 @@ export default function Canvas({
163
199
if ( isNavigator || restoring . current ) return ;
164
200
const center = frame . desc . get ( "visibleWindowCenter" ) ?. toJS ( ) ;
165
201
if ( center == null ) return ;
166
- setCenterPosition ( center . x , center . y ) ;
202
+ setCenterPositionData ( center ) ;
167
203
} , [ frame . desc . get ( "visibleWindowCenter" ) ] ) ;
168
204
169
205
useEffect ( ( ) => {
170
206
if ( isNavigator ) return ;
171
207
const center = frame . desc . get ( "center" ) ?. toJS ( ) ;
172
208
if ( center != null ) {
173
- setCenterPosition ( center . x , center . y ) ;
209
+ setCenterPositionData ( center ) ;
174
210
}
175
211
restoring . current = false ;
176
212
} , [ ] ) ;
177
213
178
214
// save center position, so can be restored later.
179
215
const saveCenterPosition = useDebouncedCallback ( ( ) => {
180
216
if ( isNavigator || restoring . current ) return ;
181
- const center = windowToData ( {
182
- transforms,
183
- canvasScale,
184
- point : getCenterPosition ( ) ,
185
- } ) ;
217
+ const c = getCenterPositionWindow ( ) ;
218
+ if ( c == null ) return ;
219
+ const center = windowToData ( c ) ;
186
220
if ( center != null ) {
187
221
frame . actions . saveCenter ( frame . id , center ) ;
188
222
}
@@ -208,24 +242,25 @@ export default function Canvas({
208
242
return timerParams ( frame . desc . get ( "timerId" ) ?? 0 ) ;
209
243
}
210
244
211
- // gets center of visible canvas in *window* coordinates.
212
- function getCenterPosition ( ) : { x : number ; y : number } | undefined {
245
+ // get window coordinates of what is currently displayed in the exact
246
+ // center of the viewport.
247
+ function getCenterPositionWindow ( ) : { x : number ; y : number } | undefined {
213
248
const c = canvasRef . current ;
214
249
if ( c == null ) return ;
215
250
const rect = c . getBoundingClientRect ( ) ;
216
251
if ( rect == null ) return ;
217
- // the current center
252
+ // the current center of the viewport, but in window coordinates, i.e.,
253
+ // absolute coordinates into the canvas div.
218
254
return {
219
255
x : c . scrollLeft + rect . width / 2 ,
220
256
y : c . scrollTop + rect . height / 2 ,
221
257
} ;
222
258
}
223
259
224
- function setCenterPosition ( x : number , y : number ) {
225
- const t = transforms . dataToWindow ( x , y ) ;
226
- t . x *= canvasScale ;
227
- t . y *= canvasScale ;
228
- const cur = getCenterPosition ( ) ;
260
+ // set center position in Data coordinates.
261
+ function setCenterPositionData ( { x, y } : Point ) : void {
262
+ const t = dataToWindow ( { x, y } ) ;
263
+ const cur = getCenterPositionWindow ( ) ;
229
264
if ( cur == null ) return ;
230
265
const delta_x = t . x - cur . x ;
231
266
const delta_y = t . y - cur . y ;
@@ -237,16 +272,31 @@ export default function Canvas({
237
272
c . scrollTop = scrollTopGoal ;
238
273
}
239
274
275
+ // when fitToScreen is true, compute data then set font_size to get zoom (plus offset) to
276
+ // everything is visible properly on the page; also set fitToScreen back to false in
277
+ // frame tree data
240
278
useEffect ( ( ) => {
241
- if ( fitToScreen ) {
242
- frame . actions . set_frame_tree ( { id : frame . id , fitToScreen : false } ) ;
279
+ if ( frame . desc . get ( "fitToScreen" ) && ! isNavigator ) {
280
+ try {
281
+ const viewport = getViewportData ( ) ;
282
+ if ( viewport == null ) return ;
283
+ const rect = rectSpan ( elements ) ;
284
+ const { scale } = fitRectToRect ( rect , viewport ) ;
285
+ frame . actions . set_font_size ( frame . id , ( font_size ?? ZOOM100 ) * scale ) ;
286
+ setCenterPositionData ( {
287
+ x : rect . x + rect . w / 2 ,
288
+ y : rect . y + rect . h / 2 ,
289
+ } ) ;
290
+ } finally {
291
+ frame . actions . fitToScreen ( frame . id , false ) ;
292
+ }
243
293
}
244
- } , [ fitToScreen ] ) ;
294
+ } , [ frame . desc . get ( " fitToScreen" ) ] ) ;
245
295
246
296
function processElement ( element , isNavRectangle = false ) {
247
297
const { id, rotate } = element ;
248
298
const { x, y, z, w, h } = getPosition ( element ) ;
249
- const t = transforms . dataToWindow ( x , y , z ) ;
299
+ const t = transforms . dataToWindowNoScale ( x , y , z ) ;
250
300
251
301
// This just shows blue boxes in the nav map, instead of actually
252
302
// rendering something.
@@ -440,10 +490,9 @@ export default function Canvas({
440
490
h : yMax - yMin ,
441
491
z : MAX_ELEMENTS + 1 ,
442
492
type : "frame" ,
443
- data : { color : "black " , radius : 0.5 } ,
493
+ data : { color : "#888 " , radius : 0.5 } ,
444
494
style : {
445
- background : "lightgrey" ,
446
- borderRadius : "5px" ,
495
+ background : "rgb(200,200,200,0.2)" ,
447
496
} ,
448
497
} ,
449
498
true
@@ -454,17 +503,53 @@ export default function Canvas({
454
503
}
455
504
}
456
505
457
- // convert mouse event to coordinates in data space
458
- function evtToData ( e ) : { x : number ; y : number } {
459
- const { clientX, clientY } = e ;
506
+ /****************************************************/
507
+ // Full coordinate transforms back and forth!
508
+ // Note, transforms has coordinate transforms without scaling
509
+ // in it, since that's very useful. However, these two
510
+ // below are the full transforms.
511
+
512
+ function viewportToWindow ( { x, y } : Point ) : Point {
460
513
const c = canvasRef . current ;
461
514
if ( c == null ) return { x : 0 , y : 0 } ;
462
515
const rect = c . getBoundingClientRect ( ) ;
463
516
if ( rect == null ) return { x : 0 , y : 0 } ;
464
- // Coordinates inside the canvas div.
465
- const divX = c . scrollLeft + clientX - rect . left ;
466
- const divY = c . scrollTop + clientY - rect . top ;
467
- return transforms . windowToData ( divX / canvasScale , divY / canvasScale ) ;
517
+ return {
518
+ x : c . scrollLeft + x - rect . left ,
519
+ y : c . scrollTop + y - rect . top ,
520
+ } ;
521
+ }
522
+
523
+ // window coords to data coords
524
+ function windowToData ( { x, y } : Point ) : Point {
525
+ return transforms . windowToDataNoScale ( x / canvasScale , y / canvasScale ) ;
526
+ }
527
+ function dataToWindow ( { x, y } : Point ) : Point {
528
+ const p = transforms . dataToWindowNoScale ( x , y ) ;
529
+ p . x *= canvasScale ;
530
+ p . y *= canvasScale ;
531
+ return { x : p . x , y : p . y } ;
532
+ }
533
+ /****************************************************/
534
+ // The viewport in *data* coordinates
535
+ function getViewportData ( ) : Rect | undefined {
536
+ const v = getViewportWindow ( ) ;
537
+ if ( v == null ) return ;
538
+ const { x, y } = windowToData ( v ) ;
539
+ return { x, y, w : v . w / canvasScale , h : v . h / canvasScale } ;
540
+ }
541
+ // The viewport in *window* coordinates
542
+ function getViewportWindow ( ) : Rect | undefined {
543
+ const c = canvasRef . current ;
544
+ if ( c == null ) return ;
545
+ const { width : w , height : h } = c . getBoundingClientRect ( ) ;
546
+ return { x : c . scrollLeft , y : c . scrollTop , w, h } ;
547
+ }
548
+
549
+ // convert mouse event to coordinates in data space
550
+ function evtToData ( e ) : Point {
551
+ const { clientX : x , clientY : y } = e ;
552
+ return windowToData ( viewportToWindow ( { x, y } ) ) ;
468
553
}
469
554
if ( evtToDataRef != null ) {
470
555
// share with outside world
@@ -540,7 +625,7 @@ export default function Canvas({
540
625
const { scrollLeft, scrollTop } = elt ;
541
626
// width and height of visible window
542
627
const { width, height } = elt . getBoundingClientRect ( ) ;
543
- const { x : xMin , y : yMin } = transforms . windowToData (
628
+ const { x : xMin , y : yMin } = transforms . windowToDataNoScale (
544
629
scrollLeft / canvasScale ,
545
630
scrollTop / canvasScale
546
631
) ;
@@ -584,8 +669,8 @@ export default function Canvas({
584
669
const p0 = mousePath . current [ 0 ] ;
585
670
const p1 = mousePath . current [ 1 ] ;
586
671
const rect = pointsToRect (
587
- transforms . windowToData ( p0 . x , p0 . y ) ,
588
- transforms . windowToData ( p1 . x , p1 . y )
672
+ transforms . windowToDataNoScale ( p0 . x , p0 . y ) ,
673
+ transforms . windowToDataNoScale ( p1 . x , p1 . y )
589
674
) ;
590
675
if ( selectedTool == "frame" ) {
591
676
// make a frame at the selection. Note that we put
@@ -613,7 +698,8 @@ export default function Canvas({
613
698
return ;
614
699
}
615
700
ignoreNextClick . current = true ;
616
- const toData = ( { x, y } ) => pointRound ( transforms . windowToData ( x , y ) ) ;
701
+ const toData = ( { x, y } ) =>
702
+ pointRound ( transforms . windowToDataNoScale ( x , y ) ) ;
617
703
const { x, y } = toData ( mousePath . current [ 0 ] ) ;
618
704
let xMin = x ,
619
705
xMax = x ;
@@ -775,15 +861,11 @@ export default function Canvas({
775
861
const pos = getMousePos ( mousePos . current ) ;
776
862
if ( pos != null ) {
777
863
const { x, y } = pos ;
778
- target = transforms . windowToData ( x , y ) ;
864
+ target = transforms . windowToDataNoScale ( x , y ) ;
779
865
} else {
780
- const point = getCenterPosition ( ) ;
866
+ const point = getCenterPositionWindow ( ) ;
781
867
if ( point != null ) {
782
- target = windowToData ( {
783
- transforms,
784
- canvasScale,
785
- point,
786
- } ) ;
868
+ target = windowToData ( point ) ;
787
869
}
788
870
}
789
871
@@ -869,12 +951,12 @@ function getTransforms(
869
951
margin ,
870
952
scale
871
953
) : {
872
- dataToWindow : (
954
+ dataToWindowNoScale : (
873
955
x : number ,
874
956
y : number ,
875
957
z ?: number
876
958
) => { x : number ; y : number ; z : number } ;
877
- windowToData : ( x : number , y : number ) => { x : number ; y : number } ;
959
+ windowToDataNoScale : ( x : number , y : number ) => { x : number ; y : number } ;
878
960
width : number ;
879
961
height : number ;
880
962
xMin : number ;
@@ -899,22 +981,22 @@ function getTransforms(
899
981
let { xMin, yMin, xMax, yMax, zMin, zMax } = getPageSpan ( elements , margin ) ;
900
982
const zMap = zIndexMap ( elements ) ;
901
983
902
- function dataToWindow ( x , y , z ?) {
984
+ function dataToWindowNoScale ( x , y , z ?) {
903
985
return {
904
986
x : ( x ?? 0 ) - xMin ,
905
987
y : ( y ?? 0 ) - yMin ,
906
988
z : zMap [ z ?? 0 ] ?? 0 ,
907
989
} ;
908
990
}
909
- function windowToData ( x , y ) {
991
+ function windowToDataNoScale ( x , y ) {
910
992
return {
911
993
x : ( x ?? 0 ) + xMin ,
912
994
y : ( y ?? 0 ) + yMin ,
913
995
} ;
914
996
}
915
997
return {
916
- dataToWindow ,
917
- windowToData ,
998
+ dataToWindowNoScale ,
999
+ windowToDataNoScale ,
918
1000
width : xMax - xMin ,
919
1001
height : yMax - yMin ,
920
1002
xMin,
@@ -941,10 +1023,6 @@ function zIndexMap(elements: Element[]) {
941
1023
return zMap ;
942
1024
}
943
1025
944
- function windowToData ( { transforms, canvasScale, point } ) {
945
- return transforms . windowToData ( point . x / canvasScale , point . y / canvasScale ) ;
946
- }
947
-
948
1026
function getSelectedElements ( {
949
1027
elements,
950
1028
selection,
0 commit comments