diff --git a/editor-packages/editor-canvas/canvas-event-target/canvas-event-target.tsx b/editor-packages/editor-canvas/canvas-event-target/canvas-event-target.tsx index ac55cbdf..6a26f436 100644 --- a/editor-packages/editor-canvas/canvas-event-target/canvas-event-target.tsx +++ b/editor-packages/editor-canvas/canvas-event-target/canvas-event-target.tsx @@ -25,6 +25,7 @@ export type OnPointerDownHandler = ( const ZOOM_WITH_SCROLL_SENSITIVITY = 0.001; export function CanvasEventTarget({ + onZoomToFit, onPanning, onPanningStart, onPanningEnd, @@ -40,6 +41,7 @@ export function CanvasEventTarget({ onDragEnd, children, }: { + onZoomToFit?: () => void; onPanning: OnPanningHandler; onPanningStart: OnPanningHandler; onPanningEnd: OnPanningHandler; @@ -69,6 +71,10 @@ export function CanvasEventTarget({ if (e.code === "Space") { setIsSpacebarPressed(true); } + // if shift + 0 + else if (e.code === "Digit0" && e.shiftKey) { + onZoomToFit?.(); + } }; const ku = (e) => { if (e.code === "Space") { @@ -202,7 +208,6 @@ export function CanvasEventTarget({ style={{ position: "absolute", inset: 0, - background: "transparent", overflow: "hidden", touchAction: "none", cursor: isSpacebarPressed ? "grab" : "default", diff --git a/editor-packages/editor-canvas/canvas/canvas.tsx b/editor-packages/editor-canvas/canvas/canvas.tsx index 81cf6839..4fd713a2 100644 --- a/editor-packages/editor-canvas/canvas/canvas.tsx +++ b/editor-packages/editor-canvas/canvas/canvas.tsx @@ -14,23 +14,28 @@ import { centerOf, edge_scrolling, target_of_area, + boundingbox, + is_point_inside_box, } from "../math"; import q from "@design-sdk/query"; import { LazyFrame } from "@code-editor/canvas/lazy-frame"; import { HudCustomRenderers, HudSurface } from "../hud"; import type { Box, XY, CanvasTransform, XYWH } from "../types"; import type { FrameOptimizationFactors } from "../frame"; +// import { TransformDraftingStore } from "../drafting"; +import { + CANVAS_LAYER_HOVER_HIT_MARGIN, + CANVAS_INITIAL_XY, + CANVAS_INITIAL_SCALE, + CANVAS_MIN_ZOOM, +} from "../k"; import { ContextMenuRoot as ContextMenu } from "@editor-ui/context-menu"; import styled from "@emotion/styled"; -const INITIAL_SCALE = 0.5; -const INITIAL_XY: XY = [0, 0]; -const LAYER_HOVER_HIT_MARGIN = 3.5; -const MIN_ZOOM = 0.02; - interface CanvasState { pageid: string; filekey: string; + backgroundColor?: React.CSSProperties["backgroundColor"]; nodes: ReflectSceneNode[]; highlightedLayer?: string; selectedNodes: string[]; @@ -56,6 +61,7 @@ type CanvasCustomRenderers = HudCustomRenderers & { interface CanvsPreferences { can_highlight_selected_layer?: boolean; marquee: MarqueeOprions; + grouping: GroupingOptions; } interface MarqueeOprions { @@ -67,11 +73,22 @@ interface MarqueeOprions { disabled?: boolean; } +interface GroupingOptions { + /** + * disable grouping - multiple selections will not be grouped. + * @default false + **/ + disabled?: boolean; +} + const default_canvas_preferences: CanvsPreferences = { can_highlight_selected_layer: false, marquee: { disabled: false, }, + grouping: { + disabled: false, + }, }; interface HovringNode { @@ -82,6 +99,9 @@ interface HovringNode { export function Canvas({ viewbound, renderItem, + onMoveNodeStart, + onMoveNode, + onMoveNodeEnd, onSelectNode: _cb_onSelectNode, onClearSelection, filekey, @@ -92,20 +112,19 @@ export function Canvas({ selectedNodes, readonly = true, config = default_canvas_preferences, + backgroundColor, ...props }: { viewbound: Box; onSelectNode?: (...node: ReflectSceneNode[]) => void; + onMoveNodeStart?: (...node: string[]) => void; + onMoveNode?: (delta: XY, ...node: string[]) => void; + onMoveNodeEnd?: (delta: XY, ...node: string[]) => void; onClearSelection?: () => void; } & CanvasCustomRenderers & CanvasState & { config?: CanvsPreferences; }) { - const _canvas_state_store = useMemo( - () => new CanvasStateStore(filekey, pageid), - [filekey, pageid] - ); - useEffect(() => { if (transformIntitialized) { return; @@ -139,8 +158,14 @@ export function Canvas({ ? [offset[0] / zoom, offset[1] / zoom] : [0, 0]; const [isPanning, setIsPanning] = useState(false); - const [isDraggomg, setIsDragging] = useState(false); - const [marquee, setMarquee] = useState(null); + const [isDraggimg, setIsDragging] = useState(false); + const [isMovingSelections, setIsMovingSelections] = useState(false); + const [marquee, setMarquee] = useState(null); + + const _canvas_state_store = useMemo( + () => new CanvasStateStore(filekey, pageid), + [filekey, pageid] + ); const cvtransform: CanvasTransform = { scale: zoom, @@ -195,7 +220,7 @@ export function Canvas({ }, [marquee]); const onPointerMove: OnPointerMoveHandler = (state) => { - if (isPanning || isZooming || isDraggomg) { + if (isPanning || isZooming || isDraggimg) { // don't perform hover calculation while transforming. return; } @@ -204,8 +229,9 @@ export function Canvas({ tree: nodes, zoom: zoom, offset: nonscaled_offset, - margin: LAYER_HOVER_HIT_MARGIN, + margin: CANVAS_LAYER_HOVER_HIT_MARGIN, reverse: true, + ignore: (n) => selectedNodes.includes(n.id), }); if (!hovering) { @@ -224,9 +250,16 @@ export function Canvas({ }; const onPointerDown: OnPointerDownHandler = (state) => { + const [x, y] = [state.event.clientX, state.event.clientY]; + if (isPanning || isZooming) { return; } + + if (shouldStartMoveSelections([x, y])) { + return; // don't do anything. onDrag will handle this. only block the event. + } + if (hoveringLayer) { switch (hoveringLayer.reason) { case "frame-title": @@ -255,7 +288,7 @@ export function Canvas({ // the origin point of the zooming point in x, y const [ox, oy]: XY = state.origin; - const newzoom = Math.max(zoom + zoomdelta, MIN_ZOOM); + const newzoom = Math.max(zoom + zoomdelta, CANVAS_MIN_ZOOM); // calculate the offset that should be applied with scale with css transform. const [newx, newy] = [ @@ -276,9 +309,35 @@ export function Canvas({ const [x, y] = s.initial; const [ox, oy] = offset; const [x1, y1] = [x - ox, y - oy]; + + // if dragging a selection group bounding box, move the selected items. + if (shouldStartMoveSelections([x, y])) { + setIsMovingSelections(true); + onMoveNodeStart?.(...selectedNodes); + return; + } + + // else, clear and start a marquee + onClearSelection(); setMarquee([x1, y1, 0, 0]); }; + const shouldStartMoveSelections = ([cx, cy]) => { + // x, y is a client x, y. + const [ox, oy] = offset; + [cx, cy] = [cx - ox, cy - oy]; + const [x, y] = [cx / zoom, cy / zoom]; + + const box = boundingbox( + selected_nodes.map((d) => { + return [d.absoluteX, d.absoluteY, d.width, d.height, d.rotation]; + }), + 2 + ); + + return is_point_inside_box([x, y], box); + }; + const onDrag: OnDragHandler = (s) => { const [ox, oy] = offset; const [x, y] = [ @@ -290,6 +349,11 @@ export function Canvas({ const [x1, y1] = [x - ox, y - oy]; + if (isMovingSelections) { + const [dx, dy] = s.delta; + onMoveNode?.([dx / zoom, dy / zoom], ...selectedNodes); + } + if (marquee) { const [w, h] = [ x1 - marquee[0], // w @@ -309,6 +373,18 @@ export function Canvas({ const onDragEnd: OnDragHandler = (s) => { setMarquee(null); setIsDragging(false); + if (isMovingSelections) { + const [ix, iy] = s.initial; + const [fx, fy] = [ + //@ts-ignore + s.event.clientX, + //@ts-ignore + s.event.clientY, + ]; + + onMoveNodeEnd?.([(fx - ix) / zoom, (fy - iy) / zoom], ...selectedNodes); + setIsMovingSelections(false); + } }; const is_canvas_transforming = isPanning || isZooming; @@ -316,8 +392,6 @@ export function Canvas({ ?.map((id) => qdoc.getNodeById(id)) .filter(Boolean); - console.log({ selectedNodes, highlightedLayer, selected_nodes }); - const items = useMemo(() => { return nodes?.map((node) => { return ( @@ -372,6 +446,11 @@ export function Canvas({ setIsPanning(false); _canvas_state_store.saveLastTransform(cvtransform); }} + onZoomToFit={() => { + setZoom(1); + // setOffset([newx, newy]); // TODO: set offset to center of the viewport + _canvas_state_store.saveLastTransform(cvtransform); + }} onZooming={onZooming} onZoomingStart={() => { setIsZooming(true); @@ -394,6 +473,7 @@ export function Canvas({ hide={is_canvas_transforming} readonly={readonly} disableMarquee={config.marquee.disabled} + disableGrouping={config.grouping.disabled} marquee={marquee} labelDisplayNodes={nodes} selectedNodes={selected_nodes} @@ -413,13 +493,14 @@ export function Canvas({ setHoveringLayer({ node: node(id), reason: "frame-title" }); }} onSelectNode={(id) => { - onSelectNode?.(node(id)); + onSelectNode(node(id)); }} renderFrameTitle={props.renderFrameTitle} /> + {items} @@ -476,13 +557,29 @@ function DisableBackdropFilter({ children }: { children: React.ReactNode }) { ); } +function CanvasBackground({ backgroundColor }: { backgroundColor?: string }) { + return ( +
+ ); +} + function auto_initial_transform( viewbound: Box, nodes: ReflectSceneNode[] ): CanvasTransform { const _default = { - scale: INITIAL_SCALE, - xy: INITIAL_XY, + scale: CANVAS_INITIAL_SCALE, + xy: CANVAS_INITIAL_XY, }; if (!nodes || viewbound_not_measured(viewbound)) { diff --git a/editor-packages/editor-canvas/docs/core-drafting.md b/editor-packages/editor-canvas/docs/core-drafting.md new file mode 100644 index 00000000..65d6b4d3 --- /dev/null +++ b/editor-packages/editor-canvas/docs/core-drafting.md @@ -0,0 +1,20 @@ +# State (property drafting) + +There are properties that are highly likely to be updated every frame (e.g. position, rotation, scale, etc.). +This are performed by drag-related user input, and this properties will not be modified direcly to the design model. + +The final callback to the higher state holder will be called once after this operation is complete. + +## The properties are.. + +**transform** + +- x +- y +- width +- height +- rotation + +**style** + +- color diff --git a/editor-packages/editor-canvas/docs/feature-resize.md b/editor-packages/editor-canvas/docs/feature-resize.md new file mode 100644 index 00000000..a1ac9d26 --- /dev/null +++ b/editor-packages/editor-canvas/docs/feature-resize.md @@ -0,0 +1,14 @@ +# Resizing selection (s) + +Group resizing alg + +``` +| a ---- b ---- c | +``` + +In above scenario, width of a as x, is scaled in + +- origin width of selection as w1 +- new width of selection as w2 + +x = x \* w2 / w1 diff --git a/editor-packages/editor-canvas/drafting/README.md b/editor-packages/editor-canvas/drafting/README.md new file mode 100644 index 00000000..31b8de9a --- /dev/null +++ b/editor-packages/editor-canvas/drafting/README.md @@ -0,0 +1 @@ +# Properties drafting diff --git a/editor-packages/editor-canvas/drafting/_.ts b/editor-packages/editor-canvas/drafting/_.ts new file mode 100644 index 00000000..d32c40a7 --- /dev/null +++ b/editor-packages/editor-canvas/drafting/_.ts @@ -0,0 +1,16 @@ +export abstract class DraftingStore { + readonly store = new Map(); + lastUpdated: number; + + constructor() { + this.lastUpdated = Date.now(); + } + + abstract update(id: string, draft: T); + + abstract get(id: string): T; + + updated() { + this.lastUpdated = Date.now(); + } +} diff --git a/editor-packages/editor-canvas/drafting/index.ts b/editor-packages/editor-canvas/drafting/index.ts new file mode 100644 index 00000000..ab76e45e --- /dev/null +++ b/editor-packages/editor-canvas/drafting/index.ts @@ -0,0 +1 @@ +export * from "./transform-drafting"; diff --git a/editor-packages/editor-canvas/drafting/transform-drafting.ts b/editor-packages/editor-canvas/drafting/transform-drafting.ts new file mode 100644 index 00000000..3521a27d --- /dev/null +++ b/editor-packages/editor-canvas/drafting/transform-drafting.ts @@ -0,0 +1,57 @@ +import type { XY } from "../types"; +import { DraftingStore } from "./_"; +// move +// resize + +// width +// height +// x +// y +// rotation + +interface Transform { + x: number; + y: number; + width: number; + height: number; + rotation: number; +} + +export class TransformDraftingStore extends DraftingStore { + constructor(transforrms: (Transform & { id: string })[]) { + super(); + + transforrms.forEach((transform) => { + this.store.set(transform.id, TransformDraftingStore.flat(transform)); + }); + } + + static flat(t: Transform): Transform { + return { + x: t.x, + y: t.y, + width: t.width, + height: t.height, + rotation: t.rotation, + }; + } + + moveBy(delta: XY) { + Object.keys(this.store).forEach((id) => { + this.store[id].x += delta[0]; + this.store[id].y += delta[1]; + }); + this.updated(); + } + + update(id: string, transform: Transform) { + this.store.set(id, TransformDraftingStore.flat(transform)); + this.updated(); + } + + get(id: string, fallback?: Transform): Transform { + return this.store.get(id) ?? fallback; + } +} + +// export function diff --git a/editor-packages/editor-canvas/hud/hud-surface.tsx b/editor-packages/editor-canvas/hud/hud-surface.tsx index c1b11e14..1c8524fd 100644 --- a/editor-packages/editor-canvas/hud/hud-surface.tsx +++ b/editor-packages/editor-canvas/hud/hud-surface.tsx @@ -1,8 +1,15 @@ import React from "react"; -import { HoverOutlineHighlight, ReadonlySelectHightlight } from "../overlay"; +import { + HoverOutlineHighlight, + ReadonlySelectHightlight, + InSelectionGroupSelectHighlight, + SelectHightlight, + SizeMeterLabelBox, +} from "../overlay"; import { FrameTitle, FrameTitleProps } from "../frame-title"; import type { XY, XYWH } from "../types"; import { Marquee } from "../marquee"; +import { boundingbox, box_to_xywh } from "../math"; interface HudControls { onSelectNode: (node: string) => void; onHoverNode: (node: string | null) => void; @@ -33,6 +40,7 @@ export function HudSurface({ labelDisplayNodes, selectedNodes, readonly, + disableGrouping = false, onSelectNode, onHoverNode, marquee, @@ -48,6 +56,7 @@ export function HudSurface({ hide: boolean; marquee?: XYWH | null; disableMarquee?: boolean; + disableGrouping?: boolean; readonly: boolean; } & HudControls & HudCustomRenderers) { @@ -109,35 +118,126 @@ export function HudSurface({ /> ); })} - {selectedNodes && - selectedNodes.map((s) => { - const xywh: [number, number, number, number] = [ - s.absoluteX, - s.absoluteY, - s.width, - s.height, - ]; - if (readonly) { - return ( - - ); - } else { - // TODO: support non readonly canvas - } - })} + {selectedNodes?.length ? ( + disableGrouping ? ( + selectedNodes.map((s) => { + const xywh: [number, number, number, number] = [ + s.absoluteX, + s.absoluteY, + s.width, + s.height, + ]; + if (readonly) { + return ( + + ); + } else { + return ( + + ); + } + }) + ) : ( + + ) + ) : ( + <> + )} )}
); } +function SelectionGroupHighlight({ + selections, + zoom, + disableSizeDisplay = false, + readonly, +}: { + readonly: boolean; + selections: DisplayNodeMeta[]; + zoom: number; + disableSizeDisplay?: boolean; +}) { + const box = boundingbox( + selections.map((d) => { + return [d.absoluteX, d.absoluteY, d.width, d.height, d.rotation]; + }), + 2 + ); + + const xywh = box_to_xywh(box); + const [x, y, w, h] = xywh; + + return ( + <> + <> + {selections.map((s) => { + return ( + + ); + })} + + <> + {!disableSizeDisplay ? ( + + ) : ( + <> + )} + + {readonly ? ( + + ) : ( + + )} + + ); +} + const frame_title_default_renderer = (p: FrameTitleProps) => ( ); diff --git a/editor-packages/editor-canvas/k/index.ts b/editor-packages/editor-canvas/k/index.ts new file mode 100644 index 00000000..45a7c562 --- /dev/null +++ b/editor-packages/editor-canvas/k/index.ts @@ -0,0 +1,6 @@ +import type { XY } from "../types"; + +export const CANVAS_INITIAL_SCALE = 0.5; +export const CANVAS_INITIAL_XY: XY = [0, 0]; +export const CANVAS_LAYER_HOVER_HIT_MARGIN = 3.5; +export const CANVAS_MIN_ZOOM = 0.02; diff --git a/editor-packages/editor-canvas/math/bounding-box.ts b/editor-packages/editor-canvas/math/bounding-box.ts new file mode 100644 index 00000000..da3f427c --- /dev/null +++ b/editor-packages/editor-canvas/math/bounding-box.ts @@ -0,0 +1,109 @@ +import type { XYWH, Box, XYWHR, XY } from "../types"; + +export function xywh_to_bounding_box({ + xywh, + scale, +}: { + xywh: XYWH; + scale: number; +}): Box { + const [x, y, w, h] = xywh; + + // return the bounding box in [number, number, number, number] form with givven x, y, w, h, rotation and scale. + const [x1, y1, x2, y2] = [ + x * scale, + y * scale, + x * scale + w * scale, + y * scale + h * scale, + ]; + return [x1, y1, x2, y2]; +} + +/** + * @deprecated - not tested + * @param box + * @param zoom + * @returns + */ +export function zoom_box(box: Box, zoom: number): Box { + const [x1, y1, x2, y2] = box; + const [w, h] = [x2 - x1, y2 - y1]; + const [dw, dh] = [w * zoom, h * zoom]; + return [x1 * zoom, y1 * zoom, x1 + dw, y1 + dh]; +} + +type BoundingBoxInput = + | (Box & { type?: 0 }) + | (XYWH & { type?: 1 }) + | (XYWHR & { type?: 2 }); + +export function boundingbox( + rects: BoundingBoxInput[], + t?: BoundingBoxInput["type"] +): Box { + let x1 = Infinity; + let y1 = Infinity; + let x2 = -Infinity; + let y2 = -Infinity; + for (const rect of rects) { + const [_x1, _y1, _x2, _y2] = to_box(rect, t); + x1 = Math.min(x1, _x1); + y1 = Math.min(y1, _y1); + x2 = Math.max(x2, _x2); + y2 = Math.max(y2, _y2); + } + return [x1, y1, x2, y2]; +} + +export function is_point_inside_box(point: XY, box: Box) { + const [x, y] = point; + const [x1, y1, x2, y2] = box; + return x >= x1 && x <= x2 && y >= y1 && y <= y2; +} + +/** + // TODO: handle rotation. (no rotation for now) + * + * @param rect + * @returns + */ +const to_box = (rect: BoundingBoxInput, t?: BoundingBoxInput["type"]): Box => { + let _x1, + _y1, + _x2, + _y2, + _r = 0; + + switch (rect.type ?? t) { + case 0: { + [_x1, _y1, _x2, _y2] = rect; + } + case 1: { + const [x, y, w, h] = rect; + _x1 = x; + _y1 = y; + _x2 = x + w; + _y2 = y + h; + } + case 2: { + const [x, y, w, h, r] = rect; + _x1 = x; + _y1 = y; + _x2 = x + w; + _y2 = y + h; + _r = r; + } + } + + return [_x1, _y1, _x2, _y2]; +}; + +export function box_to_xywh(box: Box): XYWH { + const [x1, y1, x2, y2] = box; + return [x1, y1, x2 - x1, y2 - y1]; +} + +export function box_to_xywhr(box: Box): XYWHR { + const [x1, y1, x2, y2] = box; + return [x1, y1, x2 - x1, y2 - y1, 0]; +} diff --git a/editor-packages/editor-canvas/math/center-of.ts b/editor-packages/editor-canvas/math/center-of.ts index 8b23ee4c..9c9e46e3 100644 --- a/editor-packages/editor-canvas/math/center-of.ts +++ b/editor-packages/editor-canvas/math/center-of.ts @@ -1,4 +1,5 @@ -import type { Box, XY } from "../types"; +import type { Box, XY, XYWHR } from "../types"; +import { boundingbox } from "./bounding-box"; type Rect = { x: number; @@ -24,6 +25,9 @@ export function centerOf( translate: XY; scale: number; } { + const xywhrs = rects.map((r) => { + return [r.x, r.y, r.width, r.height, r.rotation] as XYWHR; + }); if (!rects || rects.length === 0) { return { box: viewbound, @@ -36,7 +40,7 @@ export function centerOf( }; } - const [x1, y1, x2, y2] = bound(...rects); + const [x1, y1, x2, y2] = boundingbox(xywhrs, 2); // box containing the rects. const box: Box = [x1, y1, x2, y2]; // center of the box, viewbound not considered. @@ -63,22 +67,6 @@ export function centerOf( }; } -function bound(...rects: Rect[]): Box { - let x1 = Infinity; - let y1 = Infinity; - let x2 = -Infinity; - let y2 = -Infinity; - for (const rect of rects) { - const { x, y, width: w, height: h } = rect; - // TODO: handle rotation. (no rotation for now) - x1 = Math.min(x1, x); - y1 = Math.min(y1, y); - x2 = Math.max(x2, x + w); - y2 = Math.max(y2, y + h); - } - return [x1, y1, x2, y2]; -} - function rotate(x: number, y: number, r: number): [number, number] { const cos = Math.cos(r); const sin = Math.sin(r); diff --git a/editor-packages/editor-canvas/math/index.ts b/editor-packages/editor-canvas/math/index.ts index 70d1cd75..5cf8cf7e 100644 --- a/editor-packages/editor-canvas/math/index.ts +++ b/editor-packages/editor-canvas/math/index.ts @@ -1,3 +1,4 @@ +export * from "./bounding-box"; export * from "./target-of-area"; export * from "./target-of-point"; export * from "./center-of"; diff --git a/editor-packages/editor-canvas/overlay/handle.tsx b/editor-packages/editor-canvas/overlay/handle.tsx new file mode 100644 index 00000000..b5c93c95 --- /dev/null +++ b/editor-packages/editor-canvas/overlay/handle.tsx @@ -0,0 +1,67 @@ +import React, { forwardRef } from "react"; +export const Handle = forwardRef(function ({ + color, + anchor, + box, + outlineWidth = 1, + outlineColor = "transparent", + size = 4, + borderRadius = 0, + cursor, + readonly, +}: { + color: string; + /** + * the width of the outline + */ + outlineWidth?: number; + outlineColor?: string; + size: number; + anchor: "nw" | "ne" | "sw" | "se"; + box: [number, number, number, number]; + borderRadius?: React.CSSProperties["borderRadius"]; + cursor?: React.CSSProperties["cursor"]; + readonly?: boolean; +}) { + let dx = 0; + let dy = 0; + switch (anchor) { + case "nw": + dx = box[0]; + dy = box[1]; + break; + case "ne": + dx = box[2]; + dy = box[1]; + break; + case "sw": + dx = box[0]; + dy = box[3]; + break; + case "se": + dx = box[2]; + dy = box[3]; + break; + } + + // translate x, y + const [tx, ty] = [dx - size / 2 - outlineWidth, dy - size / 2 - outlineWidth]; + + return ( +
+ ); +}); diff --git a/editor-packages/editor-canvas/overlay/hover-outline-hightlight.tsx b/editor-packages/editor-canvas/overlay/hover-outline-hightlight.tsx index b5b9b6db..91fae0c9 100644 --- a/editor-packages/editor-canvas/overlay/hover-outline-hightlight.tsx +++ b/editor-packages/editor-canvas/overlay/hover-outline-hightlight.tsx @@ -1,13 +1,13 @@ import React from "react"; import { color_layer_highlight } from "../theme"; -import { get_boinding_box } from "./math"; +import { xywh_to_bounding_box } from "../math"; import { OulineSide } from "./outline-side"; import { OverlayContainer } from "./overlay-container"; import type { OutlineProps } from "./types"; export function HoverOutlineHighlight({ width = 1, ...props }: OutlineProps) { const { xywh, zoom, rotation } = props; - const bbox = get_boinding_box({ xywh, scale: zoom }); + const bbox = xywh_to_bounding_box({ xywh, scale: zoom }); const wh: [number, number] = [xywh[2], xywh[3]]; const vprops = { wh: wh, @@ -19,10 +19,10 @@ export function HoverOutlineHighlight({ width = 1, ...props }: OutlineProps) { return ( - - - - + + + + ); } diff --git a/editor-packages/editor-canvas/overlay/index.ts b/editor-packages/editor-canvas/overlay/index.ts index 576ab0ac..80998c4e 100644 --- a/editor-packages/editor-canvas/overlay/index.ts +++ b/editor-packages/editor-canvas/overlay/index.ts @@ -1,2 +1,5 @@ export * from "./hover-outline-hightlight"; -export * from "./readonly-select-hightlight"; +export * from "./select-highlight-in-selection-group"; +export * from "./select-hightlight"; +export * from "./select-hightlight-readonly"; +export * from "./size-meter-label-box"; diff --git a/editor-packages/editor-canvas/overlay/math.ts b/editor-packages/editor-canvas/overlay/math.ts deleted file mode 100644 index 9c391b42..00000000 --- a/editor-packages/editor-canvas/overlay/math.ts +++ /dev/null @@ -1,18 +0,0 @@ -export function get_boinding_box({ - xywh, - scale, -}: { - xywh: [number, number, number, number]; - scale: number; -}): [number, number, number, number] { - const [x, y, w, h] = xywh; - - // return the bounding box in [number, number, number, number] form with givven x, y, w, h, rotation and scale. - const [x1, y1, x2, y2] = [ - x * scale, - y * scale, - x * scale + w * scale, - y * scale + h * scale, - ]; - return [x1, y1, x2, y2]; -} diff --git a/editor-packages/editor-canvas/overlay/outline-side.tsx b/editor-packages/editor-canvas/overlay/outline-side.tsx index e93476b3..2bfc4054 100644 --- a/editor-packages/editor-canvas/overlay/outline-side.tsx +++ b/editor-packages/editor-canvas/overlay/outline-side.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { color_layer_highlight } from "../theme"; export function OulineSide({ @@ -7,46 +8,50 @@ export function OulineSide({ width = 1, box, color = color_layer_highlight, + readonly = true, + cursor, }: { wh: [number, number]; box: [number, number, number, number]; zoom: number; - orientation: "l" | "t" | "r" | "b"; + orientation: "w" | "n" | "e" | "s"; width?: number; color?: string; + readonly?: boolean; + cursor?: React.CSSProperties["cursor"]; }) { const d = 100; const [w, h] = wh; // is vertical line - const isvert = orientation === "l" || orientation === "r"; + const isvert = orientation === "w" || orientation === "e"; const l_scalex = isvert ? width / d : (w / d) * zoom; const l_scaley = isvert ? (h / d) * zoom : width / d; let trans = { x: 0, y: 0 }; switch (orientation) { - case "l": { + case "w": { trans = { x: box[0] - d / 2, y: box[1] + (d * l_scaley - d) / 2, }; break; } - case "r": { + case "e": { trans = { x: box[2] - d / 2, y: box[1] + (d * l_scaley - d) / 2, }; break; } - case "t": { + case "n": { trans = { x: box[0] + (d * l_scalex - d) / 2, y: box[1] - d / 2, }; break; } - case "b": { + case "s": { trans = { x: box[0] + (d * l_scalex - d) / 2, y: box[3] - d / 2, @@ -62,10 +67,11 @@ export function OulineSide({ width: d, height: d, opacity: 1, - pointerEvents: "none", + pointerEvents: readonly ? "none" : "all", + cursor: cursor, willChange: "transform", transformOrigin: "0px, 0px", - transform: `translateX(${trans.x}px) translateY(${trans.y}px) translateZ(0px) scaleX(${l_scalex}) scaleY(${l_scaley})`, + transform: `translate3d(${trans.x}px, ${trans.y}px, 0) scaleX(${l_scalex}) scaleY(${l_scaley})`, backgroundColor: color, }} /> diff --git a/editor-packages/editor-canvas/overlay/select-highlight-in-selection-group.tsx b/editor-packages/editor-canvas/overlay/select-highlight-in-selection-group.tsx new file mode 100644 index 00000000..4facd9d2 --- /dev/null +++ b/editor-packages/editor-canvas/overlay/select-highlight-in-selection-group.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import type { OutlineProps } from "./types"; +import { color_layer_readonly_highlight } from "../theme"; +import { xywh_to_bounding_box } from "../math"; +import { OulineSide } from "./outline-side"; +import { OverlayContainer } from "./overlay-container"; + +export function InSelectionGroupSelectHighlight({ + width = 0.3, + ...props +}: OutlineProps) { + const { xywh, zoom, rotation } = props; + const bbox = xywh_to_bounding_box({ xywh, scale: zoom }); + const wh: [number, number] = [xywh[2], xywh[3]]; + + const sideprops = { + wh: wh, + zoom: props.zoom, + width: width, + box: bbox, + color: color_layer_readonly_highlight, + }; + + return ( + + <> + + + + + + + ); +} diff --git a/editor-packages/editor-canvas/overlay/readonly-select-hightlight.tsx b/editor-packages/editor-canvas/overlay/select-hightlight-readonly.tsx similarity index 73% rename from editor-packages/editor-canvas/overlay/readonly-select-hightlight.tsx rename to editor-packages/editor-canvas/overlay/select-hightlight-readonly.tsx index 27ab07db..656eaf1a 100644 --- a/editor-packages/editor-canvas/overlay/readonly-select-hightlight.tsx +++ b/editor-packages/editor-canvas/overlay/select-hightlight-readonly.tsx @@ -1,16 +1,17 @@ import React from "react"; import type { OutlineProps } from "./types"; import { color_layer_readonly_highlight } from "../theme"; -import { get_boinding_box } from "./math"; +import { xywh_to_bounding_box } from "../math"; import { OulineSide } from "./outline-side"; import { OverlayContainer } from "./overlay-container"; +import { Handle } from "./handle"; export function ReadonlySelectHightlight({ width = 1, ...props }: OutlineProps) { const { xywh, zoom, rotation } = props; - const bbox = get_boinding_box({ xywh, scale: zoom }); + const bbox = xywh_to_bounding_box({ xywh, scale: zoom }); const wh: [number, number] = [xywh[2], xywh[3]]; const handle_outline_width = width; @@ -64,34 +65,34 @@ export function ReadonlySelectHightlight({ <> <> - - - - + + + + ); @@ -103,7 +104,7 @@ function SideCenterEmp({ box, size = 3, }: { - side: "l" | "r" | "t" | "b"; + side: "w" | "e" | "n" | "s"; color: string; box: [number, number, number, number]; size?: number; @@ -111,19 +112,19 @@ function SideCenterEmp({ let dx = 0; let dy = 0; switch (side) { - case "l": + case "w": dx = box[0]; dy = box[1] + (box[3] - box[1]) / 2; break; - case "r": + case "e": dx = box[2]; dy = box[1] + (box[3] - box[1]) / 2; break; - case "t": + case "n": dx = box[0] + (box[2] - box[0]) / 2; dy = box[1]; break; - case "b": + case "s": dx = box[0] + (box[2] - box[0]) / 2; dy = box[3]; break; @@ -166,43 +167,16 @@ function ReadonlyHandle({ anchor: "nw" | "ne" | "sw" | "se"; box: [number, number, number, number]; }) { - let dx = 0; - let dy = 0; - switch (anchor) { - case "nw": - dx = box[0]; - dy = box[1]; - break; - case "ne": - dx = box[2]; - dy = box[1]; - break; - case "sw": - dx = box[0]; - dy = box[3]; - break; - case "se": - dx = box[2]; - dy = box[3]; - break; - } - - // translate x, y - const [tx, ty] = [dx - size / 2 - outlineWidth, dy - size / 2 - outlineWidth]; - return ( -
); } diff --git a/editor-packages/editor-canvas/overlay/select-hightlight.tsx b/editor-packages/editor-canvas/overlay/select-hightlight.tsx new file mode 100644 index 00000000..8bd4669b --- /dev/null +++ b/editor-packages/editor-canvas/overlay/select-hightlight.tsx @@ -0,0 +1,128 @@ +import React, { useRef } from "react"; +import type { OutlineProps } from "./types"; +import { color_layer_highlight } from "../theme"; +import { xywh_to_bounding_box } from "../math"; +import { OulineSide } from "./outline-side"; +import { OverlayContainer } from "./overlay-container"; +import { Handle } from "./handle"; +import { useGesture } from "@use-gesture/react"; +import type { OnDragHandler } from "../canvas-event-target"; + +export function SelectHightlight({ + ...props +}: Omit & {}) { + const { xywh, zoom, rotation } = props; + const bbox = xywh_to_bounding_box({ xywh, scale: zoom }); + const wh: [number, number] = [xywh[2], xywh[3]]; + + const sideprops = { + wh: wh, + zoom: props.zoom, + width: 1, + readonly: false, + box: bbox, + color: color_layer_highlight, + }; + + return ( + + {/* TODO: add rotation knob */} + {/* <> + + + + + */} + <> + + + + + + <> + + + + + + + ); +} + +const resize_cursor_map = { + nw: "nwse-resize", + ne: "nesw-resize", + sw: "nesw-resize", + se: "nwse-resize", + w: "ew-resize", + n: "ns-resize", + s: "ns-resize", + e: "ew-resize", +}; + +function ResizeHandle({ + anchor, + box, +}: { + anchor: "nw" | "ne" | "sw" | "se"; + box: [number, number, number, number]; +}) { + return ( + + ); +} + +function RotateHandle({ + anchor, + box, + onDrag, +}: { + anchor: "nw" | "ne" | "sw" | "se"; + box: [number, number, number, number]; + onDrag: OnDragHandler; +}) { + const ref = useRef(); + useGesture( + { + onDragStart: (e) => { + e.event.stopPropagation(); + }, + onDragEnd: (e) => { + e.event.stopPropagation(); + }, + onDrag: (e) => { + onDrag(e); + e.event.stopPropagation(); + }, + }, + { + target: ref, + eventOptions: { + capture: false, + }, + } + ); + + return ( + + ); +} diff --git a/editor-packages/editor-canvas/overlay/size-meter-label-box.tsx b/editor-packages/editor-canvas/overlay/size-meter-label-box.tsx new file mode 100644 index 00000000..8713ab30 --- /dev/null +++ b/editor-packages/editor-canvas/overlay/size-meter-label-box.tsx @@ -0,0 +1,63 @@ +import React, { useMemo } from "react"; +import styled from "@emotion/styled"; +import type { XYWH } from "../types"; +import { xywh_to_bounding_box } from "../math"; + +const font_size = 10; + +export function SizeMeterLabelBox({ + size, + anchor = "s", + margin = 0, + xywh, + zoom, +}: { + size: { width: number; height: number }; + anchor?: "w" | "n" | "s" | "e"; + margin?: number; +} & { + xywh: XYWH; + zoom: number; +}) { + // TODO: add anchor handling + + const bbox = useMemo( + () => xywh_to_bounding_box({ xywh, scale: zoom }), + [xywh, zoom] + ); + + const [x1, y1, x2, y2] = bbox; + const bottomY = y2; + const boxWidth = x2 - x1; // use this to center position the label + + const text = `${+size.width.toFixed(2)} x ${+size.height.toFixed()}`; + const labelwidth = (text.length * font_size) / 1.8; // a view width assumption (we will not use flex box for faster painting) + const viewwidth = labelwidth + 4; // 4 is for horizontal padding + + const [tx, ty] = [x1 + boxWidth / 2 - viewwidth / 2, y2 + margin]; + + return ( +
+ {text} +
+ ); +} diff --git a/editor-packages/editor-canvas/reducer/history-reducer.ts b/editor-packages/editor-canvas/reducer/history-reducer.ts new file mode 100644 index 00000000..e69de29b diff --git a/editor-packages/editor-canvas/reducer/index.ts b/editor-packages/editor-canvas/reducer/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/editor-packages/editor-canvas/reducer/interaction-reducer.ts b/editor-packages/editor-canvas/reducer/interaction-reducer.ts new file mode 100644 index 00000000..e69de29b diff --git a/editor-packages/editor-canvas/reducer/node-property-reducer.ts b/editor-packages/editor-canvas/reducer/node-property-reducer.ts new file mode 100644 index 00000000..e69de29b diff --git a/editor-packages/editor-canvas/reducer/node-reducer.ts b/editor-packages/editor-canvas/reducer/node-reducer.ts new file mode 100644 index 00000000..e69de29b diff --git a/editor-packages/editor-canvas/reducer/node-style-reducer.ts b/editor-packages/editor-canvas/reducer/node-style-reducer.ts new file mode 100644 index 00000000..e69de29b diff --git a/editor-packages/editor-canvas/types/index.ts b/editor-packages/editor-canvas/types/index.ts index 907c5e37..793db67e 100644 --- a/editor-packages/editor-canvas/types/index.ts +++ b/editor-packages/editor-canvas/types/index.ts @@ -1,10 +1,33 @@ +/** + * represents a point with x and y + */ export type XY = [number, number]; + +/** + * represents a rectangle with x, y, width, height + */ export type XYWH = [number, number, number, number]; + +/** + * represents a rectangle with x, y, width, height, rotation + */ +export type XYWHR = [number, number, number, number, number]; + +/** + * represents a rectangle with x1, y1, x2, y2 + */ +export type X1Y1X2Y2 = [number, number, number, number]; + export type CanvasTransform = { scale: number; xy: XY; }; -export type Box = [number, number, number, number]; + +/** + * a bounding box with x1, y1, x2, y2 + */ +export type Box = X1Y1X2Y2; + export interface Tree { id: string; /** @@ -19,3 +42,13 @@ export interface Tree { height: number; children?: Tree[] | undefined; } + +export const directions_cardinal = ["n", "e", "s", "w"] as const; +export type CardinalDirection = typeof directions_cardinal[number]; +export const directions_ordinal = ["ne", "se", "sw", "nw"] as const; +export type OrdinalDirection = typeof directions_ordinal[number]; +export const directions_compass: CompassDirection[] = [ + ...directions_cardinal, + ...directions_cardinal, +]; +export type CompassDirection = CardinalDirection | OrdinalDirection; diff --git a/editor-packages/editor-services-jsx-syntax-highlight/index.ts b/editor-packages/editor-services-jsx-syntax-highlight/index.ts index 6f8b0a0f..df30d42e 100644 --- a/editor-packages/editor-services-jsx-syntax-highlight/index.ts +++ b/editor-packages/editor-services-jsx-syntax-highlight/index.ts @@ -54,4 +54,12 @@ export function registerJsxHighlighter( oldDecor = editor.deltaDecorations(oldDecor, decorations); }); }); + + return { + dispose() { + if (syntaxWorker) { + syntaxWorker.terminate(); + } + }, + }; } diff --git a/editor-packages/editor-services-prettier/index.ts b/editor-packages/editor-services-prettier/index.ts index 847f96d5..d1e4acd0 100644 --- a/editor-packages/editor-services-prettier/index.ts +++ b/editor-packages/editor-services-prettier/index.ts @@ -10,16 +10,20 @@ export function registerDocumentPrettier(editor, monaco) { const dartFormattingEditProvider = { provideDocumentFormattingEdits: (model, options, token) => { - const raw = model.getValue(); - const { code, error } = formatDartCode(raw); - if (error) return []; - __dangerous__lastFormattedValue__global = code; - return [ - { - range: model.getFullModelRange(), - text: code, - }, - ]; + try { + const raw = model.getValue(); + const { code, error } = formatDartCode(raw); + if (error) return []; + __dangerous__lastFormattedValue__global = code; + return [ + { + range: model.getFullModelRange(), + text: code, + }, + ]; + } catch (_) { + // ignore. this is caused by disposed model + } }, }; @@ -31,19 +35,23 @@ export function registerDocumentPrettier(editor, monaco) { ); } - const { canceled, error, pretty } = await prettierWorker?.emit({ - text: model.getValue(), - language: model._languageId, - }); + try { + const { canceled, error, pretty } = await prettierWorker?.emit({ + text: model.getValue(), + language: model._languageId, + }); - if (canceled || error) return []; - __dangerous__lastFormattedValue__global = pretty; - return [ - { - range: model.getFullModelRange(), - text: pretty, - }, - ]; + if (canceled || error) return []; + __dangerous__lastFormattedValue__global = pretty; + return [ + { + range: model.getFullModelRange(), + text: pretty, + }, + ]; + } catch (_) { + // ignore. this is caused by disposed model + } }, }; diff --git a/editor/store/fimga-file-store/figma-file-store.ts b/editor-packages/editor-store-figma-file/figma-file-store.ts similarity index 97% rename from editor/store/fimga-file-store/figma-file-store.ts rename to editor-packages/editor-store-figma-file/figma-file-store.ts index f4dbbca2..ce603552 100644 --- a/editor/store/fimga-file-store/figma-file-store.ts +++ b/editor-packages/editor-store-figma-file/figma-file-store.ts @@ -1,5 +1,5 @@ import { openDB, IDBPDatabase } from "idb"; -import { FileResponse } from "@design-sdk/figma-remote-api"; +import type { FileResponse } from "@design-sdk/figma-remote-api"; // #region global db initialization const __db_pref = { name: "fimga-file-store", version: 1 }; diff --git a/editor/store/fimga-file-store/index.ts b/editor-packages/editor-store-figma-file/index.ts similarity index 100% rename from editor/store/fimga-file-store/index.ts rename to editor-packages/editor-store-figma-file/index.ts diff --git a/editor-packages/editor-store-figma-file/package.json b/editor-packages/editor-store-figma-file/package.json new file mode 100644 index 00000000..299c259c --- /dev/null +++ b/editor-packages/editor-store-figma-file/package.json @@ -0,0 +1,4 @@ +{ + "name": "@editor/figma-file-store", + "version": "0.0.0" +} \ No newline at end of file diff --git a/editor/components/code-editor/monaco-utils/register-preset-types.ts b/editor/components/code-editor/monaco-utils/register-preset-types.ts index e9c61041..3e29f689 100644 --- a/editor/components/code-editor/monaco-utils/register-preset-types.ts +++ b/editor/components/code-editor/monaco-utils/register-preset-types.ts @@ -17,5 +17,5 @@ const react_preset_dependencies = [ */ export function registerPresetTypes() { // load the react presets - loadTypes(react_preset_dependencies); + return loadTypes(react_preset_dependencies); } diff --git a/editor/components/code-editor/monaco-utils/register.ts b/editor/components/code-editor/monaco-utils/register.ts index 866eb037..07a082a0 100644 --- a/editor/components/code-editor/monaco-utils/register.ts +++ b/editor/components/code-editor/monaco-utils/register.ts @@ -1,15 +1,29 @@ import * as monaco from "monaco-editor"; -import { Monaco, OnMount } from "@monaco-editor/react"; +import { Monaco } from "@monaco-editor/react"; import { registerDocumentPrettier } from "@code-editor/prettier-services"; import { registerJsxHighlighter } from "@code-editor/jsx-syntax-highlight-services"; import { registerPresetTypes } from "./register-preset-types"; type CompilerOptions = monaco.languages.typescript.CompilerOptions; -export const initEditor: OnMount = (editor, monaco) => { - registerJsxHighlighter(editor, monaco); - registerDocumentPrettier(editor, monaco); - registerPresetTypes(); +export const initEditor = ( + editor: monaco.editor.IStandaloneCodeEditor, + monaco: Monaco +) => { + const { dispose: disposeJsxHighlighter } = registerJsxHighlighter( + editor, + monaco + ); + + const { dispose: disposePrettier } = registerDocumentPrettier(editor, monaco); + + const { dispose: dispostPresetTypesLoader } = registerPresetTypes(); + + return () => { + disposeJsxHighlighter(); + disposePrettier(); + dispostPresetTypesLoader(); + }; }; export const initMonaco = (monaco: Monaco) => { diff --git a/editor/components/code-editor/monaco.tsx b/editor/components/code-editor/monaco.tsx index 919011a0..ea199449 100644 --- a/editor/components/code-editor/monaco.tsx +++ b/editor/components/code-editor/monaco.tsx @@ -29,7 +29,7 @@ export function MonacoEditor(props: MonacoEditorProps) { instance.current = { editor, format }; - register.initEditor(editor, monaco); + const dispose = register.initEditor(editor, monaco); editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, function () { format.run(); @@ -65,6 +65,10 @@ export function MonacoEditor(props: MonacoEditorProps) { editor.onDidChangeModelContent(() => debounce(() => editor.saveViewState(), 200) ); + + editor.onDidDispose(() => { + dispose(); + }); }; return ( diff --git a/editor/core/actions/index.ts b/editor/core/actions/index.ts index 50846655..091014a1 100644 --- a/editor/core/actions/index.ts +++ b/editor/core/actions/index.ts @@ -23,6 +23,7 @@ export type Action = | PageAction | SelectNodeAction | HighlightLayerAction + | CanvasEditAction | CanvasModeAction | PreviewAction | CodeEditorAction @@ -37,6 +38,14 @@ export interface SelectNodeAction { node: string | string[]; } +export type CanvasEditAction = TranslateNodeAction; + +export interface TranslateNodeAction { + type: "node-transform-translate"; + translate: [number, number]; + node: string[]; +} + export type PageAction = SelectPageAction; export interface SelectPageAction { diff --git a/editor/core/reducers/editor-reducer.ts b/editor/core/reducers/editor-reducer.ts index d1304543..78bb3752 100644 --- a/editor/core/reducers/editor-reducer.ts +++ b/editor/core/reducers/editor-reducer.ts @@ -6,6 +6,7 @@ import type { CodeEditorEditComponentCodeAction, CanvasModeSwitchAction, CanvasModeGobackAction, + TranslateNodeAction, PreviewBuildingStateUpdateAction, PreviewSetAction, DevtoolsConsoleAction, @@ -17,6 +18,7 @@ import type { import { EditorState } from "core/states"; import { useRouter } from "next/router"; import { CanvasStateStore } from "@code-editor/canvas/stores"; +import q from "@design-sdk/query"; import assert from "assert"; const _editor_path_name = "/files/[key]/"; @@ -28,10 +30,27 @@ export function editorReducer(state: EditorState, action: Action): EditorState { switch (action.type) { case "select-node": { const { node } = action; + const ids = Array.isArray(node) ? node : [node]; + + const current_node = state.selectedNodes; + + if ( + ids.length <= 1 && + current_node.length <= 1 && + ids[0] === current_node[0] + ) { + // same selection (no selection or same 1 selection) + return produce(state, (draft) => {}); + } + + if (ids.length > 1 && ids.length === current_node.length) { + // the selection event is always triggered by user, which means selecting same amount of nodes (greater thatn 1, and having a different node array is impossible.) + return produce(state, (draft) => {}); + } + console.clear(); console.info("cleard console by editorReducer#select-node"); - const ids = Array.isArray(node) ? node : [node]; const primary = ids?.[0]; // update router @@ -89,6 +108,22 @@ export function editorReducer(state: EditorState, action: Action): EditorState { draft.selectedNodes = last_known_selections_of_this_page; }); } + case "node-transform-translate": { + const { translate, node } = action; + + return produce(state, (draft) => { + const page = draft.design.pages.find( + (p) => p.id === state.selectedPage + ); + + node + .map((n) => q.getNodeByIdFrom(n, page.children)) + .map((n) => { + n.x += translate[0]; + n.y += translate[1]; + }); + }); + } case "code-editor-edit-component-code": { const { ...rest } = action; return produce(state, (draft) => { diff --git a/editor/core/states/editor-state.ts b/editor/core/states/editor-state.ts index bb991b30..ebd32524 100644 --- a/editor/core/states/editor-state.ts +++ b/editor/core/states/editor-state.ts @@ -1,6 +1,6 @@ import type { ReflectSceneNode } from "@design-sdk/figma-node"; import type { FrameworkConfig } from "@grida/builder-config"; -import type { WidgetKey } from "@reflect-ui/core"; +import type { RGBA, WidgetKey } from "@reflect-ui/core"; import type { ComponentNode } from "@design-sdk/figma-types"; import type { DesignInput } from "@grida/builder-config/input"; @@ -53,7 +53,13 @@ export interface FigmaReflectRepository { key: string; // TODO: - pages: { id: string; name: string; children: ReflectSceneNode[] }[]; + pages: { + id: string; + name: string; + children: ReflectSceneNode[]; + backgroundColor: RGBA; + flowStartingPoints: any[]; + }[]; components: { [key: string]: ComponentNode }; // styles: { [key: string]: {} }; input: DesignInput; diff --git a/editor/core/states/workspace-state.ts b/editor/core/states/workspace-state.ts index a5114f77..7670bf27 100644 --- a/editor/core/states/workspace-state.ts +++ b/editor/core/states/workspace-state.ts @@ -8,6 +8,16 @@ export interface WorkspaceState { */ highlightedLayer?: string; preferences: WorkspacePreferences; + + /** + * figma authentication data store state + * @deprecated - not implemented + */ + authenticationFigma?: { + name?: string; + accessToken?: string; + personalAccessToken?: string; + }; } export interface WorkspacePreferences { diff --git a/editor/pages/canvas-server/index.tsx b/editor/pages/canvas-server/index.tsx index a7bd1f8b..4197a46a 100644 --- a/editor/pages/canvas-server/index.tsx +++ b/editor/pages/canvas-server/index.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react"; import { D2CVanillaPreview, WebWorkerD2CVanillaPreview, -} from "scaffolds/preview"; +} from "scaffolds/preview-canvas"; import { Canvas } from "@code-editor/canvas"; import useMeasure from "react-use-measure"; import { FrameTitleRenderer } from "scaffolds/canvas/render/frame-title"; @@ -17,17 +17,6 @@ export default function CanvasServerPage() { const [canvasSizingRef, canvasBounds] = useMeasure(); const [selectedPage, setSelectedPage] = useState(null); - // useEffect(() => { - // const handler = (e) => { - // // - // }; - - // window.addEventListener("message", handler); - // return () => { - // window.removeEventListener("message", handler); - // }; - // }, []); - // const thisPageNodes = selectedPage // ? design.pages.find((p) => p.id == selectedPage).children.filter(Boolean) // : []; @@ -69,7 +58,10 @@ export default function CanvasServerPage() { config={{ can_highlight_selected_layer: true, marquee: { - disabled: true, + disabled: false, + }, + grouping: { + disabled: false, }, }} renderFrameTitle={(p) => ( diff --git a/editor/pages/files/index.tsx b/editor/pages/files/index.tsx index 8ec7c948..4ccae5ee 100644 --- a/editor/pages/files/index.tsx +++ b/editor/pages/files/index.tsx @@ -8,7 +8,7 @@ import { HomeSidebar, } from "components/home"; import { WorkspaceRepository } from "repository"; -import { FileResponseRecord } from "store/fimga-file-store/figma-file-store"; +import { FileResponseRecord } from "@editor/figma-file-store"; import { colors } from "theme"; export default function FilesPage() { diff --git a/editor/repository/figma-design-repository/index.ts b/editor/repository/figma-design-repository/index.ts index 27695a6c..ead1055a 100644 --- a/editor/repository/figma-design-repository/index.ts +++ b/editor/repository/figma-design-repository/index.ts @@ -1,9 +1,6 @@ import { fetch } from "@design-sdk/figma-remote"; import { FileResponse } from "@design-sdk/figma-remote-types"; -import { - FigmaFileStore, - FileResponseRecord, -} from "store/fimga-file-store/figma-file-store"; +import { FigmaFileStore, FileResponseRecord } from "@editor/figma-file-store"; export type TFetchFileForApp = ( | fetch.FetchFileGeneratorReturnType diff --git a/editor/repository/workspace-repository/index.ts b/editor/repository/workspace-repository/index.ts index c03388d5..98bb2503 100644 --- a/editor/repository/workspace-repository/index.ts +++ b/editor/repository/workspace-repository/index.ts @@ -1,7 +1,4 @@ -import { - FigmaFilesStore, - FileResponseRecord, -} from "store/fimga-file-store/figma-file-store"; +import { FigmaFilesStore, FileResponseRecord } from "@editor/figma-file-store"; export type LastUsedFileDisplay = FileResponseRecord & { type: "file" } & { lastUsed: Date; diff --git a/editor/scaffolds/canvas/canvas.tsx b/editor/scaffolds/canvas/canvas.tsx index c499029c..dbe0cf29 100644 --- a/editor/scaffolds/canvas/canvas.tsx +++ b/editor/scaffolds/canvas/canvas.tsx @@ -3,9 +3,9 @@ import styled from "@emotion/styled"; import { Canvas } from "@code-editor/canvas"; import { useEditorState, useWorkspace } from "core/states"; import { - D2CVanillaPreview, WebWorkerD2CVanillaPreview, -} from "scaffolds/preview"; + D2CVanillaPreview, +} from "scaffolds/preview-canvas"; import useMeasure from "react-use-measure"; import { useDispatch } from "core/dispatch"; import { FrameTitleRenderer } from "./render/frame-title"; @@ -31,9 +31,8 @@ export function VisualContentArea() { canvasMode_previous, } = state; - const thisPageNodes = selectedPage - ? design.pages.find((p) => p.id == selectedPage).children.filter(Boolean) - : []; + const thisPage = design?.pages?.find((p) => p.id == selectedPage); + const thisPageNodes = selectedPage ? thisPage.children.filter(Boolean) : []; const isEmptyPage = thisPageNodes?.length === 0; @@ -73,6 +72,12 @@ export function VisualContentArea() { [dispatch] ); + const _bg = + thisPage?.backgroundColor && + `rgba(${thisPage.backgroundColor.r * 255}, ${ + thisPage.backgroundColor.g * 255 + }, ${thisPage.backgroundColor.b * 255}, ${thisPage.backgroundColor.a})`; + return ( {/* */} @@ -113,16 +118,22 @@ export function VisualContentArea() { ]} filekey={state.design.key} pageid={selectedPage} + backgroundColor={_bg} selectedNodes={selectedNodes} highlightedLayer={highlightedLayer} onSelectNode={(...nodes) => { + dispatch({ type: "select-node", node: nodes.map((n) => n.id) }); + }} + onMoveNodeEnd={([x, y], ...nodes) => { dispatch({ - type: "select-node", - node: nodes.map((n) => n?.id), + type: "node-transform-translate", + node: nodes, + translate: [x, y], }); }} + // onMoveNode={() => {}} onClearSelection={() => { - dispatch({ type: "select-node", node: null }); + dispatch({ type: "select-node", node: [] }); }} nodes={thisPageNodes} // initialTransform={ } // TODO: if the initial selection is provided from first load, from the query param, we have to focus to fit that node. @@ -136,11 +147,16 @@ export function VisualContentArea() { ); }} + // readonly={false} + readonly config={{ can_highlight_selected_layer: true, marquee: { disabled: false, }, + grouping: { + disabled: false, + }, }} renderFrameTitle={(p) => ( - - {props.children} - + + + {props.children} + + ); diff --git a/editor/scaffolds/editor/warmup.ts b/editor/scaffolds/editor/warmup.ts index faf589d9..e7a18631 100644 --- a/editor/scaffolds/editor/warmup.ts +++ b/editor/scaffolds/editor/warmup.ts @@ -8,7 +8,7 @@ import { createInitialWorkspaceState } from "core/states"; import { workspaceReducer } from "core/reducers"; import { PendingState } from "core/utility-types"; import { WorkspaceAction } from "core/actions"; -import { FileResponse } from "@design-sdk/figma-remote-types"; +import type { Canvas, FileResponse } from "@design-sdk/figma-remote-types"; import { convert } from "@design-sdk/figma-node-conversion"; import { mapper } from "@design-sdk/figma-remote"; import { visit } from "tree-visit"; @@ -45,13 +45,15 @@ export function pagesFrom( filekey: string, file: FileResponse ): FigmaReflectRepository["pages"] { - return file.document.children.map((page) => ({ + return (file.document.children as Array).map((page) => ({ id: page.id, name: page.name, children: page["children"]?.map((child) => { const _mapped = mapper.mapFigmaRemoteToFigma(child); return convert.intoReflectNode(_mapped, null, "rest", filekey); }), + flowStartingPoints: page.flowStartingPoints, + backgroundColor: page.backgroundColor, type: "design", })); } diff --git a/editor/scaffolds/preview/README.md b/editor/scaffolds/preview-canvas/README.md similarity index 100% rename from editor/scaffolds/preview/README.md rename to editor/scaffolds/preview-canvas/README.md diff --git a/editor/scaffolds/preview-canvas/cache.ts b/editor/scaffolds/preview-canvas/cache.ts new file mode 100644 index 00000000..b0b6170a --- /dev/null +++ b/editor/scaffolds/preview-canvas/cache.ts @@ -0,0 +1,16 @@ +import type { Result } from "@designto/code"; + +type TResultCache = Result & { __image: boolean }; + +export const cache = { + set: (key: string, value: TResultCache) => { + sessionStorage.setItem(key, JSON.stringify(value)); + }, + get: (key: string): TResultCache => { + const value = sessionStorage.getItem(key); + return value ? JSON.parse(value) : null; + }, +}; + +export const cachekey = (target: { filekey; id }) => + target ? `${target.filekey}-${target.id}-${new Date().getMinutes()}` : null; diff --git a/editor/scaffolds/preview-canvas/canvas-preview-worker-messenger.ts b/editor/scaffolds/preview-canvas/canvas-preview-worker-messenger.ts new file mode 100644 index 00000000..dfc98577 --- /dev/null +++ b/editor/scaffolds/preview-canvas/canvas-preview-worker-messenger.ts @@ -0,0 +1,67 @@ +import { createWorkerQueue } from "@code-editor/webworker-services-core"; +import type { Result } from "@designto/code"; + +let previewworker: Worker; +export function initialize( + { filekey, authentication }: { filekey: string; authentication }, + onReady: () => void +) { + // initialize the worker and set the preferences. + if (!previewworker) { + const { worker } = createWorkerQueue( + new Worker(new URL("./workers/canvas-preview.worker.js", import.meta.url)) + ); + + previewworker = worker; + } + + previewworker.postMessage({ + $type: "initialize", + filekey, + authentication, + }); + + previewworker.addEventListener("message", (e) => { + if (e.data.$type === "data-readt") { + onReady(); + } + }); + + return () => { + if (previewworker) { + previewworker.terminate(); + } + }; +} + +export function preview( + { target, page }: { target: string; page: string }, + onResult: (result: Result) => void, + onError?: (error: Error) => void +) { + previewworker.postMessage({ + $type: "preview", + page, + target, + }); + + const handler = (e) => { + const id = e.data.id; + if (target === id) { + switch (e.data.$type) { + case "result": + onResult(e.data); + break; + case "error": + onError(new Error(e.data.message)); + break; + } + } + }; + + previewworker.addEventListener("message", handler); + + return () => { + previewworker.removeEventListener("message", handler); + }; +} diff --git a/editor/scaffolds/preview-canvas/editor-canvas-preview-provider.tsx b/editor/scaffolds/preview-canvas/editor-canvas-preview-provider.tsx new file mode 100644 index 00000000..7c49031e --- /dev/null +++ b/editor/scaffolds/preview-canvas/editor-canvas-preview-provider.tsx @@ -0,0 +1,30 @@ +import React, { useEffect } from "react"; +import { useFigmaAccessToken } from "hooks/use-figma-access-token"; +import { useEditorState } from "core/states"; +import { initialize } from "./canvas-preview-worker-messenger"; + +export function EditorCanvasPreviewProvider({ + children, +}: { + children?: React.ReactNode; +}) { + const [state] = useEditorState(); + const fat = useFigmaAccessToken(); + + useEffect(() => { + if ( + state.design?.key && + (fat.personalAccessToken || !fat.accessToken.loading) + ) { + const authentication = { + personalAccessToken: fat.personalAccessToken, + accessToken: fat.accessToken.token, + }; + initialize({ filekey: state.design.key, authentication }, () => { + // + }); + } + }, [fat.personalAccessToken, fat.accessToken.token, state.design?.key]); + + return <>{children}; +} diff --git a/editor/scaffolds/preview-canvas/image-preview.tsx b/editor/scaffolds/preview-canvas/image-preview.tsx new file mode 100644 index 00000000..bf7a14c2 --- /dev/null +++ b/editor/scaffolds/preview-canvas/image-preview.tsx @@ -0,0 +1,68 @@ +import React, { useState, useEffect } from "react"; +import { fetchNodeAsImage } from "@design-sdk/figma-remote"; + +const DEV_ONLY_FIGMA_PAT = + process.env.NEXT_PUBLIC_DEVELOPER_FIGMA_PERSONAL_ACCESS_TOKEN; + +export function FigmaFrameImageView({ + filekey, + nodeid, + zoom, +}: { + filekey: string; + nodeid: string; + zoom: number; +}) { + // fetch image + const [image_1, setImage_1] = useState(); + const [image_s, setImage_s] = useState(); + + useEffect(() => { + // fetch image from figma + // fetch smaller one first, then fatch the full scaled. + fetchNodeAsImage( + filekey, + { personalAccessToken: DEV_ONLY_FIGMA_PAT }, + nodeid + // scale = 1 + ).then((r) => { + console.log("fetched image from figma", r); + setImage_1(r.__default); + setImage_s(r.__default); + }); + }, [filekey, nodeid]); + + let imgscale: 1 | 0.2 = 1; + if (zoom > 1) { + return null; + } else if (zoom <= 1 && zoom > 0.3) { + imgscale = 1; + // display 1 scaled image + } else { + // display 0.2 scaled image + imgscale = 0.2; + } + + return ( +
+ +
+ ); +} diff --git a/editor/scaffolds/preview-canvas/index.ts b/editor/scaffolds/preview-canvas/index.ts new file mode 100644 index 00000000..042ca293 --- /dev/null +++ b/editor/scaffolds/preview-canvas/index.ts @@ -0,0 +1,2 @@ +export { D2CVanillaPreview } from "./vanilla-preview-async"; +export { WebWorkerD2CVanillaPreview } from "./vanilla-preview-webworker"; diff --git a/editor/scaffolds/preview-canvas/preview-content.tsx b/editor/scaffolds/preview-canvas/preview-content.tsx new file mode 100644 index 00000000..fb7a8b0e --- /dev/null +++ b/editor/scaffolds/preview-canvas/preview-content.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { VanillaRunner } from "components/app-runner/vanilla-app-runner"; + +export function PreviewContent({ + width, + height, + backgroundColor, + id, + source, + name, +}: { + width: number; + height: number; + backgroundColor: string; + id: string; + source: string; + name: string; +}) { + return ( +
+ {source && ( + + )} +
+ ); +} diff --git a/editor/scaffolds/preview-canvas/prop-type.ts b/editor/scaffolds/preview-canvas/prop-type.ts new file mode 100644 index 00000000..c02c8b82 --- /dev/null +++ b/editor/scaffolds/preview-canvas/prop-type.ts @@ -0,0 +1,6 @@ +import type { ReflectSceneNode } from "@design-sdk/figma-node"; +import type { FrameOptimizationFactors } from "@code-editor/canvas/frame"; + +export type VanillaPreviewProps = { + target: ReflectSceneNode; +} & FrameOptimizationFactors; diff --git a/editor/scaffolds/preview-canvas/util.ts b/editor/scaffolds/preview-canvas/util.ts new file mode 100644 index 00000000..7df37353 --- /dev/null +++ b/editor/scaffolds/preview-canvas/util.ts @@ -0,0 +1,8 @@ +import { colorFromFills } from "@design-sdk/core/utils"; +import type { ReflectSceneNode } from "@design-sdk/figma-node"; + +export const blurred_bg_fill = (target: ReflectSceneNode) => { + const __bg = colorFromFills(target.fills); + const bg_color_str = __bg ? "#" + __bg.hex : "transparent"; + return bg_color_str; +}; diff --git a/editor/scaffolds/preview-canvas/vanilla-preview-async.tsx b/editor/scaffolds/preview-canvas/vanilla-preview-async.tsx new file mode 100644 index 00000000..4d58c01f --- /dev/null +++ b/editor/scaffolds/preview-canvas/vanilla-preview-async.tsx @@ -0,0 +1,151 @@ +import React, { useEffect, useState } from "react"; +import { config } from "@grida/builder-config"; +import { preview_presets } from "@grida/builder-config-preset"; +import { designToCode, Result } from "@designto/code"; +import { MainImageRepository } from "@design-sdk/asset-repository"; +import { cachekey, cache } from "./cache"; +import { blurred_bg_fill } from "./util"; +import { PreviewContent } from "./preview-content"; +import type { VanillaPreviewProps } from "./prop-type"; + +const placeholderimg = + "https://bridged-service-static.s3.us-west-1.amazonaws.com/placeholder-images/image-placeholder-bw-tile-100.png"; + +const build_config: config.BuildConfiguration = { + ...config.default_build_configuration, + disable_components: true, + disable_detection: true, + disable_flags_support: true, +}; + +const framework_config: config.VanillaPreviewFrameworkConfig = { + ...preview_presets.default, + additional_css_declaration: { + declarations: [ + { + key: { + name: "body", + selector: "tag", + }, + style: { + contain: "layout style paint", + }, + }, + ], + }, +}; + +export function D2CVanillaPreview({ + target, + isZooming, + isPanning, +}: VanillaPreviewProps) { + const [preview, setPreview] = useState(); + const key = cachekey(target); + + const on_preview_result = (result: Result, __image: boolean) => { + if (preview) { + if (preview.code === result.code) { + return; + } + } + setPreview(result); + cache.set(target.filekey as string, { ...result, __image }); + }; + + const hide_preview = isZooming || isPanning; + + useEffect(() => { + if (hide_preview) { + // don't make preview if zooming. + return; + } + + if (preview) { + return; + } + + const d2c_firstload = () => { + return designToCode({ + input: _input, + build_config: build_config, + framework: framework_config, + asset_config: { + skip_asset_replacement: false, + asset_repository: MainImageRepository.instance, + custom_asset_replacement: { + type: "static", + resource: placeholderimg, + }, + }, + }); + }; + + const d2c_imageload = () => { + if (!MainImageRepository.instance.empty) { + designToCode({ + input: _input, + build_config: build_config, + framework: framework_config, + asset_config: { asset_repository: MainImageRepository.instance }, + }) + .then((r) => { + on_preview_result(r, true); + }) + .catch((e) => { + console.error( + "error while making preview with image repo provided.", + e + ); + }); + } + }; + + const _input = target + ? { + id: target.id, + name: target.name, + entry: target, + } + : null; + + const cached = cache.get(key); + if (cached) { + setPreview(cached); + if (cached.__image) { + return; + } + if (_input) { + d2c_imageload(); + } + } else { + if (_input) { + d2c_firstload() + .then((r) => { + on_preview_result(r, false); + // if the result contains a image and needs to be fetched, + if (r.code.raw.includes(placeholderimg)) { + // TODO: we don't yet have other way to know if image is used, other than checking if placeholder image is used. - this needs to be updated in d2c module to include used images meta in the result. + d2c_imageload(); + } + }) + .catch(console.error); + } + } + }, [target?.id, isZooming, isPanning]); + + const bg_color_str = blurred_bg_fill(target); + + return ( + + ); +} diff --git a/editor/scaffolds/preview-canvas/vanilla-preview-webworker.tsx b/editor/scaffolds/preview-canvas/vanilla-preview-webworker.tsx new file mode 100644 index 00000000..362546bd --- /dev/null +++ b/editor/scaffolds/preview-canvas/vanilla-preview-webworker.tsx @@ -0,0 +1,48 @@ +import React, { useEffect, useState } from "react"; +import type { Result } from "@designto/code"; +import { PreviewContent } from "./preview-content"; +import type { VanillaPreviewProps } from "./prop-type"; +import { blurred_bg_fill } from "./util"; +import { cachekey, cache } from "./cache"; +import { preview as wwpreview } from "./canvas-preview-worker-messenger"; + +export function WebWorkerD2CVanillaPreview({ target }: VanillaPreviewProps) { + const [preview, setPreview] = useState(); + const bg_color_str = blurred_bg_fill(target); + + useEffect(() => { + if (preview) { + return; + } + + let dispose; + + setTimeout(() => { + dispose = wwpreview( + { + page: "", // TODO: + // page: target.page, + target: target.id, + }, + setPreview + ); + }, 50); + + return () => { + dispose?.(); + }; + }, [target?.id]); + + return ( + + ); +} diff --git a/editor/scaffolds/preview-canvas/workers/canvas-preview.worker.js b/editor/scaffolds/preview-canvas/workers/canvas-preview.worker.js new file mode 100644 index 00000000..46157bc1 --- /dev/null +++ b/editor/scaffolds/preview-canvas/workers/canvas-preview.worker.js @@ -0,0 +1,161 @@ +import { designToCode } from "@designto/code"; +import { + ImageRepository, + MainImageRepository, +} from "@design-sdk/asset-repository"; +import { RemoteImageRepositories } from "@design-sdk/figma-remote/asset-repository"; +import { config } from "@grida/builder-config"; +import { preview_presets } from "@grida/builder-config-preset"; +import { FigmaFileStore } from "@editor/figma-file-store"; +import { convert } from "@design-sdk/figma-node-conversion"; +import { mapper } from "@design-sdk/figma-remote"; + +const placeholderimg = + "https://bridged-service-static.s3.us-west-1.amazonaws.com/placeholder-images/image-placeholder-bw-tile-100.png"; + +// : config.BuildConfiguration +const build_config = { + ...config.default_build_configuration, + disable_components: true, + disable_detection: true, + disable_flags_support: true, +}; + +// : config.VanillaPreviewFrameworkConfig +const framework_config = { + ...preview_presets.default, + additional_css_declaration: { + declarations: [ + { + key: { + name: "body", + selector: "tag", + }, + style: { + contain: "layout style paint", + }, + }, + ], + }, +}; + +let initialized = false; +let pages = []; + +function initialize({ filekey, authentication }) { + console.info("initializing.. wwpreview"); + // ------- setup image repo with auth + filekey ------- + MainImageRepository.instance = new RemoteImageRepositories(filekey, { + authentication: authentication, + }); + + MainImageRepository.instance.register( + new ImageRepository( + "fill-later-assets", + "grida://assets-reservation/images/" + ) + ); + // ---------------------------------------------------- + + // setup indexed db connection for reading the file. + // ⛔️ the assumbtion is that file does not change during the app is running. + + const store = new FigmaFileStore(filekey); + // 1. read the raw data from indexed db + store.get().then((raw) => { + // 2. format the data to reflect + pages = pagesFrom(raw); + + // 3. set the data status as 'ready' + initialized = true; + postMessage({ $type: "data-ready" }); + }); + // (the below requests can be operated after when this processes are complete) +} + +function pagesFrom(file) { + return file.document.children.map((page) => ({ + id: page.id, + name: page.name, + children: page["children"]?.map((child) => { + const _mapped = mapper.mapFigmaRemoteToFigma(child); + return convert.intoReflectNode(_mapped); + }), + flowStartingPoints: page.flowStartingPoints, + backgroundColor: page.backgroundColor, + type: "design", + })); +} + +addEventListener("message", async (event) => { + function respond(data) { + setTimeout(() => { + postMessage({ $type: "result", ...data }); + }, 0); + } + + if (event.data.$type === "initialize") { + initialize({ + filekey: event.data.filekey, + authentication: event.data.authentication, + }); + return; + } + + if (!initialized) { + return; + } + + switch (event.data.$type) { + case "initialize": { + // unreachable + break; + } + case "preview": { + try { + // requires all 2 data for faster query + const { page, target } = event.data; + const node = pages + .find((p) => p.id === page) + .children.find((c) => c.id === target); + + const _input = { + id: node.id, + name: node.name, + entry: node, + }; + + const result = await designToCode({ + input: _input, + build_config: build_config, + framework: framework_config, + asset_config: { + skip_asset_replacement: false, + asset_repository: MainImageRepository.instance, + custom_asset_replacement: { + type: "static", + resource: placeholderimg, + }, + }, + }); + + respond(result); + + const result_w_img = await designToCode({ + input: _input, + build_config: build_config, + framework: framework_config, + asset_config: { + asset_repository: MainImageRepository.instance, + }, + }); + + respond(result_w_img); + } catch (error) { + // respond({ error }); + } + + break; + } + } +}); diff --git a/editor/scaffolds/preview/index.tsx b/editor/scaffolds/preview/index.tsx deleted file mode 100644 index a1170d78..00000000 --- a/editor/scaffolds/preview/index.tsx +++ /dev/null @@ -1,347 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { preview_presets } from "@grida/builder-config-preset"; -import { designToCode, Result } from "@designto/code"; -import { config } from "@grida/builder-config"; -import { MainImageRepository } from "@design-sdk/asset-repository"; -import type { ReflectSceneNode } from "@design-sdk/figma-node"; -import { VanillaRunner } from "components/app-runner/vanilla-app-runner"; -import { colorFromFills } from "@design-sdk/core/utils"; -import type { FrameOptimizationFactors } from "@code-editor/canvas/frame"; -import { fetchNodeAsImage } from "@design-sdk/figma-remote"; - -const DEV_ONLY_FIGMA_PAT = - process.env.NEXT_PUBLIC_DEVELOPER_FIGMA_PERSONAL_ACCESS_TOKEN; - -const placeholderimg = - "https://bridged-service-static.s3.us-west-1.amazonaws.com/placeholder-images/image-placeholder-bw-tile-100.png"; - -const build_config: config.BuildConfiguration = { - ...config.default_build_configuration, - disable_components: true, - disable_detection: true, - disable_flags_support: true, -}; - -const framework_config: config.VanillaPreviewFrameworkConfig = { - ...preview_presets.default, - additional_css_declaration: { - declarations: [ - { - key: { - name: "body", - selector: "tag", - }, - style: { - contain: "layout style paint", - }, - }, - ], - }, -}; - -type TResultCache = Result & { __image: boolean }; -const cache = { - set: (key: string, value: TResultCache) => { - sessionStorage.setItem(key, JSON.stringify(value)); - }, - get: (key: string): TResultCache => { - const value = sessionStorage.getItem(key); - return value ? JSON.parse(value) : null; - }, -}; - -const cachekey = (target: { filekey; id }) => - target ? `${target.filekey}-${target.id}-${new Date().getMinutes()}` : null; - -const blurred_bg_fill = (target: ReflectSceneNode) => { - const __bg = colorFromFills(target.fills); - const bg_color_str = __bg ? "#" + __bg.hex : "transparent"; - return bg_color_str; -}; - -type VanillaPreviewProps = { - target: ReflectSceneNode; -} & FrameOptimizationFactors; - -export function D2CVanillaPreview({ - target, - isZooming, - isPanning, -}: VanillaPreviewProps) { - const [preview, setPreview] = useState(); - const key = cachekey(target); - - const on_preview_result = (result: Result, __image: boolean) => { - if (preview) { - if (preview.code === result.code) { - return; - } - } - setPreview(result); - - if (typeof target.filekey == "string") { - delete result["widget"]; - cache.set(target.filekey, { ...result, __image }); - } - }; - - const hide_preview = isZooming || isPanning; - - useEffect(() => { - if (hide_preview) { - // don't make preview if zooming. - return; - } - - if (preview) { - return; - } - - const d2c_firstload = () => { - return designToCode({ - input: _input, - build_config: build_config, - framework: framework_config, - asset_config: { - skip_asset_replacement: false, - asset_repository: MainImageRepository.instance, - custom_asset_replacement: { - type: "static", - resource: placeholderimg, - }, - }, - }); - }; - - const d2c_imageload = () => { - if (!MainImageRepository.instance.empty) { - designToCode({ - input: _input, - build_config: build_config, - framework: framework_config, - asset_config: { asset_repository: MainImageRepository.instance }, - }) - .then((r) => { - on_preview_result(r, true); - }) - .catch((e) => { - console.error( - "error while making preview with image repo provided.", - e - ); - }); - } - }; - - const _input = target - ? { - id: target.id, - name: target.name, - entry: target, - } - : null; - - const cached = cache.get(key); - if (cached) { - setPreview(cached); - if (cached.__image) { - return; - } - if (_input) { - d2c_imageload(); - } - } else { - if (_input) { - d2c_firstload() - .then((r) => { - on_preview_result(r, false); - // if the result contains a image and needs to be fetched, - if (r.code.raw.includes(placeholderimg)) { - // TODO: we don't yet have other way to know if image is used, other than checking if placeholder image is used. - this needs to be updated in d2c module to include used images meta in the result. - d2c_imageload(); - } - }) - .catch(console.error); - } - } - }, [target?.id, isZooming, isPanning]); - - const bg_color_str = blurred_bg_fill(target); - - return ( - - ); -} - -import { createWorkerQueue } from "@code-editor/webworker-services-core"; -import { useFigmaAccessToken } from "hooks/use-figma-access-token"; - -export function WebWorkerD2CVanillaPreview({ target }: VanillaPreviewProps) { - const [preview, setPreview] = useState(); - const bg_color_str = blurred_bg_fill(target); - - const fat = useFigmaAccessToken(); - - useEffect(() => { - if (preview) { - return; - } - - const { worker, terminate } = createWorkerQueue( - new Worker(new URL("./workers/vanilla.worker.js", import.meta.url)) - ); - - const input = target - ? { - id: target.id, - name: target.name, - entry: target, - } - : null; - - // TODO: this is not production ready - worker.postMessage({ - input, - authentication: fat, - filekey: target.filekey, - }); - - worker.addEventListener("message", (e) => { - // console.log(target.id, e.data.id); - // if (((e.data as Result).id = target.id)) { - setPreview(e.data as Result); - // } - }); - - () => { - terminate(); - }; - }, [target?.id]); - - return ( - - ); -} - -function PreviewContent({ - width, - height, - backgroundColor, - id, - source, - name, -}: { - width: number; - height: number; - backgroundColor: string; - id: string; - source: string; - name: string; -}) { - return ( -
- {source && ( - - )} -
- ); -} - -function FigmaFrameImageView({ - filekey, - nodeid, - zoom, -}: { - filekey: string; - nodeid: string; - zoom: number; -}) { - // fetch image - const [image_1, setImage_1] = useState(); - const [image_s, setImage_s] = useState(); - - useEffect(() => { - // fetch image from figma - // fetch smaller one first, then fatch the full scaled. - fetchNodeAsImage( - filekey, - { personalAccessToken: DEV_ONLY_FIGMA_PAT }, - nodeid - // scale = 1 - ).then((r) => { - console.log("fetched image from figma", r); - setImage_1(r.__default); - setImage_s(r.__default); - }); - }, [filekey, nodeid]); - - let imgscale: 1 | 0.2 = 1; - if (zoom > 1) { - return null; - } else if (zoom <= 1 && zoom > 0.3) { - imgscale = 1; - // display 1 scaled image - } else { - // display 0.2 scaled image - imgscale = 0.2; - } - - return ( -
- -
- ); -} diff --git a/editor/scaffolds/preview/workers/vanilla.worker.js b/editor/scaffolds/preview/workers/vanilla.worker.js deleted file mode 100644 index 9890da45..00000000 --- a/editor/scaffolds/preview/workers/vanilla.worker.js +++ /dev/null @@ -1,78 +0,0 @@ -import { designToCode } from "@designto/code"; -import { - ImageRepository, - MainImageRepository, -} from "@design-sdk/asset-repository"; -import { RemoteImageRepositories } from "@design-sdk/figma-remote/asset-repository"; -import { config } from "@grida/builder-config"; -import { preview_presets } from "@grida/builder-config-preset"; - -const placeholderimg = - "https://bridged-service-static.s3.us-west-1.amazonaws.com/placeholder-images/image-placeholder-bw-tile-100.png"; - -// : config.BuildConfiguration -const build_config = { - ...config.default_build_configuration, - disable_components: true, - disable_detection: true, - disable_flags_support: true, -}; - -// : config.VanillaPreviewFrameworkConfig -const framework_config = { - ...preview_presets.default, - additional_css_declaration: { - declarations: [ - { - key: { - name: "body", - selector: "tag", - }, - style: { - contain: "layout style paint", - }, - }, - ], - }, -}; - -addEventListener("message", async (event) => { - function respond(data) { - setTimeout(() => { - postMessage({ type: "response", ...data }); - }, 0); - } - - try { - const { input, authentication, filekey } = event.data; - - MainImageRepository.instance = new RemoteImageRepositories(filekey, { - authentication: authentication, - }); - - MainImageRepository.instance.register( - new ImageRepository( - "fill-later-assets", - "grida://assets-reservation/images/" - ) - ); - - const result = await designToCode({ - input: input, - build_config: build_config, - framework: framework_config, - asset_config: { - skip_asset_replacement: false, - asset_repository: MainImageRepository.instance, - custom_asset_replacement: { - type: "static", - resource: placeholderimg, - }, - }, - }); - - respond(result); - } catch (error) { - respond({ error }); - } -}); diff --git a/editor/store/index.ts b/editor/store/index.ts index c5235dc3..9b865148 100644 --- a/editor/store/index.ts +++ b/editor/store/index.ts @@ -1 +1,2 @@ +export * from "@editor/figma-file-store"; export * from "./remote-design-session-cache-store"; diff --git a/editor/styles/global.css b/editor/styles/global.css index 4a78ecbb..e1991479 100644 --- a/editor/styles/global.css +++ b/editor/styles/global.css @@ -2,11 +2,17 @@ body { margin: 0px; padding: 0; font-family: "Helvetica Nueue", "Roboto", sans-serif; +} - /* for editor canvas */ +/* for editor canvas */ +html { + overscroll-behavior-x: none; +} +body { overscroll-behavior-x: none; - overscroll-behavior-y: none; + touch-action: none; } +/* ----------------- */ iframe { border: none;