Skip to content

Commit 22e5f33

Browse files
Merge pull request #189 from gridaco/staging
Update action `canvas/focus` on home double click to locate target node on canvas
2 parents f6868e0 + 4c7d32b commit 22e5f33

10 files changed

Lines changed: 236 additions & 65 deletions

File tree

editor-packages/editor-canvas/canvas/canvas.tsx

Lines changed: 77 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
import q from "@design-sdk/query";
2121
import { LazyFrame } from "@code-editor/canvas/lazy-frame";
2222
import { HudCustomRenderers, HudSurface } from "../hud";
23-
import type { Box, XY, CanvasTransform, XYWH } from "../types";
23+
import type { Box, XY, CanvasTransform, XYWH, XYWHR } from "../types";
2424
import type { FrameOptimizationFactors } from "../frame";
2525
// import { TransformDraftingStore } from "../drafting";
2626
import {
@@ -98,23 +98,44 @@ const default_canvas_preferences: CanvsPreferences = {
9898
},
9999
};
100100

101-
type CanvasProps = CanvasCursorOptions & {
102-
viewbound: Box;
103-
onSelectNode?: (...node: ReflectSceneNode[]) => void;
104-
onMoveNodeStart?: (...node: string[]) => void;
105-
onMoveNode?: (delta: XY, ...node: string[]) => void;
106-
onMoveNodeEnd?: (delta: XY, ...node: string[]) => void;
107-
onClearSelection?: () => void;
108-
} & CanvasCustomRenderers &
101+
type CanvasProps = CanvasFocusProps &
102+
CanvasCursorOptions & {
103+
viewbound: Box;
104+
onSelectNode?: (...node: ReflectSceneNode[]) => void;
105+
onMoveNodeStart?: (...node: string[]) => void;
106+
onMoveNode?: (delta: XY, ...node: string[]) => void;
107+
onMoveNodeEnd?: (delta: XY, ...node: string[]) => void;
108+
onClearSelection?: () => void;
109+
} & CanvasCustomRenderers &
109110
CanvasState & {
110111
config?: CanvsPreferences;
111112
};
112113

114+
type CanvasFocusProps = {
115+
/**
116+
* IDs of focus nodes.
117+
*
118+
* @default []
119+
*/
120+
focus?: string[];
121+
focusRefreshkey?: string;
122+
};
123+
113124
interface HovringNode {
114125
node: ReflectSceneNode;
115126
reason: "frame-title" | "raycast" | "external";
116127
}
117128

129+
function xywhr_of(node: ReflectSceneNode): XYWHR {
130+
return [
131+
node.absoluteX,
132+
node.absoluteY,
133+
node.width,
134+
node.height,
135+
node.rotation,
136+
] as XYWHR;
137+
}
138+
118139
export function Canvas({
119140
viewbound,
120141
renderItem,
@@ -126,6 +147,8 @@ export function Canvas({
126147
filekey,
127148
pageid,
128149
nodes,
150+
focus = [],
151+
focusRefreshkey: focusRefreshKey,
129152
initialTransform,
130153
highlightedLayer,
131154
selectedNodes,
@@ -135,6 +158,11 @@ export function Canvas({
135158
cursor,
136159
...props
137160
}: CanvasProps) {
161+
const viewboundmeasured = useMemo(
162+
() => !viewbound_not_measured(viewbound),
163+
viewbound
164+
);
165+
138166
useEffect(() => {
139167
if (transformIntitialized) {
140168
return;
@@ -148,7 +176,7 @@ export function Canvas({
148176
return;
149177
}
150178

151-
if (viewbound_not_measured(viewbound)) {
179+
if (!viewboundmeasured) {
152180
return;
153181
}
154182

@@ -158,6 +186,39 @@ export function Canvas({
158186
setTransformInitialized(true);
159187
}, [viewbound]);
160188

189+
useEffect(() => {
190+
// change the canvas transform to visually fit the focus nodes.
191+
192+
if (!viewboundmeasured) {
193+
return;
194+
}
195+
196+
if (focus.length == 0) {
197+
return;
198+
}
199+
200+
// TODO: currently only the root nodes are supported to be focused.
201+
const _focus_nodes = nodes.filter((n) => focus.includes(n.id));
202+
if (_focus_nodes.length == 0) {
203+
return;
204+
}
205+
206+
const _focus_center = centerOf(
207+
viewbound,
208+
200,
209+
..._focus_nodes.map((n) => ({
210+
x: n.absoluteX,
211+
y: n.absoluteY,
212+
width: n.width,
213+
height: n.height,
214+
rotation: n.rotation,
215+
}))
216+
);
217+
218+
setOffset(_focus_center.translate);
219+
setZoom(_focus_center.scale);
220+
}, [...focus, focusRefreshKey, viewboundmeasured]);
221+
161222
const [transformIntitialized, setTransformInitialized] = useState(false);
162223
const [zoom, setZoom] = useState(initialTransform?.scale || 1);
163224
const [isZooming, setIsZooming] = useState(false);
@@ -339,9 +400,7 @@ export function Canvas({
339400
const [x, y] = [cx / zoom, cy / zoom];
340401

341402
const box = boundingbox(
342-
selected_nodes.map((d) => {
343-
return [d.absoluteX, d.absoluteY, d.width, d.height, d.rotation];
344-
}),
403+
selected_nodes.map((d) => xywhr_of(d)),
345404
2
346405
);
347406

@@ -543,29 +602,12 @@ function position_guide({
543602

544603
const guides = [];
545604
const a = boundingbox(
546-
selections.map((s) => [
547-
s.absoluteX,
548-
s.absoluteY,
549-
s.width,
550-
s.height,
551-
s.rotation,
552-
]),
605+
selections.map((s) => xywhr_of(s)),
553606
2
554607
);
555608

556609
if (hover) {
557-
const hover_box = boundingbox(
558-
[
559-
[
560-
hover.absoluteX,
561-
hover.absoluteY,
562-
hover.width,
563-
hover.height,
564-
hover.rotation,
565-
],
566-
],
567-
2
568-
);
610+
const hover_box = boundingbox([xywhr_of(hover)], 2);
569611

570612
const guide_relative_to_hover = {
571613
a: a,
@@ -580,18 +622,7 @@ function position_guide({
580622
if (selections.length === 1) {
581623
const parent = selections[0].parent;
582624
if (parent) {
583-
const parent_box = boundingbox(
584-
[
585-
[
586-
parent.absoluteX,
587-
parent.absoluteY,
588-
parent.width,
589-
parent.height,
590-
parent.rotation,
591-
],
592-
],
593-
2
594-
);
625+
const parent_box = boundingbox([xywhr_of(parent)], 2);
595626
const guide_relative_to_parent = {
596627
a: a,
597628
b: parent_box,
@@ -702,7 +733,7 @@ function auto_initial_transform(
702733
}
703734

704735
const fit_single_node = (n: ReflectSceneNode) => {
705-
return centerOf(viewbound, n);
736+
return centerOf(viewbound, 0, n);
706737
};
707738

708739
if (nodes.length === 0) {
@@ -716,7 +747,7 @@ function auto_initial_transform(
716747
};
717748
} else if (nodes.length < 20) {
718749
// fit bounds
719-
const c = centerOf(viewbound, ...nodes);
750+
const c = centerOf(viewbound, 0, ...nodes);
720751
return {
721752
xy: c.translate,
722753
scale: c.scale,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { X1Y1X2Y2 } from "types";
2+
import { centerOf, scaleToFit, scaleToFit1D } from "./center-of";
3+
4+
test("centerof", () => {
5+
const box = [0, 0, 100, 100] as X1Y1X2Y2;
6+
const fit = {
7+
x: 10,
8+
y: 10,
9+
width: 50,
10+
height: 50,
11+
rotation: 0,
12+
};
13+
14+
const r = centerOf(box, 0, fit);
15+
// to make "fit" fit into "box", we need to translate it by 10, 10 and scale it by 0.5
16+
17+
expect(r.scale).toBe(2);
18+
expect(r.center).toStrictEqual([35, 35]);
19+
// todo
20+
expect(r.translate).toStrictEqual([-20, -20]);
21+
});
22+
23+
test("scale to fit (smaller)", () => {
24+
const a = [0, 0, 100, 100] as X1Y1X2Y2;
25+
const b = [0, 0, 50, 50] as X1Y1X2Y2;
26+
expect(scaleToFit1D(100, 50)).toBe(2);
27+
expect(scaleToFit(a, b)).toBe(2);
28+
});
29+
30+
test("scale to fit (bigger)", () => {
31+
const a = [0, 0, 100, 100] as X1Y1X2Y2;
32+
const b = [0, 0, 200, 200] as X1Y1X2Y2;
33+
expect(scaleToFit(a, b)).toBe(0.5);
34+
});
35+
36+
test("scale to fit (bigger) #1", () => {
37+
const a = [0, 0, 100, 100] as X1Y1X2Y2;
38+
const b = [0, 0, 10, 200] as X1Y1X2Y2;
39+
expect(scaleToFit(a, b)).toBe(0.5);
40+
});
41+
42+
test("scale to fit (bigger) #2", () => {
43+
const a = [0, 0, 100, 100] as X1Y1X2Y2;
44+
const b = [0, 0, 200, 10] as X1Y1X2Y2;
45+
expect(scaleToFit(a, b)).toBe(0.5);
46+
});
47+
48+
test("scale to fit with margin", () => {
49+
const a = [0, 0, 100, 100] as X1Y1X2Y2;
50+
const b = [0, 0, 100, 100] as X1Y1X2Y2;
51+
expect(scaleToFit(a, b, 50)).toBe(0.5);
52+
});
53+
54+
test("scale to fit 1D", () => {
55+
expect(scaleToFit1D(100, 200)).toBe(0.5);
56+
});
57+
58+
test("scale to fit 1D", () => {
59+
expect(scaleToFit1D(100, 50, 25)).toBe(1);
60+
});

editor-packages/editor-canvas/math/center-of.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ type Rect = {
1818
*/
1919
export function centerOf(
2020
viewbound: Box,
21+
m: number = 0,
2122
...rects: Rect[]
2223
): {
2324
box: Box;
25+
/**
26+
* center of the givven rects
27+
*/
2428
center: XY;
2529
translate: XY;
2630
scale: number;
@@ -46,7 +50,7 @@ export function centerOf(
4650
// center of the box, viewbound not considered.
4751
const boxcenter: XY = [(x1 + x2) / 2, (y1 + y2) / 2];
4852
// scale factor to fix the box to the viewbound.
49-
const scale = Math.min(scaleToFit(box, viewbound), 1); // no need to zoom-in
53+
const scale = scaleToFit(viewbound, box, m);
5054
// center of the viewbound.
5155
const vbcenter: XY = [
5256
viewbound[0] + (viewbound[0] + viewbound[2]) / 2,
@@ -73,16 +77,55 @@ function rotate(x: number, y: number, r: number): [number, number] {
7377
return [x * cos - y * sin, x * sin + y * cos];
7478
}
7579

76-
function scaleToFit(a: Box, b: Box): number {
80+
/**
81+
* scale to fit a box into b box. with optional margin.
82+
* @param a box a container
83+
* @param b box b contained
84+
* @param m optional margin @default 0 (does not get affected by the scale)
85+
* @returns how much to scale should be applied to b to fit a
86+
*
87+
* @example
88+
* const a = [0, 0, 100, 100];
89+
* const b = [0, 0, 200, 200];
90+
* const m = 50;
91+
* => scaleToFit(a, b, m) === 0.4
92+
*
93+
* const a = [0, 0, 100, 100];
94+
* const b = [0, 0, 50, 50];
95+
* const m = 50;
96+
* => scaleToFit(a, b, m) === 1
97+
*
98+
*/
99+
export function scaleToFit(a: Box, b: Box, m: number = 0): number {
77100
if (!a || !b) {
78101
return 1;
79102
}
103+
80104
const [ax1, ay1, ax2, ay2] = a;
81105
const [bx1, by1, bx2, by2] = b;
106+
82107
const aw = ax2 - ax1;
83108
const ah = ay2 - ay1;
84109
const bw = bx2 - bx1;
85110
const bh = by2 - by1;
86-
const scale = Math.min(bw / aw, bh / ah);
87-
return scale;
111+
112+
const sw = scaleToFit1D(aw, bw, m);
113+
const sh = scaleToFit1D(ah, bh, m);
114+
115+
return Math.min(sw, sh);
116+
}
117+
118+
/**
119+
*
120+
* @param a line a
121+
* @param b line b
122+
* @param m margin
123+
*
124+
* @returns the scale factor to be applied to b to fit a with margin
125+
*/
126+
export function scaleToFit1D(a: number, b: number, m: number = 0): number {
127+
const aw = a;
128+
const bw = b + m * 2;
129+
130+
return aw / bw;
88131
}

editor/core/actions/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export type Action =
3131
| EditorModeAction
3232
| DesignerModeSwitchActon
3333
| SelectNodeAction
34-
| LocateNodeAction
34+
| CanvasFocusNodeAction
3535
| HighlightNodeAction
3636
| CanvasEditAction
3737
| CanvasModeAction
@@ -78,8 +78,8 @@ export interface SelectNodeAction {
7878
/**
7979
* Select and move to the node.
8080
*/
81-
export interface LocateNodeAction {
82-
type: "locate-node";
81+
export interface CanvasFocusNodeAction {
82+
type: "canvas/focus";
8383
node: string;
8484
}
8585

0 commit comments

Comments
 (0)