Skip to content

Commit 95d8ae6

Browse files
committed
whiteboard: work in progress on properly sorting out the coordinate systems
1 parent a5dc2c6 commit 95d8ae6

File tree

6 files changed

+162
-60
lines changed

6 files changed

+162
-60
lines changed

src/packages/frontend/frame-editors/whiteboard-editor/actions.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,8 @@ export class Actions extends BaseActions<State> {
259259
this._syncstring.commit();
260260
}
261261

262-
fitToScreen(id: string): void {
263-
this.set_frame_tree({ id, fitToScreen: true });
262+
fitToScreen(id: string, state: boolean = true): void {
263+
this.set_frame_tree({ id, fitToScreen: state ? true : undefined });
264264
}
265265

266266
toggleMap(id: string): void {

src/packages/frontend/frame-editors/whiteboard-editor/canvas.tsx

+133-55
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,42 @@ NOTE: This component assumes that when it is first mounted that elements
1010
is actually what it will be for the initial load, so that it can properly
1111
set the center position. Do not create with elements=[], then the real
1212
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!
1347
*/
48+
1449
import {
1550
ClipboardEvent,
1651
ReactNode,
@@ -20,7 +55,7 @@ import {
2055
useRef,
2156
useState,
2257
} from "react";
23-
import { Element, ElementType, Point } from "./types";
58+
import { Element, ElementType, Point, Rect } from "./types";
2459
import { Tool, TOOLS } from "./tools/spec";
2560
import RenderElement from "./elements/render";
2661
import Focused, {
@@ -34,14 +69,17 @@ import { useFrameContext } from "./hooks";
3469
import usePinchToZoom from "@cocalc/frontend/frame-editors/frame-tree/pinch-to-zoom";
3570
import Grid from "./elements/grid";
3671
import {
72+
compressPath,
3773
fontSizeToZoom,
74+
ZOOM100,
3875
getPageSpan,
3976
getPosition,
77+
fitRectToRect,
4078
getOverlappingElements,
4179
pointEqual,
4280
pointRound,
4381
pointsToRect,
44-
compressPath,
82+
rectSpan,
4583
MAX_ELEMENTS,
4684
} from "./math";
4785
import { throttle } from "lodash";
@@ -70,7 +108,6 @@ interface Props {
70108
margin?: number;
71109
readOnly?: boolean;
72110
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
74111
evtToDataRef?: MutableRefObject<Function | null>;
75112
isNavigator?: boolean; // is the navigator, so hide the grid, don't save window, don't scroll, don't move
76113
}
@@ -83,7 +120,6 @@ export default function Canvas({
83120
margin,
84121
readOnly,
85122
selectedTool,
86-
fitToScreen,
87123
evtToDataRef,
88124
isNavigator,
89125
}: Props) {
@@ -128,7 +164,7 @@ export default function Canvas({
128164
useEffect(() => {
129165
if (isNavigator) return;
130166
if (canvasScale == lastScale) return;
131-
const ctr = getCenterPosition();
167+
const ctr = getCenterPositionWindow();
132168
if (ctr == null) return;
133169
const { x, y } = ctr;
134170
const new_x = (lastScale / canvasScale) * x;
@@ -163,26 +199,24 @@ export default function Canvas({
163199
if (isNavigator || restoring.current) return;
164200
const center = frame.desc.get("visibleWindowCenter")?.toJS();
165201
if (center == null) return;
166-
setCenterPosition(center.x, center.y);
202+
setCenterPositionData(center);
167203
}, [frame.desc.get("visibleWindowCenter")]);
168204

169205
useEffect(() => {
170206
if (isNavigator) return;
171207
const center = frame.desc.get("center")?.toJS();
172208
if (center != null) {
173-
setCenterPosition(center.x, center.y);
209+
setCenterPositionData(center);
174210
}
175211
restoring.current = false;
176212
}, []);
177213

178214
// save center position, so can be restored later.
179215
const saveCenterPosition = useDebouncedCallback(() => {
180216
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);
186220
if (center != null) {
187221
frame.actions.saveCenter(frame.id, center);
188222
}
@@ -208,24 +242,25 @@ export default function Canvas({
208242
return timerParams(frame.desc.get("timerId") ?? 0);
209243
}
210244

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 {
213248
const c = canvasRef.current;
214249
if (c == null) return;
215250
const rect = c.getBoundingClientRect();
216251
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.
218254
return {
219255
x: c.scrollLeft + rect.width / 2,
220256
y: c.scrollTop + rect.height / 2,
221257
};
222258
}
223259

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();
229264
if (cur == null) return;
230265
const delta_x = t.x - cur.x;
231266
const delta_y = t.y - cur.y;
@@ -237,16 +272,31 @@ export default function Canvas({
237272
c.scrollTop = scrollTopGoal;
238273
}
239274

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
240278
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+
}
243293
}
244-
}, [fitToScreen]);
294+
}, [frame.desc.get("fitToScreen")]);
245295

246296
function processElement(element, isNavRectangle = false) {
247297
const { id, rotate } = element;
248298
const { x, y, z, w, h } = getPosition(element);
249-
const t = transforms.dataToWindow(x, y, z);
299+
const t = transforms.dataToWindowNoScale(x, y, z);
250300

251301
// This just shows blue boxes in the nav map, instead of actually
252302
// rendering something.
@@ -440,10 +490,9 @@ export default function Canvas({
440490
h: yMax - yMin,
441491
z: MAX_ELEMENTS + 1,
442492
type: "frame",
443-
data: { color: "black", radius: 0.5 },
493+
data: { color: "#888", radius: 0.5 },
444494
style: {
445-
background: "lightgrey",
446-
borderRadius: "5px",
495+
background: "rgb(200,200,200,0.2)",
447496
},
448497
},
449498
true
@@ -454,17 +503,53 @@ export default function Canvas({
454503
}
455504
}
456505

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 {
460513
const c = canvasRef.current;
461514
if (c == null) return { x: 0, y: 0 };
462515
const rect = c.getBoundingClientRect();
463516
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 }));
468553
}
469554
if (evtToDataRef != null) {
470555
// share with outside world
@@ -540,7 +625,7 @@ export default function Canvas({
540625
const { scrollLeft, scrollTop } = elt;
541626
// width and height of visible window
542627
const { width, height } = elt.getBoundingClientRect();
543-
const { x: xMin, y: yMin } = transforms.windowToData(
628+
const { x: xMin, y: yMin } = transforms.windowToDataNoScale(
544629
scrollLeft / canvasScale,
545630
scrollTop / canvasScale
546631
);
@@ -584,8 +669,8 @@ export default function Canvas({
584669
const p0 = mousePath.current[0];
585670
const p1 = mousePath.current[1];
586671
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)
589674
);
590675
if (selectedTool == "frame") {
591676
// make a frame at the selection. Note that we put
@@ -613,7 +698,8 @@ export default function Canvas({
613698
return;
614699
}
615700
ignoreNextClick.current = true;
616-
const toData = ({ x, y }) => pointRound(transforms.windowToData(x, y));
701+
const toData = ({ x, y }) =>
702+
pointRound(transforms.windowToDataNoScale(x, y));
617703
const { x, y } = toData(mousePath.current[0]);
618704
let xMin = x,
619705
xMax = x;
@@ -775,15 +861,11 @@ export default function Canvas({
775861
const pos = getMousePos(mousePos.current);
776862
if (pos != null) {
777863
const { x, y } = pos;
778-
target = transforms.windowToData(x, y);
864+
target = transforms.windowToDataNoScale(x, y);
779865
} else {
780-
const point = getCenterPosition();
866+
const point = getCenterPositionWindow();
781867
if (point != null) {
782-
target = windowToData({
783-
transforms,
784-
canvasScale,
785-
point,
786-
});
868+
target = windowToData(point);
787869
}
788870
}
789871

@@ -869,12 +951,12 @@ function getTransforms(
869951
margin,
870952
scale
871953
): {
872-
dataToWindow: (
954+
dataToWindowNoScale: (
873955
x: number,
874956
y: number,
875957
z?: number
876958
) => { 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 };
878960
width: number;
879961
height: number;
880962
xMin: number;
@@ -899,22 +981,22 @@ function getTransforms(
899981
let { xMin, yMin, xMax, yMax, zMin, zMax } = getPageSpan(elements, margin);
900982
const zMap = zIndexMap(elements);
901983

902-
function dataToWindow(x, y, z?) {
984+
function dataToWindowNoScale(x, y, z?) {
903985
return {
904986
x: (x ?? 0) - xMin,
905987
y: (y ?? 0) - yMin,
906988
z: zMap[z ?? 0] ?? 0,
907989
};
908990
}
909-
function windowToData(x, y) {
991+
function windowToDataNoScale(x, y) {
910992
return {
911993
x: (x ?? 0) + xMin,
912994
y: (y ?? 0) + yMin,
913995
};
914996
}
915997
return {
916-
dataToWindow,
917-
windowToData,
998+
dataToWindowNoScale,
999+
windowToDataNoScale,
9181000
width: xMax - xMin,
9191001
height: yMax - yMin,
9201002
xMin,
@@ -941,10 +1023,6 @@ function zIndexMap(elements: Element[]) {
9411023
return zMap;
9421024
}
9431025

944-
function windowToData({ transforms, canvasScale, point }) {
945-
return transforms.windowToData(point.x / canvasScale, point.y / canvasScale);
946-
}
947-
9481026
function getSelectedElements({
9491027
elements,
9501028
selection,

0 commit comments

Comments
 (0)