diff --git a/editor/scaffolds/inspector/inspect-layout/index.ts b/editor/scaffolds/inspector/inspect-layout/index.ts new file mode 100644 index 00000000..b5ff1425 --- /dev/null +++ b/editor/scaffolds/inspector/inspect-layout/index.ts @@ -0,0 +1 @@ +export * from "./inspect-layout"; diff --git a/editor/scaffolds/inspector/inspect-layout/inspect-layout.tsx b/editor/scaffolds/inspector/inspect-layout/inspect-layout.tsx new file mode 100644 index 00000000..91261690 --- /dev/null +++ b/editor/scaffolds/inspector/inspect-layout/inspect-layout.tsx @@ -0,0 +1,429 @@ +import React, { forwardRef, useCallback, useEffect, useMemo } from "react"; +import styled from "@emotion/styled"; +import { useDrag, useGesture } from "@use-gesture/react"; +import { useTargetContainer } from "hooks/use-target-node"; +import { visit } from "tree-visit"; +import type { ReflectSceneNode } from "@design-sdk/figma-node"; +import { useDispatch } from "core/dispatch"; +import { ReloadIcon } from "@radix-ui/react-icons"; +import { motion } from "framer-motion"; +import useMeasure from "react-use-measure"; + +type FlatScebeBode = { + id: string; + name: string; + x: number; + y: number; + width: number; + height: number; + // absolute relative to the root + absoluteX: number; + // absolute relative to the root + absoluteY: number; + depth: number; +}; + +const flatten = (node: ReflectSceneNode) => { + const [rx, ry] = [node.absoluteX, node.absoluteY]; + const result: FlatScebeBode[] = []; + + visit(node, { + getChildren: (node) => { + return node.children; + }, + onEnter(node, indexPath) { + const d = { + id: node.id, + name: node.name, + x: node.x, + y: node.y, + width: node.width, + height: node.height, + absoluteX: node.absoluteTransform[0][2] - rx, + absoluteY: node.absoluteTransform[1][2] - ry, + depth: indexPath.length, + }; + result.push(d); + }, + }); + + return result; +}; + +export function InspectLayout() { + // + const dispatch = useDispatch(); + const { target } = useTargetContainer(); + const [selected, setSelected] = React.useState(null); + const [scale, setScale] = React.useState(0); + + const [ref, bound] = useMeasure(); + + const highlight = useCallback( + (id: string) => { + dispatch({ + type: "highlight-node", + id: id, + }); + }, + [dispatch] + ); + + const flattened = useMemo( + () => (target ? flatten(target) : []), + [target?.id] + ); + + useEffect(() => { + if (target?.width && bound.width > 0) { + const margin = 100; + const s = bound.width / (target.width + margin); + setScale(s); + } + }, [bound.width, target?.width]); + + if (!(target?.children?.length > 0 && flattened.length > 0)) return <>; + + return ( + + + + {flattened.map((node: FlatScebeBode) => { + return ( + { + highlight(node.id); + setSelected(node.id); + }} + /> + ); + })} + + + + ); +} + +const Container = styled.div` + perspective: 1000; + transform-style: preserve-3d; +`; + +function Layer({ + x, + y, + width, + height, + depth, + onClick, + selected, +}: { + x: number; + y: number; + width: number; + height: number; + depth: number; + onClick?: () => void; + selected?: boolean; +}) { + return ( +
+ ); +} + +const minmax = (min: number, max: number, value: number) => { + return Math.min(Math.max(min, value), max); +}; + +const initialtransform = { + offsetX: 0, + offsetY: 100, + rotateX: 0, + rotateY: 0, + scale: 1, +}; + +type Projection = "perspective" | "orthographic"; +const Canvas = forwardRef(function ( + { + children, + projection, + contentMeta, + scale = 0.5, + }: React.PropsWithChildren<{ + projection: Projection; + scale?: number; + contentMeta: { + width: number; + height: number; + }; + }>, + fref: React.Ref +) { + const gestureref = React.useRef(null); + + const [transform, setTransform] = React.useState<{ + offsetX: number; + offsetY: number; + rotateX: number; + rotateY: number; + scale: number; + }>({ ...initialtransform, scale }); + + const [helpinfo, setHelpInfo] = React.useState(""); + + useEffect(() => { + setTransform({ ...initialtransform, scale }); + }, [scale]); + + const canReset = React.useMemo(() => { + return ( + transform.offsetX !== initialtransform.offsetX || + transform.offsetY !== initialtransform.offsetY || + transform.rotateX !== initialtransform.rotateX || + transform.rotateY !== initialtransform.rotateY + ); + }, [JSON.stringify(transform)]); + + const mode3d = React.useMemo(() => { + return ( + transform.rotateX !== initialtransform.rotateX || + transform.rotateY !== initialtransform.rotateY + ); + }, [JSON.stringify(transform)]); + + const reset = () => { + setTransform({ + ...initialtransform, + scale: scale, + }); + }; + + const displayhelp = (info: string) => { + if (!helpinfo) { + setHelpInfo(info); + setTimeout(() => { + setHelpInfo(""); + }, 1000); + } + }; + + const appliedscale = transform.scale; + + useGesture( + { + onDrag: ({ delta, metaKey, event }) => { + if (metaKey) { + const [dx, dy] = delta; + const _new = { + offsetX: minmax(-2000, 2000, transform.offsetX + dx / appliedscale), + offsetY: minmax(-2000, 100, transform.offsetY + dy / appliedscale), + rotateX: transform.rotateX, + rotateY: transform.rotateY, + scale: transform.scale, + }; + setTransform(_new); + } else { + const [dx, dy] = delta; + const { rotateX, rotateY } = transform; + const _new = { + offsetX: transform.offsetX, + offsetY: transform.offsetY, + rotateX: minmax(-95, 95, rotateX - dy * 0.5), + rotateY: minmax(-95, 95, rotateY + dx * 0.5), + scale: transform.scale, + }; + setTransform(_new); + + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + } + // + }, + onPinch: ({ delta: [d], _delta: [_d], event }) => { + const _new = { + offsetX: transform.offsetX, + offsetY: transform.offsetY, + rotateX: transform.rotateX, + rotateY: transform.rotateY, + scale: minmax(0.1, 10, transform.scale + d), + }; + setTransform(_new); + + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + }, + onWheel: ({ delta: [dx, dy], event, metaKey }) => { + if (metaKey) { + const _new = { + offsetX: minmax(-2000, 2000, transform.offsetX - dx / appliedscale), + offsetY: minmax(-2000, 100, transform.offsetY - dy / appliedscale), + rotateX: transform.rotateX, + rotateY: transform.rotateY, + scale: transform.scale, + }; + setTransform(_new); + + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + } else { + displayhelp("Hold ⌘ to pan. Drag to tilt."); + } + }, + }, + { + target: gestureref, + eventOptions: { + passive: false, + }, + } + ); + + return ( +
+
+ + {helpinfo} + +
+ {canReset && ( + + + + )} +
+ + {children} + +
+
+ ); +}); + +const HelpDisplay = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + pointer-events: none; + position: absolute; + opacity: 0; + background: rgba(0, 0, 0, 0.5); + color: white; + + top: 0; + left: 0; + right: 0; + bottom: 0; + + z-index: 99; + + transition: opacity 0.2s ease-in-out; +`; + +const TransformContainer = styled.div` + position: relative; + will-change: transform; + transform-style: preserve-3d; + transform-origin: top center; + perspective: none; + transition: transform 0.12s ease; +`; + +const IconButton = styled(motion.button)` + display: flex; + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; + border: none; + padding: 8px; + cursor: pointer; + outline: none; + + &:hover { + background: rgba(255, 255, 255, 0.3); + } +`; diff --git a/editor/scaffolds/inspector/inspect-layout/readme.md b/editor/scaffolds/inspector/inspect-layout/readme.md new file mode 100644 index 00000000..f3f50257 --- /dev/null +++ b/editor/scaffolds/inspector/inspect-layout/readme.md @@ -0,0 +1,3 @@ +# Layout Inspect + +## 3D View diff --git a/editor/scaffolds/inspector/section-layout.tsx b/editor/scaffolds/inspector/section-layout.tsx index 4dc538ec..560da6e8 100644 --- a/editor/scaffolds/inspector/section-layout.tsx +++ b/editor/scaffolds/inspector/section-layout.tsx @@ -9,6 +9,7 @@ import { } from "@editor-ui/property"; import { useTargetContainer } from "hooks/use-target-node"; import { IRadius } from "@reflect-ui/core"; +import { InspectLayout } from "./inspect-layout"; export function LayoutSection() { const { target, root } = useTargetContainer(); @@ -69,6 +70,7 @@ export function LayoutSection() { )} + ); }