Skip to content

Commit 75e6488

Browse files
updat initial canvas positioning (incomplete)
1 parent a79b14b commit 75e6488

10 files changed

Lines changed: 200 additions & 48 deletions

File tree

editor-packages/editor-canvas/canvas-event-target/canvas-event-target.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export function CanvasEventTarget({
140140
{
141141
target: interactionEventTargetRef,
142142
eventOptions: {
143+
// passive to false to raise `e.preventDefault()` and `e.stopPropagation()`. - this will prevent the browser from scrolling the page, navigating with swipe gesture (safari, firefox).
143144
passive: false,
144145
},
145146
}

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

Lines changed: 108 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import {
88
OnPointerMoveHandler,
99
OnPointerDownHandler,
1010
} from "../canvas-event-target";
11-
import { get_hovering_target } from "../math";
11+
import { get_hovering_target, centerOf } from "../math";
1212
import { utils } from "@design-sdk/core";
1313
import { LazyFrame } from "@code-editor/canvas/lazy-frame";
1414
import { HudCustomRenderers, HudSurface } from "./hud-surface";
15-
import type { XY, XYWH, CanvasTransform } from "../types";
15+
import type { Box, XY, CanvasTransform } from "../types";
1616
import type { FrameOptimizationFactors } from "../frame";
1717
const designq = utils.query;
1818

@@ -28,6 +28,13 @@ interface CanvasState {
2828
highlightedLayer?: string;
2929
selectedNodes: string[];
3030
readonly?: boolean;
31+
/**
32+
* when provided, it will override the saved value or centering logic and use this transform as initial instead.
33+
*
34+
* Canvas automatically saves the last transform and also automatically calculates the initial transform based on the input's complexity.
35+
*
36+
* @default undefined
37+
*/
3138
initialTransform?: CanvasTransform;
3239
}
3340

@@ -52,7 +59,69 @@ interface HovringNode {
5259
reason: "frame-title" | "raycast" | "external";
5360
}
5461

62+
function auto_initial_transform(
63+
viewbound: Box,
64+
nodes: ReflectSceneNode[]
65+
): CanvasTransform {
66+
const _default = {
67+
scale: INITIAL_SCALE,
68+
xy: INITIAL_XY,
69+
};
70+
71+
if (!nodes || viewbound_not_measured(viewbound)) {
72+
return _default;
73+
}
74+
75+
const fit_single_node = (n: ReflectSceneNode) => {
76+
return centerOf(viewbound, n);
77+
};
78+
79+
if (nodes.length === 0) {
80+
return _default;
81+
} else if (nodes.length === 1) {
82+
// return center the node
83+
const c = fit_single_node(nodes[0]);
84+
return {
85+
xy: c.translate,
86+
scale: c.scale,
87+
};
88+
} else if (nodes.length < 20) {
89+
// fit bounds
90+
const c = centerOf(viewbound, ...nodes);
91+
return {
92+
xy: c.translate,
93+
scale: c.scale,
94+
};
95+
} else {
96+
// if more than 20 nodes, just center the first one. why? -> loading all frames at once will slow down the canvas, and in most cases, we don't have to show the whole content of the canvas.
97+
// fit first item
98+
const c = fit_single_node(nodes[0]);
99+
return {
100+
xy: c.translate,
101+
scale: c.scale,
102+
};
103+
}
104+
105+
return _default;
106+
}
107+
108+
/**
109+
* when viewbound is not measured, it means the canvas is not ready to render. and the value will be `[0,0,0,0]` (from react-use-measure)
110+
* @param viewbound visible canvas area bound
111+
* @returns
112+
*/
113+
const viewbound_not_measured = (viewbound: Box) => {
114+
return (
115+
!viewbound ||
116+
(viewbound[0] === 0 &&
117+
viewbound[1] === 0 &&
118+
viewbound[2] === 0 &&
119+
viewbound[3] === 0)
120+
);
121+
};
122+
55123
export function Canvas({
124+
viewbound,
56125
renderItem,
57126
onSelectNode,
58127
onClearSelection,
@@ -66,24 +135,48 @@ export function Canvas({
66135
config = default_canvas_preferences,
67136
...props
68137
}: {
138+
viewbound: Box;
69139
onSelectNode?: (node?: ReflectSceneNode) => void;
70140
onClearSelection?: () => void;
71141
} & CanvasCustomRenderers &
72142
CanvasState & {
73143
config?: CanvsPreferences;
74144
}) {
75-
const _canvas_state_store = new CanvasStateStore(filekey, pageid);
145+
const _canvas_state_store = useMemo(
146+
() => new CanvasStateStore(filekey, pageid),
147+
[filekey, pageid]
148+
);
76149

77-
initialTransform = initialTransform ??
78-
_canvas_state_store.getLastTransform() ?? {
79-
scale: INITIAL_SCALE,
80-
xy: INITIAL_XY,
81-
};
150+
useEffect(() => {
151+
if (transformIntitialized) {
152+
return;
153+
}
154+
155+
const _last_knwon = _canvas_state_store.getLastTransform();
156+
if (_last_knwon) {
157+
setZoom(_last_knwon.scale);
158+
setOffset(_last_knwon.xy);
159+
setTransformInitialized(true);
160+
return;
161+
}
82162

83-
const [zoom, setZoom] = useState(initialTransform.scale);
163+
if (viewbound_not_measured(viewbound)) {
164+
return;
165+
}
166+
167+
const t = auto_initial_transform(viewbound, nodes);
168+
setZoom(t.scale);
169+
setOffset(t.xy);
170+
setTransformInitialized(true);
171+
}, [viewbound]);
172+
173+
const [transformIntitialized, setTransformInitialized] = useState(false);
174+
const [zoom, setZoom] = useState(initialTransform?.scale);
84175
const [isZooming, setIsZooming] = useState(false);
85-
const [offset, setOffset] = useState<[number, number]>(initialTransform.xy);
86-
const nonscaled_offset: XY = [offset[0] / zoom, offset[1] / zoom]; // offset;
176+
const [offset, setOffset] = useState<[number, number]>(initialTransform?.xy);
177+
const nonscaled_offset: XY = offset
178+
? [offset[0] / zoom, offset[1] / zoom]
179+
: [0, 0];
87180
const [isPanning, setIsPanning] = useState(false);
88181
const cvtransform: CanvasTransform = {
89182
scale: zoom,
@@ -176,6 +269,10 @@ export function Canvas({
176269
?.map((id) => designq.find_node_by_id_under_inpage_nodes(id, nodes))
177270
.filter(Boolean);
178271

272+
if (!transformIntitialized) {
273+
return <></>;
274+
}
275+
179276
return (
180277
<>
181278
<CanvasEventTarget

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

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Box, XY } from "../types";
2+
13
type Rect = {
24
x: number;
35
y: number;
@@ -6,8 +8,6 @@ type Rect = {
68
rotation: number;
79
};
810

9-
type Box = [number, number, number, number];
10-
1111
/**
1212
* get center of a list of rectangles and the scale factor to fit them all.
1313
*
@@ -16,43 +16,70 @@ type Box = [number, number, number, number];
1616
* the output is a bounding box x1, y1, x2, y2 and scale.
1717
*/
1818
export function centerOf(
19-
bound: Box,
19+
viewbound: Box,
2020
...rects: Rect[]
2121
): {
2222
box: Box;
23-
center: [number, number];
23+
center: XY;
24+
translate: XY;
2425
scale: number;
2526
} {
27+
console.log("get center of", rects, "in", viewbound);
2628
if (!rects || rects.length === 0) {
2729
return {
28-
box: [0, 0, 0, 0],
30+
box: viewbound,
2931
center: [0, 0],
32+
translate: [
33+
viewbound[0] + (viewbound[0] + viewbound[2]) / 2,
34+
viewbound[1] + (viewbound[1] + viewbound[3]) / 2,
35+
],
3036
scale: 1,
3137
};
3238
}
3339

40+
const [x1, y1, x2, y2] = bound(...rects);
41+
// box containing the rects.
42+
const box: Box = [x1, y1, x2, y2];
43+
// center of the box, viewbound not considered.
44+
const boxcenter: XY = [(x1 + x2) / 2, (y1 + y2) / 2];
45+
// scale factor to fix the box to the viewbound.
46+
const scale = scaleToFit(box, viewbound);
47+
// center of the viewbound.
48+
const vbcenter: XY = [
49+
viewbound[0] + (viewbound[0] + viewbound[2]) / 2,
50+
viewbound[1] + (viewbound[1] + viewbound[3]) / 2,
51+
];
52+
53+
// translate x, y to center the box's center into the viewbound's center considering the scale.
54+
const translate: XY = [
55+
vbcenter[0] - boxcenter[0] * scale,
56+
vbcenter[1] - boxcenter[1] * scale,
57+
];
58+
59+
console.log(translate, scale);
60+
61+
return {
62+
box: box,
63+
center: boxcenter,
64+
translate: translate,
65+
scale: scale,
66+
};
67+
}
68+
69+
function bound(...rects: Rect[]): Box {
3470
let x1 = Infinity;
3571
let y1 = Infinity;
3672
let x2 = -Infinity;
3773
let y2 = -Infinity;
38-
3974
for (const rect of rects) {
40-
const { x, y, width: w, height: h, rotation: r } = rect;
41-
const [cx, cy] = rotate(x + w / 2, y + h / 2, r);
42-
x1 = Math.min(x1, cx - w / 2);
43-
y1 = Math.min(y1, cy - h / 2);
44-
x2 = Math.max(x2, cx + w / 2);
45-
y2 = Math.max(y2, cy + h / 2);
75+
const { x, y, width: w, height: h } = rect;
76+
// TODO: handle rotation. (no rotation for now)
77+
x1 = Math.min(x1, x);
78+
y1 = Math.min(y1, y);
79+
x2 = Math.max(x2, x + w);
80+
y2 = Math.max(y2, y + h);
4681
}
47-
const ww = x2 - x1;
48-
const hh = y2 - y1;
49-
const box: Box = [x1, y1, x2, y2];
50-
51-
return {
52-
box: box,
53-
center: [(x1 + x2) / 2, (y1 + y2) / 2],
54-
scale: scaleToFit(bound, box),
55-
};
82+
return [x1, y1, x2, y2];
5683
}
5784

5885
function rotate(x: number, y: number, r: number): [number, number] {

editor-packages/editor-canvas/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export type CanvasTransform = {
44
scale: number;
55
xy: XY;
66
};
7+
export type Box = [number, number, number, number];

editor/next.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ module.exports = withTM({
111111
},
112112
{
113113
source: "/files/:key/:id",
114-
destination: "/files/:key",
114+
destination: "/files/:key?node=:id",
115115
permanent: false,
116116
},
117117
];

editor/pages/_development/editor-canvas/index.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ export default function EditorCanvasDevPage() {
1212
<Canvas
1313
filekey="unknown"
1414
pageid="1"
15+
viewbound={[
16+
0,
17+
0,
18+
window.innerWidth ||
19+
document.documentElement.clientWidth ||
20+
document.body.clientWidth,
21+
window.innerHeight ||
22+
document.documentElement.clientHeight ||
23+
document.body.clientHeight,
24+
]}
1525
selectedNodes={[]}
1626
nodes={[]}
1727
renderItem={function (node): React.ReactNode {

editor/scaffolds/canvas/canvas.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ import { EditorAppbarFragments } from "components/editor";
44
import { Canvas } from "@code-editor/canvas";
55
import { useEditorState, useWorkspace } from "core/states";
66
import { Preview } from "scaffolds/preview";
7+
import useMeasure from "react-use-measure";
78
import { useDispatch } from "core/dispatch";
89
import { FrameTitleRenderer } from "./render/frame-title";
910
import { IsolateModeCanvas } from "./isolate-mode";
1011

1112
/**
1213
* Statefull canvas segment that contains canvas as a child, with state-data connected.
1314
*/
14-
export function VisualContentArea({ fileid }: { fileid: string }) {
15+
export function VisualContentArea() {
1516
const [state] = useEditorState();
17+
const [canvasSizingRef, canvasBounds] = useMeasure();
18+
1619
const { highlightedLayer, highlightLayer } = useWorkspace();
1720
const dispatch = useDispatch();
1821

@@ -29,7 +32,7 @@ export function VisualContentArea({ fileid }: { fileid: string }) {
2932
const [mode, setMode] = useState<"full" | "isolate">("full");
3033

3134
return (
32-
<CanvasContainer id="canvas">
35+
<CanvasContainer ref={canvasSizingRef} id="canvas">
3336
{/* <EditorAppbarFragments.Canvas /> */}
3437

3538
{isEmptyPage ? (
@@ -50,6 +53,12 @@ export function VisualContentArea({ fileid }: { fileid: string }) {
5053
>
5154
<Canvas
5255
key={selectedPage}
56+
viewbound={[
57+
canvasBounds.left,
58+
canvasBounds.top,
59+
canvasBounds.bottom,
60+
canvasBounds.right,
61+
]}
5362
filekey={state.design.key}
5463
pageid={selectedPage}
5564
selectedNodes={selectedNodes.filter(Boolean)}
@@ -84,6 +93,5 @@ export function VisualContentArea({ fileid }: { fileid: string }) {
8493
const CanvasContainer = styled.div`
8594
display: flex;
8695
flex-direction: column;
87-
max-width: calc((100vw - 200px) * 0.6); // TODO: make this dynamic
8896
height: 100%;
8997
`;

editor/scaffolds/editor/editor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function Editor({
7878
>
7979
<WorkspaceContentPanelGridLayout>
8080
<WorkspaceContentPanel flex={6}>
81-
<Canvas key={_refreshkey} fileid={state?.design?.key} />
81+
<Canvas key={_refreshkey} />
8282
</WorkspaceContentPanel>
8383
<WorkspaceContentPanel
8484
hidden={state.selectedNodes.length === 0}

editor/scaffolds/preview/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ export function Preview({
6363
: null;
6464

6565
const on_preview_result = (result: Result, __image: boolean) => {
66+
if (preview) {
67+
if (preview.code === result.code) {
68+
return;
69+
}
70+
}
6671
setPreview(result);
6772
cache.set(target.filekey, { ...result, __image });
6873
};

0 commit comments

Comments
 (0)