feat: add @idraw/figma
BIN
packages/figma/dev/figma/iOS-Native-Wireframes-Community.fig
Normal file
36
packages/figma/dev/index.html
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>canvas</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
height: 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
.full-screen {
|
||||
/* position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0; */
|
||||
/* background: #000000; */
|
||||
/* background: #eaeaea; */
|
||||
}
|
||||
canvas {
|
||||
/* background-image: linear-gradient(#aaaaaa30 1px, transparent 0), linear-gradient(90deg, #aaaaaa30 1px, transparent 0),
|
||||
linear-gradient(#aaaaaa30 1px, transparent 0), linear-gradient(90deg, #aaaaaa30 1px, transparent 0);
|
||||
background-size: 10px 10px, 10px 10px, 50px 50px, 50px 50px;
|
||||
border-right: 1px solid #aaaaaa30;
|
||||
border-bottom: 1px solid #aaaaaa30; */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="canvas-preview"></div>
|
||||
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
90
packages/figma/dev/main.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import type { Data, ElementAssets, Element } from '@idraw/types';
|
||||
import { deepClone, getElemenetsAssetIds } from '@idraw/util';
|
||||
import { figmaBytesToMap, figmaMapToIDrawData, figmaBytesToIDrawData } from '../src';
|
||||
import { iDraw } from '../../idraw';
|
||||
// import data from './data';
|
||||
|
||||
const url = new URLSearchParams(window.location.search);
|
||||
|
||||
async function action(params: { data: Data }) {
|
||||
const previewDOM = document.querySelector('#canvas-preview') as HTMLDivElement;
|
||||
|
||||
const { data } = params;
|
||||
const devicePixelRatio = window.devicePixelRatio;
|
||||
const width = window.innerWidth;
|
||||
const height = 600;
|
||||
|
||||
const data1 = deepClone(data);
|
||||
|
||||
const idraw = new iDraw(previewDOM, {
|
||||
devicePixelRatio,
|
||||
width,
|
||||
height
|
||||
});
|
||||
idraw.setData(data1);
|
||||
idraw.centerContent();
|
||||
}
|
||||
|
||||
// async function main() {
|
||||
// if (targetFile) {
|
||||
// const filePath = `/demo/lab-figma-to-elements/figma/${targetFile}`;
|
||||
// const figma = await fetch(filePath).then((res) => res.blob());
|
||||
// const buffer = await figma.arrayBuffer();
|
||||
|
||||
// {
|
||||
// const filePath = `/demo/lab-figma-to-elements/figma/${targetFile}`;
|
||||
// const figma = await fetch(filePath).then((res) => res.blob());
|
||||
// const buff = await figma.arrayBuffer();
|
||||
// const bytes = new Uint8Array(buff);
|
||||
// const figmaMap = await figmaBytesToMap(bytes);
|
||||
// console.log('figmaMap ===== ', figmaMap);
|
||||
// const data = await figmaMapToIDrawData(figmaMap);
|
||||
// console.log('object ===== ', data);
|
||||
// }
|
||||
|
||||
// let data = await figmaBufferToIDrawData(buffer);
|
||||
// // console.log('object ====== ', object);
|
||||
|
||||
// // const map = figmaObjectToMap(object);
|
||||
// // console.log('map ==== ', map);
|
||||
|
||||
// // const tree = figmaObjectToTree(object);
|
||||
// // console.log('tree ==== ', tree);
|
||||
|
||||
// // let data = figmaObjectToIDrawData(object);
|
||||
// // TODO
|
||||
// data = {
|
||||
// elements: (data.elements[0] as Element<'group'>).detail.children
|
||||
// };
|
||||
// // console.log('data ===== ', data);
|
||||
// await action({ data });
|
||||
// } else {
|
||||
// list();
|
||||
// }
|
||||
// }
|
||||
|
||||
async function main() {
|
||||
const filePath = `/dev/figma/iOS-Native-Wireframes-Community.fig`;
|
||||
console.log('filePath ------ ', filePath);
|
||||
const figma = await fetch(filePath).then((res) => res.blob());
|
||||
const arrayBuffer = await figma.arrayBuffer();
|
||||
const buffer = new Uint8Array(arrayBuffer);
|
||||
|
||||
let data: Data = await figmaBytesToIDrawData(buffer);
|
||||
// // TODO
|
||||
data = {
|
||||
elements: (data.elements[0] as Element<'group'>).detail.children,
|
||||
global: data.elements[0].global
|
||||
};
|
||||
|
||||
console.log('data ===== ', data);
|
||||
await action({ data });
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => {
|
||||
console.log('Ok');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
17
packages/figma/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@idraw/figma",
|
||||
"version": "0.4.0-beta.25",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.1.3",
|
||||
"@idraw/types": "workspace:^0.4.0-beta.25",
|
||||
"@idraw/util": "workspace:^0.4.0-beta.25",
|
||||
"kiwi-schema": "^0.5.0",
|
||||
"matrix-inverse": "^2.0.0",
|
||||
"pako": "^2.1.0",
|
||||
"uzip": "^0.20201231.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pako": "^2.0.3",
|
||||
"@idraw/types": "workspace:^0.4.0-beta.25"
|
||||
}
|
||||
}
|
||||
88
packages/figma/src/common/calc.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import type { Element, ElementSize } from '@idraw/types';
|
||||
import { rotateElementVertexes } from '@idraw/util';
|
||||
|
||||
export function calcGroupSize(group: Element<'group'>): ElementSize {
|
||||
const area: ElementSize = { x: 0, y: 0, w: 0, h: 0 };
|
||||
group.detail.children.forEach((elem) => {
|
||||
const elemSize: ElementSize = {
|
||||
x: elem.x,
|
||||
y: elem.y,
|
||||
w: elem.w,
|
||||
h: elem.h,
|
||||
angle: elem.angle
|
||||
};
|
||||
if (elemSize.angle && (elemSize.angle > 0 || elemSize.angle < 0)) {
|
||||
const ves = rotateElementVertexes(elemSize);
|
||||
if (ves.length === 4) {
|
||||
const xList = [ves[0].x, ves[1].x, ves[2].x, ves[3].x];
|
||||
const yList = [ves[0].y, ves[1].y, ves[2].y, ves[3].y];
|
||||
elemSize.x = Math.min(...xList);
|
||||
elemSize.y = Math.min(...yList);
|
||||
elemSize.w = Math.abs(Math.max(...xList) - Math.min(...xList));
|
||||
elemSize.h = Math.abs(Math.max(...yList) - Math.min(...yList));
|
||||
}
|
||||
}
|
||||
const areaStartX = Math.min(elemSize.x, area.x);
|
||||
const areaStartY = Math.min(elemSize.y, area.y);
|
||||
|
||||
const areaEndX = Math.max(elemSize.x + elemSize.w, area.x + area.w);
|
||||
const areaEndY = Math.max(elemSize.y + elemSize.h, area.y + area.h);
|
||||
|
||||
area.x = areaStartX;
|
||||
area.y = areaStartY;
|
||||
area.w = Math.abs(areaEndX - areaStartX);
|
||||
area.h = Math.abs(areaEndY - areaStartY);
|
||||
});
|
||||
return area;
|
||||
}
|
||||
|
||||
export function resetGroupSize(group: Element<'group'>): Element<'group'> {
|
||||
const area = { x: 0, y: 0, w: 0, h: 0 };
|
||||
if (group.detail.children.length > 0) {
|
||||
const firstElem = group.detail.children[0];
|
||||
area.x = firstElem.x;
|
||||
area.y = firstElem.y;
|
||||
area.w = firstElem.w;
|
||||
area.h = firstElem.h;
|
||||
}
|
||||
group.detail.children.forEach((elem) => {
|
||||
const elemSize: ElementSize = {
|
||||
x: elem.x,
|
||||
y: elem.y,
|
||||
w: elem.w,
|
||||
h: elem.h,
|
||||
angle: elem.angle
|
||||
};
|
||||
if (elemSize.angle && (elemSize.angle > 0 || elemSize.angle < 0)) {
|
||||
const ves = rotateElementVertexes(elemSize);
|
||||
if (ves.length === 4) {
|
||||
const xList = [ves[0].x, ves[1].x, ves[2].x, ves[3].x];
|
||||
const yList = [ves[0].y, ves[1].y, ves[2].y, ves[3].y];
|
||||
elemSize.x = Math.min(...xList);
|
||||
elemSize.y = Math.min(...yList);
|
||||
elemSize.w = Math.abs(Math.max(...xList) - Math.min(...xList));
|
||||
elemSize.h = Math.abs(Math.max(...yList) - Math.min(...yList));
|
||||
}
|
||||
}
|
||||
const areaStartX = Math.min(elemSize.x, area.x);
|
||||
const areaStartY = Math.min(elemSize.y, area.y);
|
||||
|
||||
const areaEndX = Math.max(elemSize.x + elemSize.w, area.x + area.w);
|
||||
const areaEndY = Math.max(elemSize.y + elemSize.h, area.y + area.h);
|
||||
|
||||
area.x = areaStartX;
|
||||
area.y = areaStartY;
|
||||
area.w = Math.abs(areaEndX - areaStartX);
|
||||
area.h = Math.abs(areaEndY - areaStartY);
|
||||
});
|
||||
|
||||
group.detail.children.forEach((elem) => {
|
||||
elem.x -= area.x;
|
||||
elem.y -= area.y;
|
||||
});
|
||||
|
||||
group.w = area.w;
|
||||
group.h = area.h;
|
||||
|
||||
return group;
|
||||
}
|
||||
77
packages/figma/src/common/node.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import type { FigmaGUID, FigmaNode, FigmaNodeType, FigmaParseOptions, FigmaSymbolOverrideItem } from '../types';
|
||||
|
||||
export function figmaGUIDToID(guid: FigmaGUID): string {
|
||||
return `${guid.sessionID}:${guid.localID}`;
|
||||
}
|
||||
|
||||
export function getOverrideNodeMap(node: FigmaNode<'INSTANCE'>): Record<string, Partial<FigmaNode>> {
|
||||
const overrideNodeMap: Record<string, Partial<FigmaNode>> = {};
|
||||
const { symbolData, derivedSymbolData } = node;
|
||||
const { symbolOverrides } = symbolData;
|
||||
if (Array.isArray(symbolOverrides) && symbolOverrides.length > 0) {
|
||||
symbolOverrides.forEach((item) => {
|
||||
const { guidPath, ...restData } = item;
|
||||
guidPath.guids.forEach((guid) => {
|
||||
const id = figmaGUIDToID(guid);
|
||||
if (overrideNodeMap[id]) {
|
||||
overrideNodeMap[id] = { ...overrideNodeMap[id], ...restData };
|
||||
} else {
|
||||
overrideNodeMap[id] = restData;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(derivedSymbolData) && derivedSymbolData.length > 0) {
|
||||
derivedSymbolData.forEach((item) => {
|
||||
const { guidPath, ...restData } = item;
|
||||
guidPath.guids.forEach((guid) => {
|
||||
const id = figmaGUIDToID(guid);
|
||||
if (overrideNodeMap[id]) {
|
||||
overrideNodeMap[id] = { ...overrideNodeMap[id], ...restData };
|
||||
} else {
|
||||
overrideNodeMap[id] = restData;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return overrideNodeMap;
|
||||
}
|
||||
|
||||
export function mergeNodeOverrideData<T extends FigmaNodeType = 'CANVAS'>(
|
||||
node: FigmaNode<T>,
|
||||
opts: FigmaParseOptions<T>
|
||||
): Partial<FigmaNode<T>> | FigmaSymbolOverrideItem {
|
||||
let overrideData: Partial<FigmaNode<T>> = {};
|
||||
|
||||
const { overrideNodeMap = {}, overrideProperties } = opts;
|
||||
const { overrideKey } = node;
|
||||
|
||||
if (overrideKey) {
|
||||
const overrideId = figmaGUIDToID(overrideKey);
|
||||
if (overrideNodeMap[overrideId]) {
|
||||
overrideData = { ...overrideData, ...overrideNodeMap[overrideId] } as Partial<FigmaNode<T>>;
|
||||
}
|
||||
}
|
||||
if (overrideProperties) {
|
||||
overrideData = { ...overrideData, ...overrideProperties };
|
||||
}
|
||||
|
||||
return overrideData;
|
||||
}
|
||||
|
||||
export async function uint8ArrayToBase64(u8: Uint8Array, opts: { type: string }): Promise<string> {
|
||||
const { type } = opts;
|
||||
return new Promise((resolve, reject) => {
|
||||
var blob = new Blob([u8], { type });
|
||||
var fileReader = new FileReader();
|
||||
fileReader.addEventListener('load', () => {
|
||||
resolve(fileReader.result as string);
|
||||
});
|
||||
fileReader.addEventListener('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
fileReader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
1
packages/figma/src/config.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const figmaImageDir = 'images/';
|
||||
257
packages/figma/src/figma-node/base.ts
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
import type { Element, ElementBaseDetail, SVGPathCommand, SVGPathCommandType } from '@idraw/types';
|
||||
import { createUUID, rotateVertexes, parseAngleToRadian, calcElementCenterFromVertexes } from '@idraw/util';
|
||||
import type { FigmaNode, FigmaNodeType, FigmaNodeFillBase, FigmaNodeStrokeBase, FigmaEffect, FigmaFillPaintImage } from '../types';
|
||||
import { figmaPaintsToHexColor, figmaPaintsToColor, figmaColorToHex } from './color';
|
||||
|
||||
export function nodeToElementBase(node: FigmaNode<FigmaNodeType>): Omit<Element, 'type' | 'detail'> {
|
||||
const {
|
||||
m00,
|
||||
m01,
|
||||
m02,
|
||||
// m10,
|
||||
// m11,
|
||||
m12
|
||||
} = node.transform;
|
||||
const originAngle = Math.round((Math.atan2(m01, m00) * 180) / Math.PI);
|
||||
const angle = 0 - originAngle;
|
||||
const elemBase = {
|
||||
uuid: createUUID(),
|
||||
name: node.name,
|
||||
x: m02 || 0,
|
||||
y: m12 || 0,
|
||||
w: node?.size?.x || 0,
|
||||
h: node?.size?.y || 0,
|
||||
angle
|
||||
};
|
||||
|
||||
if (originAngle !== 0) {
|
||||
let { x, y, w, h } = elemBase;
|
||||
const rotateCenter = { x, y };
|
||||
const v0 = { x, y };
|
||||
const v1 = { x: x + w, y };
|
||||
const v2 = { x: x + w, y: y + h };
|
||||
const v3 = { x, y: y + h };
|
||||
const radian = parseAngleToRadian(angle);
|
||||
const ves = rotateVertexes(rotateCenter, [v0, v1, v2, v3], radian);
|
||||
const center = calcElementCenterFromVertexes(ves);
|
||||
elemBase.x = center.x - w / 2;
|
||||
elemBase.y = center.y - h / 2;
|
||||
}
|
||||
return elemBase;
|
||||
}
|
||||
|
||||
export function nodeToBaseDetail(node: FigmaNode<FigmaNodeType>): ElementBaseDetail {
|
||||
let detail: ElementBaseDetail = {};
|
||||
const {
|
||||
fillPaints,
|
||||
strokeWeight,
|
||||
strokePaints,
|
||||
cornerRadius,
|
||||
dashPattern,
|
||||
borderStrokeWeightsIndependent,
|
||||
borderBottomWeight,
|
||||
borderLeftWeight,
|
||||
borderRightWeight,
|
||||
borderTopWeight,
|
||||
rectangleCornerRadiiIndependent,
|
||||
rectangleBottomLeftCornerRadius,
|
||||
rectangleBottomRightCornerRadius,
|
||||
rectangleTopLeftCornerRadius,
|
||||
rectangleTopRightCornerRadius,
|
||||
strokeAlign,
|
||||
opacity
|
||||
} = node as FigmaNode<'ROUNDED_RECTANGLE'>;
|
||||
const background = figmaPaintsToColor(fillPaints, {
|
||||
w: node.size.x,
|
||||
h: node.size.y
|
||||
});
|
||||
if (background) {
|
||||
detail.background = background;
|
||||
}
|
||||
if (cornerRadius > 0) {
|
||||
detail.borderRadius = cornerRadius;
|
||||
} else if (rectangleCornerRadiiIndependent === true) {
|
||||
detail.borderRadius = [
|
||||
rectangleTopLeftCornerRadius || 0,
|
||||
rectangleTopRightCornerRadius || 0,
|
||||
rectangleBottomRightCornerRadius || 0,
|
||||
rectangleBottomLeftCornerRadius || 0
|
||||
];
|
||||
}
|
||||
detail.borderDash = dashPattern || [];
|
||||
detail.boxSizing = 'border-box';
|
||||
if (strokeAlign === 'CENTER') {
|
||||
detail.boxSizing = 'center-line';
|
||||
} else if (strokeAlign === 'OUTSIDE') {
|
||||
detail.boxSizing = 'content-box';
|
||||
}
|
||||
|
||||
if (strokePaints?.length === 1 && strokePaints[0].color) {
|
||||
const hexColor = figmaPaintsToHexColor(strokePaints);
|
||||
if (hexColor) {
|
||||
detail.borderColor = hexColor;
|
||||
}
|
||||
|
||||
if (borderStrokeWeightsIndependent) {
|
||||
detail.borderWidth = [borderTopWeight || 0, borderRightWeight || 0, borderBottomWeight || 0, borderLeftWeight || 0];
|
||||
} else {
|
||||
detail.borderWidth = strokeWeight;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof opacity === 'number' && opacity >= 0) {
|
||||
detail.opacity = opacity;
|
||||
}
|
||||
|
||||
detail = {
|
||||
...detail,
|
||||
...getShadow(node.effects)
|
||||
};
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
export function getShadow(effects?: FigmaEffect[]): Pick<Partial<ElementBaseDetail>, 'shadowBlur' | 'shadowColor' | 'shadowOffsetX' | 'shadowOffsetY'> {
|
||||
const shadow = {};
|
||||
if (Array.isArray(effects) && effects.length > 0) {
|
||||
for (let i = 0; i < effects.length; i++) {
|
||||
const { color, offset, spread = 0, type, visible, radius } = effects[i];
|
||||
if (visible === true && type === 'DROP_SHADOW') {
|
||||
return {
|
||||
shadowColor: figmaColorToHex(color),
|
||||
shadowBlur: spread || radius || 0, // TODO
|
||||
shadowOffsetX: offset.x || 0,
|
||||
shadowOffsetY: offset.y || 0
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return shadow;
|
||||
}
|
||||
|
||||
export function getStrokeColor(node: FigmaNodeStrokeBase): string | null {
|
||||
const { strokePaints } = node;
|
||||
let stroke: string | null = null;
|
||||
if (strokePaints?.length > 0) {
|
||||
const hexColor = figmaPaintsToHexColor(strokePaints);
|
||||
if (hexColor) {
|
||||
stroke = hexColor;
|
||||
}
|
||||
}
|
||||
return stroke;
|
||||
}
|
||||
|
||||
export function getFillColor(node: FigmaNodeFillBase): string | null {
|
||||
const { fillPaints } = node;
|
||||
let fill: string | null = null;
|
||||
if (fillPaints?.length === 1 && fillPaints[0].color && fillPaints[0].type === 'SOLID') {
|
||||
const hexColor = figmaPaintsToHexColor(fillPaints);
|
||||
if (hexColor) {
|
||||
fill = hexColor;
|
||||
}
|
||||
}
|
||||
return fill;
|
||||
}
|
||||
|
||||
export function getFillPathCommands(node: FigmaNodeFillBase): SVGPathCommand[] {
|
||||
const pathCmds: SVGPathCommand[] = [];
|
||||
const { fillGeometry } = node;
|
||||
if (Array.isArray(fillGeometry) && fillGeometry[0] && Array.isArray(fillGeometry[0]?.commands)) {
|
||||
const { commands } = fillGeometry[0];
|
||||
let pathCmd: SVGPathCommand | null = null;
|
||||
|
||||
commands.forEach((item, i) => {
|
||||
if (typeof item === 'string') {
|
||||
if (pathCmd?.type && Array.isArray(pathCmd.params)) {
|
||||
pathCmds.push(pathCmd);
|
||||
}
|
||||
pathCmd = { type: item as SVGPathCommandType, params: [] };
|
||||
} else if (typeof item === 'number' && Array.isArray(pathCmd?.params)) {
|
||||
pathCmd.params.push(item);
|
||||
}
|
||||
|
||||
if (i === commands.length - 1 && pathCmd) {
|
||||
pathCmds.push(pathCmd);
|
||||
pathCmd = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
return pathCmds;
|
||||
}
|
||||
|
||||
export function getFillAttributes(node: FigmaNodeFillBase): { commands: SVGPathCommand[]; fillRule?: string } {
|
||||
const pathCmds: SVGPathCommand[] = [];
|
||||
const { fillGeometry } = node;
|
||||
let fillRule: string | undefined = undefined;
|
||||
if (Array.isArray(fillGeometry) && fillGeometry[0] && Array.isArray(fillGeometry[0]?.commands)) {
|
||||
const { commands, windingRule } = fillGeometry[0];
|
||||
let pathCmd: SVGPathCommand | null = null;
|
||||
if (windingRule === 'ODD') {
|
||||
fillRule = 'evenodd';
|
||||
} else if (windingRule === 'NONZERO') {
|
||||
fillRule = 'nonzero';
|
||||
}
|
||||
|
||||
commands.forEach((item, i) => {
|
||||
if (typeof item === 'string') {
|
||||
if (pathCmd?.type && Array.isArray(pathCmd.params)) {
|
||||
pathCmds.push(pathCmd);
|
||||
}
|
||||
pathCmd = { type: item as SVGPathCommandType, params: [] };
|
||||
} else if (typeof item === 'number' && Array.isArray(pathCmd?.params)) {
|
||||
pathCmd.params.push(item);
|
||||
}
|
||||
|
||||
if (i === commands.length - 1 && pathCmd) {
|
||||
pathCmds.push(pathCmd);
|
||||
pathCmd = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
const attrs: { commands: SVGPathCommand[]; fillRule?: string } = {
|
||||
commands: pathCmds
|
||||
};
|
||||
if (fillRule) {
|
||||
attrs.fillRule = fillRule;
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
export function getStrokePathCommands(node: FigmaNodeStrokeBase): SVGPathCommand[] {
|
||||
const pathCmds: SVGPathCommand[] = [];
|
||||
const { strokeGeometry } = node;
|
||||
if (Array.isArray(strokeGeometry) && strokeGeometry[0] && Array.isArray(strokeGeometry[0]?.commands)) {
|
||||
const { commands } = strokeGeometry[0];
|
||||
let pathCmd: SVGPathCommand | null = null;
|
||||
|
||||
commands.forEach((item, i) => {
|
||||
if (typeof item === 'string') {
|
||||
if (pathCmd?.type && Array.isArray(pathCmd.params)) {
|
||||
pathCmds.push(pathCmd);
|
||||
}
|
||||
pathCmd = { type: item as SVGPathCommandType, params: [] };
|
||||
} else if (typeof item === 'number' && Array.isArray(pathCmd?.params)) {
|
||||
pathCmd.params.push(item);
|
||||
}
|
||||
|
||||
if (i === commands.length - 1 && pathCmd) {
|
||||
pathCmds.push(pathCmd);
|
||||
pathCmd = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
return pathCmds;
|
||||
}
|
||||
|
||||
export function hasFillImage(node: FigmaNode) {
|
||||
const { fillPaints } = node;
|
||||
if (Array.isArray(fillPaints)) {
|
||||
for (let i = 0; i < fillPaints.length; i++) {
|
||||
const paint = fillPaints[i];
|
||||
if (paint.visible === true && (paint as FigmaFillPaintImage).image) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
121
packages/figma/src/figma-node/color.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { is, calcDistance } from '@idraw/util';
|
||||
import { LinearGradientColor, RadialGradientColor } from '@idraw/types';
|
||||
import type { FigmaColor, FigmaPaint, FigmaFillPaintSolid, FigmaFillPaintGradientLinear, FigmaFillPaintGradientRadial } from '../types';
|
||||
import { parseLinearGradientParamsFromTransform, parseRadialOrDiamondGradientParamsFromTransform } from './gradient';
|
||||
|
||||
function numToHex(num: number): string {
|
||||
const unit = 255;
|
||||
const hexNum = Math.min(Math.max(Math.round(num * unit), 0), unit);
|
||||
const hex = hexNum.toString(16).toUpperCase().padStart(2, '0');
|
||||
return hex;
|
||||
}
|
||||
|
||||
export function figmaColorToHex(color: FigmaColor, opts?: { opacity?: number }): string {
|
||||
const { r, g, b, a } = color;
|
||||
let opacity = 1;
|
||||
if (is.number(opts?.opacity)) {
|
||||
opacity = opts?.opacity as number;
|
||||
}
|
||||
const list: string[] = ['#', numToHex(r), numToHex(g), numToHex(b)];
|
||||
const alpha = a * opacity;
|
||||
if (alpha < 1) {
|
||||
list.push(numToHex(alpha));
|
||||
}
|
||||
return list.join('');
|
||||
}
|
||||
|
||||
export function figmaPaintsToHexColor(paints: FigmaPaint[]): string {
|
||||
if (Array.isArray(paints) && paints.length > 0) {
|
||||
for (let i = 0; i < paints.length; i++) {
|
||||
const { color, opacity, visible, type } = paints[i] as FigmaFillPaintSolid;
|
||||
if (visible === true && type === 'SOLID') {
|
||||
return figmaColorToHex(color, { opacity });
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'transparent';
|
||||
}
|
||||
|
||||
export function figmaPaintToLinearGradient(paint: FigmaFillPaintGradientLinear, opts: { w: number; h: number }): LinearGradientColor | string {
|
||||
const { type, transform, stops } = paint;
|
||||
const { w, h } = opts;
|
||||
if (type === 'GRADIENT_LINEAR') {
|
||||
const { start, end } = parseLinearGradientParamsFromTransform(w, h, transform);
|
||||
const linearGradient: LinearGradientColor = {
|
||||
type: 'linear-gradient',
|
||||
start,
|
||||
end,
|
||||
stops: stops.map((stop) => {
|
||||
const { position, color } = stop;
|
||||
return {
|
||||
color: figmaColorToHex(color),
|
||||
offset: position
|
||||
};
|
||||
})
|
||||
};
|
||||
return linearGradient;
|
||||
}
|
||||
return 'transparent';
|
||||
}
|
||||
|
||||
export function figmaPaintToRadialGradient(paint: FigmaFillPaintGradientRadial, opts: { w: number; h: number }): RadialGradientColor | string {
|
||||
const { type, transform, stops } = paint;
|
||||
const { w, h } = opts;
|
||||
if (type === 'GRADIENT_RADIAL') {
|
||||
const { rotation, center, radius } = parseRadialOrDiamondGradientParamsFromTransform(w, h, transform);
|
||||
const centerPoint = {
|
||||
x: center[0],
|
||||
y: center[1]
|
||||
};
|
||||
const radiusPoint = {
|
||||
x: radius[0],
|
||||
y: radius[1]
|
||||
};
|
||||
const r = calcDistance(centerPoint, radiusPoint);
|
||||
const radialGradient: RadialGradientColor = {
|
||||
type: 'radial-gradient',
|
||||
angle: rotation,
|
||||
inner: {
|
||||
x: centerPoint.x,
|
||||
y: centerPoint.y,
|
||||
radius: r
|
||||
},
|
||||
outer: {
|
||||
x: radiusPoint.x,
|
||||
y: radiusPoint.y,
|
||||
radius: r
|
||||
},
|
||||
stops: stops.map((stop) => {
|
||||
const { position, color } = stop;
|
||||
return {
|
||||
color: figmaColorToHex(color),
|
||||
offset: position
|
||||
};
|
||||
})
|
||||
};
|
||||
return radialGradient;
|
||||
}
|
||||
return 'transparent';
|
||||
}
|
||||
|
||||
export function figmaPaintsToColor(paints: FigmaPaint[], opts: { w: number; h: number }): string | LinearGradientColor | RadialGradientColor {
|
||||
if (Array.isArray(paints) && paints.length > 0) {
|
||||
for (let i = 0; i < paints.length; i++) {
|
||||
const { visible, type } = paints[i];
|
||||
|
||||
if (visible === true) {
|
||||
if (type === 'SOLID') {
|
||||
const { color, opacity } = paints[i] as FigmaFillPaintSolid;
|
||||
return figmaColorToHex(color, { opacity });
|
||||
}
|
||||
if (type === 'GRADIENT_LINEAR') {
|
||||
return figmaPaintToLinearGradient(paints[i] as FigmaFillPaintGradientLinear, opts);
|
||||
}
|
||||
if (type === 'GRADIENT_RADIAL') {
|
||||
return figmaPaintToRadialGradient(paints[i] as FigmaFillPaintGradientRadial, opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'transparent';
|
||||
}
|
||||
27
packages/figma/src/figma-node/ellipse.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import type { Element } from '@idraw/types';
|
||||
import { nodeToElementBase, nodeToBaseDetail } from './base';
|
||||
import { nodeToOperations } from './operations';
|
||||
import { mergeNodeOverrideData } from '../common/node';
|
||||
import type { FigmaNode, FigmaParseOptions } from '../types';
|
||||
|
||||
export function ellipseNodeToCircleElement(figmaNode: FigmaNode<'ELLIPSE'>, opts: FigmaParseOptions<'ELLIPSE'>): Element<'circle'> {
|
||||
const overrideData = mergeNodeOverrideData<'ELLIPSE'>(figmaNode, opts);
|
||||
const node = { ...figmaNode, ...overrideData };
|
||||
|
||||
const elemBase = nodeToElementBase(node as FigmaNode);
|
||||
const { w, h } = elemBase;
|
||||
const radius = Math.max(w, h) / 2;
|
||||
const baseDetail = nodeToBaseDetail(node as FigmaNode);
|
||||
const operations = nodeToOperations(node as FigmaNode);
|
||||
|
||||
const elem: Element<'circle'> = {
|
||||
...elemBase,
|
||||
type: 'circle',
|
||||
detail: {
|
||||
...(baseDetail as Element<'circle'>['detail']),
|
||||
radius
|
||||
},
|
||||
operations
|
||||
};
|
||||
return elem;
|
||||
}
|
||||
69
packages/figma/src/figma-node/gradient.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// import { matrixInverse } from '../common/matrix-inverse';
|
||||
// @ts-ignore
|
||||
import matrixInverse from 'matrix-inverse';
|
||||
import type { FigmaTransform } from '../types';
|
||||
|
||||
// https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/52136d7f7628ca704bb3905dc1e20f7ef50036f7/src/helpers/applyMatrixToPoint.ts
|
||||
function applyMatrixToPoint(matrix: number[][], point: number[]) {
|
||||
return [point[0] * matrix[0][0] + point[1] * matrix[0][1] + matrix[0][2], point[0] * matrix[1][0] + point[1] * matrix[1][1] + matrix[1][2]];
|
||||
}
|
||||
|
||||
// https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/52136d7f7628ca704bb3905dc1e20f7ef50036f7/src/helpers/extractLinearGradientStartEnd.ts
|
||||
export function parseLinearGradientParamsFromTransform(shapeWidth: number, shapeHeight: number, figmatTransform: FigmaTransform) {
|
||||
const { m00, m01, m02, m10, m11, m12 } = figmatTransform;
|
||||
const t = [
|
||||
[m00, m01, m02],
|
||||
[m10, m11, m12]
|
||||
];
|
||||
const transform = t.length === 2 ? [...t, [0, 0, 1]] : [...t];
|
||||
const mxInv = matrixInverse(transform);
|
||||
// const mxInv = transform;
|
||||
const startEnd = [
|
||||
[0, 0.5],
|
||||
[1, 0.5]
|
||||
].map((p) => applyMatrixToPoint(mxInv, p));
|
||||
return {
|
||||
start: {
|
||||
x: startEnd[0][0] * shapeWidth,
|
||||
y: startEnd[0][1] * shapeHeight
|
||||
},
|
||||
|
||||
end: {
|
||||
x: startEnd[1][0] * shapeWidth,
|
||||
y: startEnd[1][1] * shapeHeight
|
||||
}
|
||||
|
||||
// start: {
|
||||
// x: startEnd[0][0] * shapeWidth,
|
||||
// y: startEnd[1][1] * shapeHeight
|
||||
// },
|
||||
|
||||
// end: {
|
||||
// x: startEnd[1][0] * shapeWidth,
|
||||
// y: startEnd[0][1] * shapeHeight
|
||||
// }
|
||||
};
|
||||
}
|
||||
|
||||
// https://github.com/figma-plugin-helper-functions/figma-plugin-helpers/blob/52136d7f7628ca704bb3905dc1e20f7ef50036f7/src/helpers/extractRadialOrDiamondGradientParams.ts
|
||||
export function parseRadialOrDiamondGradientParamsFromTransform(shapeWidth: number, shapeHeight: number, figmatTransform: FigmaTransform) {
|
||||
const { m00, m01, m02, m10, m11, m12 } = figmatTransform;
|
||||
const t = [
|
||||
[m00, m01, m02],
|
||||
[m10, m11, m12]
|
||||
];
|
||||
const transform = t.length === 2 ? [...t, [0, 0, 1]] : [...t];
|
||||
const mxInv = matrixInverse(transform);
|
||||
// const mxInv = transform;
|
||||
const centerPoint = applyMatrixToPoint(mxInv, [0.5, 0.5]);
|
||||
const rxPoint = applyMatrixToPoint(mxInv, [1, 0.5]);
|
||||
const ryPoint = applyMatrixToPoint(mxInv, [0.5, 1]);
|
||||
const rx = Math.sqrt(Math.pow(rxPoint[0] - centerPoint[0], 2) + Math.pow(rxPoint[1] - centerPoint[1], 2));
|
||||
const ry = Math.sqrt(Math.pow(ryPoint[0] - centerPoint[0], 2) + Math.pow(ryPoint[1] - centerPoint[1], 2));
|
||||
const angle = Math.atan((rxPoint[1] - centerPoint[1]) / (rxPoint[0] - centerPoint[0])) * (180 / Math.PI);
|
||||
return {
|
||||
rotation: angle,
|
||||
center: [centerPoint[0] * shapeWidth, centerPoint[1] * shapeHeight],
|
||||
radius: [rx * shapeWidth, ry * shapeHeight]
|
||||
};
|
||||
}
|
||||
83
packages/figma/src/figma-node/image.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import type { Element, ElementImageDetail } from '@idraw/types';
|
||||
import { nodeToElementBase, nodeToBaseDetail, getFillPathCommands } from './base';
|
||||
import { nodeToOperations } from './operations';
|
||||
import type { FigmaNode, FigmaFillPaint, FigmaFillPaintImage, FigmaParseOptions } from '../types';
|
||||
import { figmaImageDir } from '../config';
|
||||
import { mergeNodeOverrideData, uint8ArrayToBase64 } from '../common/node';
|
||||
|
||||
async function getFillImageDetail(fillPaints: FigmaFillPaint[], opts: FigmaParseOptions): Promise<ElementImageDetail> {
|
||||
const { figmaMap } = opts;
|
||||
const detail: ElementImageDetail = {
|
||||
src: ''
|
||||
};
|
||||
for (let i = 0; i < fillPaints.length; i++) {
|
||||
if (fillPaints[i].type === 'IMAGE') {
|
||||
const fillPaintImage: FigmaFillPaintImage = fillPaints[i] as FigmaFillPaintImage;
|
||||
const { image, imageScaleMode, originalImageHeight, originalImageWidth } = fillPaintImage;
|
||||
const hashStr = Array.from(image.hash)
|
||||
.map((num) => num.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
const imageKey = `${figmaImageDir}${hashStr}`;
|
||||
const imageBuffer = figmaMap[imageKey];
|
||||
if (imageBuffer) {
|
||||
const base64 = await uint8ArrayToBase64(imageBuffer as Uint8Array, { type: 'image/png' });
|
||||
detail.src = base64;
|
||||
}
|
||||
if (imageScaleMode === 'FILL') {
|
||||
detail.scaleMode = 'fill';
|
||||
} else if (imageScaleMode === 'FIT') {
|
||||
detail.scaleMode = 'fit';
|
||||
} else if (imageScaleMode === 'TILE') {
|
||||
detail.scaleMode = 'tile';
|
||||
}
|
||||
if (originalImageHeight >= 0) {
|
||||
detail.originH = originalImageHeight;
|
||||
}
|
||||
if (originalImageWidth >= 0) {
|
||||
detail.originW = originalImageWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
|
||||
export async function nodeToImageElement(figmaNode: FigmaNode, opts: FigmaParseOptions): Promise<Element<'image'>> {
|
||||
const overrideData = mergeNodeOverrideData(figmaNode, opts);
|
||||
const node = { ...figmaNode, ...overrideData };
|
||||
|
||||
const { fillPaints, fillGeometry } = node;
|
||||
const elemBase = nodeToElementBase(node);
|
||||
const elem: Element<'image'> = {
|
||||
...elemBase,
|
||||
type: 'image',
|
||||
detail: {
|
||||
src: ''
|
||||
}
|
||||
};
|
||||
const operations: Required<Element<'rect'>>['operations'] = nodeToOperations(node);
|
||||
let detail: Element<'image'>['detail'] = nodeToBaseDetail(node) as Element<'image'>['detail'];
|
||||
let imageDetail = await getFillImageDetail(fillPaints, opts);
|
||||
detail = {
|
||||
...detail,
|
||||
...imageDetail
|
||||
};
|
||||
|
||||
if (Array.isArray(fillGeometry) && fillGeometry.length > 0) {
|
||||
const commands = getFillPathCommands(node);
|
||||
detail.clipPath = {
|
||||
commands,
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
originW: elemBase.w,
|
||||
originH: elemBase.h
|
||||
};
|
||||
detail.clipPathStrokeColor = detail.borderColor;
|
||||
if (typeof detail.borderWidth === 'number') {
|
||||
detail.clipPathStrokeWidth = detail.borderWidth;
|
||||
}
|
||||
}
|
||||
|
||||
elem.operations = operations;
|
||||
elem.detail = detail as Element<'image'>['detail'];
|
||||
return elem;
|
||||
}
|
||||
40
packages/figma/src/figma-node/line.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { Element } from '@idraw/types';
|
||||
import { nodeToElementBase, getStrokeColor, getStrokePathCommands } from './base';
|
||||
import { nodeToOperations } from './operations';
|
||||
import { mergeNodeOverrideData } from '../common/node';
|
||||
import type { FigmaNode, FigmaParseOptions } from '../types';
|
||||
|
||||
// TODO
|
||||
export function lineNodeToPathElement(figmaNode: FigmaNode<'LINE'>, opts: FigmaParseOptions<'LINE'>): Element<'path'> {
|
||||
const overrideData = mergeNodeOverrideData<'LINE'>(figmaNode, opts);
|
||||
const node = { ...figmaNode, ...overrideData };
|
||||
|
||||
const elemBase = nodeToElementBase(node);
|
||||
const strokeWidth = node.strokeWeight || 1;
|
||||
const height = elemBase.h || strokeWidth;
|
||||
const y = elemBase.h ? elemBase.y : elemBase.y - height / 2;
|
||||
const elem: Element<'path'> = {
|
||||
...elemBase,
|
||||
y,
|
||||
h: height,
|
||||
type: 'path',
|
||||
detail: {
|
||||
commands: getStrokePathCommands(node),
|
||||
// strokeWidth: 1,
|
||||
originX: 0,
|
||||
originY: -height,
|
||||
originW: elemBase.w,
|
||||
originH: height
|
||||
}
|
||||
};
|
||||
const strokeColor = getStrokeColor(node);
|
||||
if (strokeColor) {
|
||||
// elem.detail.stroke = strokeColor;
|
||||
elem.detail.fill = strokeColor;
|
||||
}
|
||||
const operations: Required<Element<'rect'>>['operations'] = nodeToOperations(node);
|
||||
// const detail: Required<Element<'rect'>>['detail'] = nodeToBaseDetail(node);
|
||||
elem.operations = operations;
|
||||
// elem.detail = detail;
|
||||
return elem;
|
||||
}
|
||||
298
packages/figma/src/figma-node/node.ts
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
import type { Element } from '@idraw/types';
|
||||
import { nodeToElementBase, nodeToBaseDetail, hasFillImage } from './base';
|
||||
import { figmaColorToHex, figmaPaintsToColor } from './color';
|
||||
import { textNodeToTextElement } from './text';
|
||||
import { nodeToImageElement } from './image';
|
||||
import { roundedRectangleNodeToRectElement } from './rectangle';
|
||||
import { ellipseNodeToCircleElement } from './ellipse';
|
||||
import { regularPolygonNodeToPathElement } from './regular-polygon';
|
||||
import { lineNodeToPathElement } from './line';
|
||||
import { starNodeToPathElement } from './star';
|
||||
import { vectorNodeToPathElement } from './vector';
|
||||
import { getOverrideNodeMap, mergeNodeOverrideData } from '../common/node';
|
||||
import { nodeToOperations } from './operations';
|
||||
|
||||
import type { FigmaGUID, FigmaNode, FigmaNodeType, FigmaParseOptions, FigmaSymbolOverrideItem, FigmaInstanceNode } from '../types';
|
||||
import { figmaGUIDToID } from '../common/node';
|
||||
import { resetGroupSize } from '../common/calc';
|
||||
|
||||
async function instanceNodeToGroupElement(figmaNode: FigmaNode<'INSTANCE'>, opts: FigmaParseOptions<'INSTANCE'>): Promise<Element<'group'>> {
|
||||
const { backupNodeMap } = opts;
|
||||
const overrideData = mergeNodeOverrideData<'INSTANCE'>(figmaNode, opts);
|
||||
const node = { ...figmaNode, ...overrideData };
|
||||
const { derivedSymbolData } = node;
|
||||
const elemBase = nodeToElementBase(node);
|
||||
const elem: Element<'group'> = {
|
||||
...elemBase,
|
||||
type: 'group',
|
||||
detail: {
|
||||
children: []
|
||||
}
|
||||
};
|
||||
const operations: Required<Element<'group'>>['operations'] = nodeToOperations(node);
|
||||
const detail: Partial<Element<'group'>['detail']> = nodeToBaseDetail(node);
|
||||
elem.detail = {
|
||||
...elem.detail,
|
||||
...detail
|
||||
};
|
||||
|
||||
if (Array.isArray(derivedSymbolData)) {
|
||||
for (let i = 0; i < derivedSymbolData.length; i++) {
|
||||
const item = derivedSymbolData[i];
|
||||
const { guidPath, ...restProperties } = item;
|
||||
// const { fillGeometry, strokeGeometry, } = item as Partial<FigmaNode<'VECTOR'>>;
|
||||
|
||||
let copyNode: FigmaNode | undefined = undefined;
|
||||
if (Array.isArray(item.guidPath.guids) && item.guidPath.guids.length > 0) {
|
||||
// TODO
|
||||
const id = figmaGUIDToID(item.guidPath.guids[0]);
|
||||
copyNode = backupNodeMap[id];
|
||||
}
|
||||
|
||||
if (copyNode) {
|
||||
const childNode = { ...copyNode, ...restProperties };
|
||||
const chidElem = await figmaNodeToElement(childNode, opts);
|
||||
elem.detail.children.push(chidElem as Element);
|
||||
}
|
||||
|
||||
// const baseSize: ElementSize = {
|
||||
// x: 0, // TODO
|
||||
// y: 0, // TODO
|
||||
// w: elemBase.w,
|
||||
// h: elemBase.h
|
||||
// };
|
||||
// if (restProperties.size) {
|
||||
// baseSize.w = restProperties.size.x;
|
||||
// baseSize.h = restProperties.size.y;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
elem.operations = operations;
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
async function nestedNodeToGroupElement<T extends 'CANVAS' | 'FRAME' | 'SYMBOL' | 'BOOLEAN_OPERATION' = 'CANVAS'>(
|
||||
figmaNode: FigmaNode<'CANVAS'> | FigmaNode<'FRAME'> | FigmaNode<'SYMBOL'> | FigmaNode<'BOOLEAN_OPERATION'>,
|
||||
opts: FigmaParseOptions<T>
|
||||
): Promise<Element<'group'>> {
|
||||
const overrideData = mergeNodeOverrideData<T>(figmaNode as any, opts);
|
||||
const node: FigmaNode = { ...figmaNode, ...overrideData } as FigmaNode;
|
||||
const elemBase = nodeToElementBase(node);
|
||||
let group: Element<'group'> = {
|
||||
...elemBase,
|
||||
type: 'group',
|
||||
detail: {
|
||||
children: [],
|
||||
overflow: 'visible'
|
||||
},
|
||||
operations: nodeToOperations(node)
|
||||
};
|
||||
// const baseDetail = nodeToBaseDetail(treeNode.node);
|
||||
if ((node as unknown as FigmaNode<'FRAME'>).type === 'FRAME') {
|
||||
// const background = figmaPaintsToHexColor(node.fillPaints);
|
||||
const background = figmaPaintsToColor(node.fillPaints, { w: elemBase.w, h: elemBase.h });
|
||||
|
||||
if (background) {
|
||||
group.detail.background = background;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.backgroundColor) {
|
||||
if (!group.global) {
|
||||
group.global = {};
|
||||
}
|
||||
group.global.background = figmaColorToHex(node.backgroundColor);
|
||||
}
|
||||
|
||||
if (Array.isArray(node.children)) {
|
||||
for (let i = 0; i < node.children.length; i++) {
|
||||
const child = node.children[i];
|
||||
const elem: Element = (await figmaNodeToElement(child, opts as FigmaParseOptions)) as Element;
|
||||
group.detail.children.push(elem);
|
||||
}
|
||||
|
||||
group.detail.children.reverse();
|
||||
}
|
||||
|
||||
if (['FRAME', 'BOOLEAN_OPERATION'].includes(node.type)) {
|
||||
if (group.x === 0 || group.y === 0) {
|
||||
const size = resetGroupSize(group);
|
||||
group = {
|
||||
...size,
|
||||
...group
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if ((node as unknown as FigmaNode<'FRAME'>).type === 'FRAME') {
|
||||
const figmaNode = node as unknown as FigmaNode<'FRAME'>;
|
||||
if (figmaNode.frameMaskDisabled === true) {
|
||||
group.detail.overflow = 'visible';
|
||||
} else {
|
||||
group.detail.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
async function canvasNodeToGroupElement(node: FigmaNode<'CANVAS'>, opts: FigmaParseOptions): Promise<Element<'group'>> {
|
||||
return await nestedNodeToGroupElement(node, opts);
|
||||
}
|
||||
|
||||
async function booleanOperationNodeToGroupElement(
|
||||
node: FigmaNode<'BOOLEAN_OPERATION'>,
|
||||
opts: FigmaParseOptions<'BOOLEAN_OPERATION'>
|
||||
): Promise<Element<'group' | 'path'>> {
|
||||
const { overrideNodeMap = {} } = opts;
|
||||
const overrideProperties: FigmaNode = {} as FigmaNode;
|
||||
const { fillPaints, strokePaints, fillGeometry, strokeGeometry } = node;
|
||||
|
||||
let useFillGeometry = false;
|
||||
if (Array.isArray(fillPaints) && fillPaints.length > 0 && Array.isArray(fillGeometry) && fillGeometry.length > 0) {
|
||||
useFillGeometry = fillPaints.findIndex((p) => p.visible === true) >= 0;
|
||||
}
|
||||
|
||||
let useStrokeGeometry = false;
|
||||
if (Array.isArray(strokePaints) && strokePaints.length > 0 && Array.isArray(strokeGeometry) && strokeGeometry.length > 0) {
|
||||
useStrokeGeometry = strokePaints.findIndex((p) => p.visible === true) >= 0;
|
||||
}
|
||||
|
||||
if (useFillGeometry === true || useStrokeGeometry === true) {
|
||||
return await vectorNodeToPathElement(node as unknown as FigmaNode<'VECTOR'>, opts as FigmaParseOptions<'VECTOR'>);
|
||||
}
|
||||
|
||||
if (Array.isArray(fillPaints) && fillPaints.length > 0) {
|
||||
overrideProperties.fillPaints = fillPaints;
|
||||
}
|
||||
if (Array.isArray(strokePaints) && strokePaints.length > 0) {
|
||||
overrideProperties.strokePaints = strokePaints;
|
||||
}
|
||||
|
||||
return await nestedNodeToGroupElement<any>({ ...node, ...overrideNodeMap[figmaGUIDToID(node.guid)] }, {
|
||||
...opts,
|
||||
...{ overrideProperties: overrideProperties as any }
|
||||
} as FigmaParseOptions<any>);
|
||||
}
|
||||
|
||||
async function frameNodeToGroupElement(node: FigmaNode<'FRAME'>, opts: FigmaParseOptions<'FRAME'>): Promise<Element<'group'>> {
|
||||
return await nestedNodeToGroupElement(node, opts);
|
||||
}
|
||||
|
||||
async function symbolNodeToGroupElement(node: FigmaNode<'SYMBOL'>, opts: FigmaParseOptions<'SYMBOL'>): Promise<Element<'group'>> {
|
||||
return await nestedNodeToGroupElement(node, opts);
|
||||
}
|
||||
|
||||
async function instanceNodeToElement(node: FigmaNode<'INSTANCE'>, opts: FigmaParseOptions<'INSTANCE'>): Promise<Element | null> {
|
||||
const { instanceNodeMap, overrideProperties } = opts;
|
||||
const overrideNodeMap = {
|
||||
...opts.overrideNodeMap,
|
||||
...getOverrideNodeMap(node)
|
||||
};
|
||||
const symbolID = figmaGUIDToID(node.symbolData.symbolID);
|
||||
let symbolNode = instanceNodeMap[symbolID];
|
||||
|
||||
let newOverrideProperties: Partial<FigmaInstanceNode> = {
|
||||
...overrideProperties
|
||||
};
|
||||
|
||||
if (symbolNode) {
|
||||
symbolNode = {
|
||||
...symbolNode,
|
||||
...{
|
||||
visible: node.visible
|
||||
}
|
||||
};
|
||||
const elemSize = nodeToElementBase(node);
|
||||
const overrideData = mergeNodeOverrideData<'INSTANCE'>(node, opts);
|
||||
|
||||
// console.log(' -------------- node -------------- ', node.name, figmaGUIDToID(node.guid), symbolID);
|
||||
// console.log('node =', node);
|
||||
// console.log('overrideProperties =', overrideProperties);
|
||||
// console.log('symbolNode = ', symbolNode);
|
||||
// console.log('instanceNodeMap =', instanceNodeMap);
|
||||
// console.log('overrideNodeMap =', overrideNodeMap);
|
||||
// console.log('overrideData =======', node.overrideKey, overrideData);
|
||||
|
||||
if ((overrideData as FigmaSymbolOverrideItem).overriddenSymbolID) {
|
||||
const overriddenSymbolID = figmaGUIDToID((overrideData as FigmaSymbolOverrideItem).overriddenSymbolID as FigmaGUID);
|
||||
|
||||
if (instanceNodeMap[overriddenSymbolID]) {
|
||||
const overriddenSymbolNode = instanceNodeMap[overriddenSymbolID];
|
||||
symbolNode = {
|
||||
...symbolNode,
|
||||
...overriddenSymbolNode
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// TODO
|
||||
const { textData } = overrideData as Partial<FigmaNode<'TEXT'>>;
|
||||
if (textData) {
|
||||
(newOverrideProperties as unknown as FigmaNode<'TEXT'>).textData = textData;
|
||||
}
|
||||
}
|
||||
|
||||
const elem = await figmaNodeToElement(symbolNode, {
|
||||
...opts,
|
||||
...{ overrideNodeMap, overrideProperties: newOverrideProperties as Record<string, Partial<FigmaInstanceNode>> }
|
||||
});
|
||||
if (elem) {
|
||||
return {
|
||||
...elem,
|
||||
...elemSize
|
||||
};
|
||||
}
|
||||
return elem;
|
||||
}
|
||||
return await instanceNodeToGroupElement(node, opts as FigmaParseOptions<'INSTANCE'>);
|
||||
}
|
||||
|
||||
export async function figmaNodeToElement(node: FigmaNode<FigmaNodeType>, opts: FigmaParseOptions<FigmaNodeType>): Promise<Element | null> {
|
||||
if (node.type === 'CANVAS') {
|
||||
let elem: Element<'group'> = await canvasNodeToGroupElement(node as FigmaNode<'CANVAS'>, opts as FigmaParseOptions<'CANVAS'>);
|
||||
return elem;
|
||||
} else if (node.type === 'FRAME') {
|
||||
const elem: Element<'group'> = await frameNodeToGroupElement(node as FigmaNode<'FRAME'>, opts as FigmaParseOptions<'FRAME'>);
|
||||
return elem;
|
||||
} else if (node.type === 'SYMBOL') {
|
||||
const elem: Element<'group'> = await symbolNodeToGroupElement(node as FigmaNode<'SYMBOL'>, opts as FigmaParseOptions<'SYMBOL'>);
|
||||
return elem;
|
||||
} else if (hasFillImage(node as unknown as FigmaNode)) {
|
||||
const elem: Element<'image'> = await nodeToImageElement(node as unknown as FigmaNode, opts as FigmaParseOptions);
|
||||
return elem;
|
||||
} else if (node.type === 'ROUNDED_RECTANGLE') {
|
||||
const elem: Element<'rect'> = roundedRectangleNodeToRectElement(node as FigmaNode<'ROUNDED_RECTANGLE'>, opts as FigmaParseOptions<'ROUNDED_RECTANGLE'>);
|
||||
return elem;
|
||||
} else if (node.type === 'ELLIPSE') {
|
||||
const elem: Element<'circle'> = ellipseNodeToCircleElement(node as FigmaNode<'ELLIPSE'>, opts as FigmaParseOptions<'ELLIPSE'>);
|
||||
return elem;
|
||||
} else if (node.type === 'TEXT') {
|
||||
const elem: Element<'text'> = textNodeToTextElement(node as FigmaNode<'TEXT'>, opts as FigmaParseOptions<'TEXT'>);
|
||||
return elem;
|
||||
} else if (node.type === 'REGULAR_POLYGON') {
|
||||
const elem: Element<'path'> = regularPolygonNodeToPathElement(node as FigmaNode<'REGULAR_POLYGON'>, opts as FigmaParseOptions<'REGULAR_POLYGON'>);
|
||||
return elem;
|
||||
} else if (node.type === 'LINE') {
|
||||
const elem: Element<'path'> = lineNodeToPathElement(node as FigmaNode<'LINE'>, opts as FigmaParseOptions<'LINE'>);
|
||||
return elem;
|
||||
} else if (node.type === 'STAR') {
|
||||
const elem: Element<'path'> = starNodeToPathElement(node as FigmaNode<'STAR'>, opts as FigmaParseOptions<'STAR'>);
|
||||
return elem;
|
||||
} else if (node.type === 'VECTOR') {
|
||||
const elem: Element<'path'> = vectorNodeToPathElement(node as FigmaNode<'VECTOR'>, opts as FigmaParseOptions<'VECTOR'>);
|
||||
return elem;
|
||||
} else if (node.type === 'INSTANCE') {
|
||||
const elem: Element | null = await instanceNodeToElement(node as FigmaNode<'INSTANCE'>, opts as FigmaParseOptions<'INSTANCE'>);
|
||||
return elem;
|
||||
} else if (node.type === 'BOOLEAN_OPERATION') {
|
||||
const elem: Element | null = await booleanOperationNodeToGroupElement(
|
||||
node as FigmaNode<'BOOLEAN_OPERATION'>,
|
||||
opts as FigmaParseOptions<'BOOLEAN_OPERATION'>
|
||||
);
|
||||
return elem;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
18
packages/figma/src/figma-node/operations.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import type { ElementOperations } from '@idraw/types';
|
||||
import type { FigmaNode, FigmaNodeType } from '../types';
|
||||
|
||||
export function nodeToOperations(node: FigmaNode<FigmaNodeType>): ElementOperations {
|
||||
const operations: ElementOperations = {};
|
||||
const { visible } = node as FigmaNode<FigmaNodeType>;
|
||||
|
||||
if (visible === false) {
|
||||
operations.invisible = true;
|
||||
}
|
||||
|
||||
// TODO
|
||||
if (node.mask === true) {
|
||||
operations.invisible = true;
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
22
packages/figma/src/figma-node/rectangle.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { Element } from '@idraw/types';
|
||||
import { nodeToElementBase, nodeToBaseDetail } from './base';
|
||||
import { nodeToOperations } from './operations';
|
||||
import { mergeNodeOverrideData } from '../common/node';
|
||||
import type { FigmaNode, FigmaParseOptions } from '../types';
|
||||
|
||||
export function roundedRectangleNodeToRectElement(figmaNode: FigmaNode<'ROUNDED_RECTANGLE'>, opts: FigmaParseOptions<'ROUNDED_RECTANGLE'>): Element<'rect'> {
|
||||
const overrideData = mergeNodeOverrideData<'ROUNDED_RECTANGLE'>(figmaNode, opts);
|
||||
const node = { ...figmaNode, ...overrideData };
|
||||
|
||||
const elemBase = nodeToElementBase(node as FigmaNode);
|
||||
const elem: Element<'rect'> = {
|
||||
...elemBase,
|
||||
type: 'rect',
|
||||
detail: {}
|
||||
};
|
||||
const operations: Required<Element<'rect'>>['operations'] = nodeToOperations(node as FigmaNode);
|
||||
const detail: Required<Element<'rect'>>['detail'] = nodeToBaseDetail(node as FigmaNode);
|
||||
elem.operations = operations;
|
||||
elem.detail = detail;
|
||||
return elem;
|
||||
}
|
||||
31
packages/figma/src/figma-node/regular-polygon.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { Element } from '@idraw/types';
|
||||
import { nodeToElementBase, getFillColor, getFillPathCommands } from './base';
|
||||
import { nodeToOperations } from './operations';
|
||||
import { mergeNodeOverrideData } from '../common/node';
|
||||
import type { FigmaNode, FigmaParseOptions } from '../types';
|
||||
|
||||
export function regularPolygonNodeToPathElement(figmaNode: FigmaNode<'REGULAR_POLYGON'>, opts: FigmaParseOptions<'REGULAR_POLYGON'>): Element<'path'> {
|
||||
const overrideData = mergeNodeOverrideData<'REGULAR_POLYGON'>(figmaNode, opts);
|
||||
const node = { ...figmaNode, ...overrideData };
|
||||
const elemBase = nodeToElementBase(node);
|
||||
const elem: Element<'path'> = {
|
||||
...elemBase,
|
||||
type: 'path',
|
||||
detail: {
|
||||
commands: getFillPathCommands(node),
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
originW: elemBase.w,
|
||||
originH: elemBase.h
|
||||
}
|
||||
};
|
||||
const fillColor = getFillColor(node);
|
||||
if (fillColor) {
|
||||
elem.detail.fill = fillColor;
|
||||
}
|
||||
const operations: Required<Element<'rect'>>['operations'] = nodeToOperations(node);
|
||||
// const detail: Required<Element<'rect'>>['detail'] = nodeToBaseDetail(node);
|
||||
elem.operations = operations;
|
||||
// elem.detail = detail;
|
||||
return elem;
|
||||
}
|
||||
32
packages/figma/src/figma-node/star.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { Element } from '@idraw/types';
|
||||
import { nodeToElementBase, getFillColor, getFillPathCommands } from './base';
|
||||
import { nodeToOperations } from './operations';
|
||||
import { mergeNodeOverrideData } from '../common/node';
|
||||
import type { FigmaNode, FigmaParseOptions } from '../types';
|
||||
|
||||
export function starNodeToPathElement(figmaNode: FigmaNode<'STAR'>, opts: FigmaParseOptions<'STAR'>): Element<'path'> {
|
||||
const overrideData = mergeNodeOverrideData<'STAR'>(figmaNode, opts);
|
||||
const node = { ...figmaNode, ...overrideData };
|
||||
|
||||
const elemBase = nodeToElementBase(node);
|
||||
const elem: Element<'path'> = {
|
||||
...elemBase,
|
||||
type: 'path',
|
||||
detail: {
|
||||
commands: getFillPathCommands(node),
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
originW: elemBase.w,
|
||||
originH: elemBase.h
|
||||
}
|
||||
};
|
||||
const fillColor = getFillColor(node);
|
||||
if (fillColor) {
|
||||
elem.detail.fill = fillColor;
|
||||
}
|
||||
const operations: Required<Element<'rect'>>['operations'] = nodeToOperations(node);
|
||||
// const detail: Required<Element<'rect'>>['detail'] = nodeToBaseDetail(node);
|
||||
elem.operations = operations;
|
||||
// elem.detail = detail;
|
||||
return elem;
|
||||
}
|
||||
83
packages/figma/src/figma-node/text.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import type { Element } from '@idraw/types';
|
||||
import { nodeToElementBase } from './base';
|
||||
import { figmaPaintsToHexColor } from './color';
|
||||
import { nodeToOperations } from './operations';
|
||||
import type { FigmaNode, FigmaParseOptions } from '../types';
|
||||
import { mergeNodeOverrideData } from '../common/node';
|
||||
|
||||
const defaultFontWeight = 400;
|
||||
|
||||
export function textNodeToTextElement(figmaNode: FigmaNode<'TEXT'>, opts: FigmaParseOptions<'TEXT'>): Element<'text'> {
|
||||
const overrideData = mergeNodeOverrideData<'TEXT'>(figmaNode, opts);
|
||||
const node = { ...figmaNode, ...overrideData };
|
||||
const { textData, fontSize, fillPaints, fontName, textAlignHorizontal, textAlignVertical, textCase, lineHeight } = node;
|
||||
const { fontMetaData } = textData;
|
||||
const elemBase = nodeToElementBase(node as FigmaNode);
|
||||
|
||||
const elem: Element<'text'> = {
|
||||
...elemBase,
|
||||
type: 'text',
|
||||
detail: {
|
||||
text: textData.characters,
|
||||
fontFamily: fontName.family,
|
||||
// fontFamily: 'arial, sans-serif',
|
||||
fontSize: fontSize,
|
||||
textAlign: 'left',
|
||||
verticalAlign: 'top',
|
||||
wordBreak: 'normal',
|
||||
overflow: 'visible',
|
||||
minInlineSize: 'auto'
|
||||
}
|
||||
};
|
||||
|
||||
if (lineHeight.value > 0 && lineHeight.units === 'PIXELS') {
|
||||
elem.detail.lineHeight = lineHeight.value;
|
||||
}
|
||||
|
||||
// if (!((elem.detail.lineHeight as number) > 0 && elemBase.h > (elem.detail.lineHeight as number))) {
|
||||
// elem.detail.minInlineSize = 'maxContent';
|
||||
// }
|
||||
|
||||
const color = figmaPaintsToHexColor(fillPaints);
|
||||
if (color) {
|
||||
elem.detail.color = color;
|
||||
}
|
||||
|
||||
if (typeof node.opacity === 'number' && node.opacity >= 0) {
|
||||
elem.detail.opacity = node.opacity;
|
||||
}
|
||||
|
||||
if (Array.isArray(fontMetaData) && fontMetaData.length > 0) {
|
||||
const fontMeta = fontMetaData[0];
|
||||
const { fontWeight = defaultFontWeight } = fontMeta;
|
||||
elem.detail.fontWeight = fontWeight;
|
||||
// elem.detail.lineHeight = fontLineHeight * fontSize;
|
||||
}
|
||||
|
||||
if (textAlignHorizontal === 'LEFT') {
|
||||
elem.detail.textAlign = 'left';
|
||||
} else if (textAlignHorizontal === 'RIGHT') {
|
||||
elem.detail.textAlign = 'right';
|
||||
} else if (textAlignHorizontal === 'CENTER') {
|
||||
elem.detail.textAlign = 'center';
|
||||
}
|
||||
|
||||
if (textAlignVertical === 'TOP') {
|
||||
elem.detail.verticalAlign = 'top';
|
||||
} else if (textAlignVertical === 'BOTTOM') {
|
||||
elem.detail.verticalAlign = 'bottom';
|
||||
} else if (textAlignVertical === 'CENTER') {
|
||||
elem.detail.verticalAlign = 'middle';
|
||||
}
|
||||
|
||||
if (textCase === 'UPPER') {
|
||||
elem.detail.textTransform = 'uppercase';
|
||||
} else if (textCase === 'LOWER') {
|
||||
elem.detail.textTransform = 'lowercase';
|
||||
}
|
||||
|
||||
const operations: Required<Element<'text'>>['operations'] = nodeToOperations(node as FigmaNode);
|
||||
elem.operations = operations;
|
||||
|
||||
return elem;
|
||||
}
|
||||
136
packages/figma/src/figma-node/vector.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import type { Element, SVGPathCommand } from '@idraw/types';
|
||||
import { nodeToElementBase, getStrokeColor, getStrokePathCommands, getFillAttributes, getFillColor, getShadow } from './base';
|
||||
import { nodeToOperations } from './operations';
|
||||
import type { FigmaNode, FigmaParseOptions, FigmaVectorNode } from '../types';
|
||||
import { mergeNodeOverrideData } from '../common/node';
|
||||
import { figmaPaintsToColor } from './color';
|
||||
|
||||
type SVGPathDetail = Pick<Element<'path'>['detail'], 'commands' | 'stroke' | 'fill' | 'strokeWidth' | 'fillRule'>;
|
||||
|
||||
function getSVGPathDetail(node: FigmaVectorNode, opts: FigmaParseOptions<'VECTOR'>): SVGPathDetail {
|
||||
const { vectorData, strokeGeometry, fillGeometry, size, strokeWeight } = node;
|
||||
const { overrideProperties } = opts;
|
||||
const detail: SVGPathDetail = {
|
||||
commands: []
|
||||
};
|
||||
|
||||
const fillColor = figmaPaintsToColor(node.fillPaints || [], { w: size.x, h: size.y });
|
||||
const strokeColor = getStrokeColor(node);
|
||||
|
||||
// console.log('x --------- ', node.guid);
|
||||
// console.log('x --------- strokeGeometry ', strokeGeometry);
|
||||
// console.log('x --------- strokeWeight ', strokeWeight);
|
||||
// console.log('x --------- strokePaints ', node.strokePaints);
|
||||
// console.log('x --------- fillGeometry ', fillGeometry);
|
||||
// console.log('x --------- fillPaints ', node.fillPaints);
|
||||
|
||||
// if (strokeWeight > 0 && fillColor && strokeColor && vectorData?.vectorNetwork) {
|
||||
// const pathCmds: SVGPathCommand[] = [];
|
||||
// const { vectorNetwork } = vectorData;
|
||||
// const { segments, vertices } = vectorNetwork;
|
||||
// if (Array.isArray(segments) && Array.isArray(vertices)) {
|
||||
// for (const { start, end } of segments) {
|
||||
// const from = vertices[start.vertex];
|
||||
// const to = vertices[end.vertex];
|
||||
// pathCmds.push({
|
||||
// type: 'M',
|
||||
// params: [from.x, from.y]
|
||||
// });
|
||||
// pathCmds.push({
|
||||
// type: 'C',
|
||||
// params: [from.x + start.dx, from.y + start.dy, to.x + end.dx, to.y + end.dy, to.x, to.y]
|
||||
// });
|
||||
// // ctx.moveTo(from.x, from.y);
|
||||
// // ctx.bezierCurveTo(from.x + start.dx, from.y + start.dy, to.x + end.dx, to.y + end.dy, to.x, to.y);
|
||||
// }
|
||||
// }
|
||||
// detail.commands = pathCmds;
|
||||
|
||||
// detail.stroke = strokeColor;
|
||||
// detail.strokeWidth = strokeWeight;
|
||||
// detail.fill = fillColor;
|
||||
// }
|
||||
if (Array.isArray(strokeGeometry) && strokeGeometry.length > 0) {
|
||||
detail.commands = getStrokePathCommands(node);
|
||||
if (overrideProperties?.fillPaints) {
|
||||
if (fillColor) {
|
||||
detail.fill = fillColor;
|
||||
}
|
||||
} else {
|
||||
if (strokeColor) {
|
||||
detail.fill = strokeColor;
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(fillGeometry) && fillGeometry.length > 0) {
|
||||
const { commands, fillRule } = getFillAttributes(node);
|
||||
detail.commands = commands;
|
||||
if (fillRule) {
|
||||
detail.fillRule = fillRule;
|
||||
}
|
||||
if (fillColor) {
|
||||
detail.fill = fillColor;
|
||||
}
|
||||
} else if (strokeGeometry) {
|
||||
detail.commands = getStrokePathCommands(node);
|
||||
if (strokeColor) {
|
||||
detail.fill = strokeColor;
|
||||
}
|
||||
} else if (vectorData.vectorNetwork) {
|
||||
const pathCmds: SVGPathCommand[] = [];
|
||||
const { vectorNetwork } = vectorData;
|
||||
const { segments, vertices } = vectorNetwork;
|
||||
if (Array.isArray(segments) && Array.isArray(vertices)) {
|
||||
for (const { start, end } of segments) {
|
||||
const from = vertices[start.vertex];
|
||||
const to = vertices[end.vertex];
|
||||
pathCmds.push({
|
||||
type: 'M',
|
||||
params: [from.x, from.y]
|
||||
});
|
||||
pathCmds.push({
|
||||
type: 'C',
|
||||
params: [from.x + start.dx, from.y + start.dy, to.x + end.dx, to.y + end.dy, to.x, to.y]
|
||||
});
|
||||
// ctx.moveTo(from.x, from.y);
|
||||
// ctx.bezierCurveTo(from.x + start.dx, from.y + start.dy, to.x + end.dx, to.y + end.dy, to.x, to.y);
|
||||
}
|
||||
}
|
||||
detail.commands = pathCmds;
|
||||
|
||||
if (strokeColor) {
|
||||
detail.stroke = strokeColor;
|
||||
detail.strokeWidth = node.strokeWeight || 1;
|
||||
}
|
||||
if (fillColor) {
|
||||
detail.fill = fillColor;
|
||||
}
|
||||
}
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
export function vectorNodeToPathElement(figmaNode: FigmaNode<'VECTOR'>, opts: FigmaParseOptions<'VECTOR'>): Element<'path'> {
|
||||
const overrideData = mergeNodeOverrideData<'VECTOR'>(figmaNode, opts);
|
||||
|
||||
const node = { ...figmaNode, ...overrideData };
|
||||
const elemBase = nodeToElementBase(node as FigmaNode);
|
||||
const elem: Element<'path'> = {
|
||||
...elemBase,
|
||||
type: 'path',
|
||||
detail: {
|
||||
...getSVGPathDetail(node as FigmaVectorNode, opts),
|
||||
...(Array.isArray(node.effects) && node.effects.length > 0 ? getShadow(node.effects) : {}),
|
||||
// strokeWidth: node.strokeWeight || 1,
|
||||
originX: 0,
|
||||
originY: 0,
|
||||
originW: elemBase.w,
|
||||
originH: elemBase.h
|
||||
}
|
||||
};
|
||||
|
||||
const operations: Required<Element<'rect'>>['operations'] = nodeToOperations(node as FigmaNode);
|
||||
// const detail: Required<Element<'rect'>>['detail'] = nodeToBaseDetail(node);
|
||||
elem.operations = operations;
|
||||
// elem.detail = detail;
|
||||
return elem;
|
||||
}
|
||||
88
packages/figma/src/figma-object/index.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { parse as parseZIP } from 'uzip';
|
||||
import type { Data, Element } from '@idraw/types';
|
||||
import { parseCanvasFigBytes } from './parser';
|
||||
import type { FigmaMap, FigmaParseOptions } from '../types';
|
||||
import { figmaNodeToElement } from '../figma-node/node';
|
||||
import { figmaGUIDToID } from '../common/node';
|
||||
import { resetGroupSize } from '../common/calc';
|
||||
|
||||
const canvasFileName = 'canvas.fig';
|
||||
const metaFileName = 'meta.json';
|
||||
|
||||
export async function figmaBytesToMap(bytes: Uint8Array): Promise<FigmaMap> {
|
||||
const unzipped = parseZIP(bytes);
|
||||
|
||||
const fileKeys = Object.keys(unzipped);
|
||||
const map: Partial<FigmaMap> = {}; // TODO
|
||||
|
||||
for (let i = 0; i < fileKeys.length; i++) {
|
||||
const fileKey = fileKeys[i];
|
||||
if (fileKey === canvasFileName) {
|
||||
const canvasFig = unzipped[canvasFileName] as Uint8Array;
|
||||
const canvasResult: FigmaMap['canvas.fig'] = parseCanvasFigBytes({ bytes: canvasFig });
|
||||
map[fileKey] = canvasResult;
|
||||
} else if (fileKey === metaFileName) {
|
||||
const metaJSON = unzipped[metaFileName] as Uint8Array;
|
||||
const metaResult = JSON.parse(new TextDecoder().decode(metaJSON));
|
||||
map[fileKey] = metaResult;
|
||||
} else {
|
||||
map[fileKey] = unzipped[fileKey];
|
||||
}
|
||||
}
|
||||
|
||||
return map as FigmaMap;
|
||||
}
|
||||
|
||||
function figmaMapToParseOptions(figmaMap: FigmaMap): FigmaParseOptions {
|
||||
const instanceNodeMap: FigmaParseOptions['instanceNodeMap'] = {};
|
||||
const canvasFig = figmaMap['canvas.fig'];
|
||||
const { root, backupNodeMap } = canvasFig;
|
||||
if (Array.isArray(root.children)) {
|
||||
root.children.forEach((child) => {
|
||||
if (child?.type === 'CANVAS' && child.internalOnly === true && Array.isArray(child.children)) {
|
||||
child.children.forEach((item) => {
|
||||
const id = figmaGUIDToID(item.guid);
|
||||
instanceNodeMap[id] = item;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const opts: FigmaParseOptions = {
|
||||
figmaMap,
|
||||
instanceNodeMap,
|
||||
backupNodeMap
|
||||
};
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
export async function figmaMapToIDrawData(figmaMap: FigmaMap): Promise<Data> {
|
||||
const data: Data = { elements: [] };
|
||||
const canvasFig: FigmaMap['canvas.fig'] = figmaMap['canvas.fig'];
|
||||
const { root } = canvasFig;
|
||||
|
||||
// // TODO
|
||||
// console.log('root =', root);
|
||||
|
||||
if (Array.isArray(root.children)) {
|
||||
for (let i = 0; i < root.children.length; i++) {
|
||||
const child = root.children[i];
|
||||
if (child.internalOnly === true) {
|
||||
continue;
|
||||
}
|
||||
const elem: Element = (await figmaNodeToElement(child, figmaMapToParseOptions(figmaMap))) as Element;
|
||||
if (elem.type === 'group' && (elem.w === 0 || elem.h === 0)) {
|
||||
resetGroupSize(elem as Element<'group'>);
|
||||
}
|
||||
data.elements.push(elem);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function figmaBytesToIDrawData(bytes: Uint8Array): Promise<Data> {
|
||||
const figmaMap = await figmaBytesToMap(bytes);
|
||||
const data = await figmaMapToIDrawData(figmaMap);
|
||||
return data;
|
||||
}
|
||||
243
packages/figma/src/figma-object/parser.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { compileSchema, decodeBinarySchema } from 'kiwi-schema';
|
||||
import pako from 'pako';
|
||||
import type { FigmaNode } from '../types';
|
||||
|
||||
function parseBlob(key: string, opts: { bytes: Uint8Array }) {
|
||||
const { bytes } = opts;
|
||||
const view = new DataView(bytes.buffer);
|
||||
let offset = 0;
|
||||
|
||||
switch (key) {
|
||||
case 'vectorNetwork':
|
||||
if (bytes.length < 12) return;
|
||||
const vertexCount = view.getUint32(0, true);
|
||||
const segmentCount = view.getUint32(4, true);
|
||||
const regionCount = view.getUint32(8, true);
|
||||
const vertices = [];
|
||||
const segments = [];
|
||||
const regions = [];
|
||||
offset += 12;
|
||||
|
||||
for (let i = 0; i < vertexCount; i++) {
|
||||
if (offset + 12 > bytes.length) return;
|
||||
vertices.push({
|
||||
styleID: view.getUint32(offset + 0, true),
|
||||
x: view.getFloat32(offset + 4, true),
|
||||
y: view.getFloat32(offset + 8, true)
|
||||
});
|
||||
offset += 12;
|
||||
}
|
||||
|
||||
for (let i = 0; i < segmentCount; i++) {
|
||||
if (offset + 28 > bytes.length) return;
|
||||
const startVertex = view.getUint32(offset + 4, true);
|
||||
const endVertex = view.getUint32(offset + 16, true);
|
||||
if (startVertex >= vertexCount || endVertex >= vertexCount) return;
|
||||
segments.push({
|
||||
styleID: view.getUint32(offset + 0, true),
|
||||
start: {
|
||||
vertex: startVertex,
|
||||
dx: view.getFloat32(offset + 8, true),
|
||||
dy: view.getFloat32(offset + 12, true)
|
||||
},
|
||||
end: {
|
||||
vertex: endVertex,
|
||||
dx: view.getFloat32(offset + 20, true),
|
||||
dy: view.getFloat32(offset + 24, true)
|
||||
}
|
||||
});
|
||||
offset += 28;
|
||||
}
|
||||
|
||||
for (let i = 0; i < regionCount; i++) {
|
||||
if (offset + 8 > bytes.length) return;
|
||||
let styleID = view.getUint32(offset, true);
|
||||
const windingRule = styleID & 1 ? 'NONZERO' : 'ODD';
|
||||
styleID >>= 1;
|
||||
const loopCount = view.getUint32(offset + 4, true);
|
||||
const loops = [];
|
||||
offset += 8;
|
||||
|
||||
for (let j = 0; j < loopCount; j++) {
|
||||
if (offset + 4 > bytes.length) return;
|
||||
const indexCount = view.getUint32(offset, true);
|
||||
const indices = [];
|
||||
offset += 4;
|
||||
if (offset + indexCount * 4 > bytes.length) return;
|
||||
for (let k = 0; k < indexCount; k++) {
|
||||
const segment = view.getUint32(offset, true);
|
||||
if (segment >= segmentCount) return;
|
||||
indices.push(segment);
|
||||
offset += 4;
|
||||
}
|
||||
loops.push({ segments: indices });
|
||||
}
|
||||
|
||||
regions.push({ styleID, windingRule, loops });
|
||||
}
|
||||
|
||||
return { vertices, segments, regions };
|
||||
|
||||
case 'commands':
|
||||
const path = [];
|
||||
while (offset < bytes.length) {
|
||||
switch (bytes[offset++]) {
|
||||
case 0:
|
||||
path.push('Z');
|
||||
break;
|
||||
|
||||
case 1:
|
||||
if (offset + 8 > bytes.length) return;
|
||||
path.push('M', view.getFloat32(offset, true), view.getFloat32(offset + 4, true));
|
||||
offset += 8;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
if (offset + 8 > bytes.length) return;
|
||||
path.push('L', view.getFloat32(offset, true), view.getFloat32(offset + 4, true));
|
||||
offset += 8;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
if (offset + 16 > bytes.length) return;
|
||||
path.push(
|
||||
'Q',
|
||||
view.getFloat32(offset, true),
|
||||
view.getFloat32(offset + 4, true),
|
||||
view.getFloat32(offset + 8, true),
|
||||
view.getFloat32(offset + 12, true)
|
||||
);
|
||||
offset += 16;
|
||||
break;
|
||||
|
||||
case 4:
|
||||
if (offset + 24 > bytes.length) return;
|
||||
path.push(
|
||||
'C',
|
||||
view.getFloat32(offset, true),
|
||||
view.getFloat32(offset + 4, true),
|
||||
view.getFloat32(offset + 8, true),
|
||||
view.getFloat32(offset + 12, true),
|
||||
view.getFloat32(offset + 16, true),
|
||||
view.getFloat32(offset + 20, true)
|
||||
);
|
||||
offset += 24;
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCanvasFigBytesToNodeChanges(opts: { bytes: Uint8Array }) {
|
||||
const { bytes } = opts;
|
||||
const header = String.fromCharCode(...bytes.slice(0, 8));
|
||||
if (header !== 'fig-kiwi' && header !== 'fig-jam.') {
|
||||
throw new Error('Invalid header');
|
||||
}
|
||||
|
||||
const view = new DataView(bytes.buffer);
|
||||
const version = view.getUint32(8, true);
|
||||
const chunks = [];
|
||||
let offset = 12;
|
||||
|
||||
while (offset < bytes.length) {
|
||||
const chunkLength = view.getUint32(offset, true);
|
||||
offset += 4;
|
||||
chunks.push(bytes.slice(offset, offset + chunkLength));
|
||||
offset += chunkLength;
|
||||
}
|
||||
|
||||
if (chunks.length < 2) throw new Error('Not enough chunks');
|
||||
const encodedSchema = pako.inflateRaw(chunks[0]);
|
||||
const encodedData = pako.inflateRaw(chunks[1]);
|
||||
const schema = compileSchema(decodeBinarySchema(encodedSchema));
|
||||
const { nodeChanges, blobs } = schema.decodeMessage(encodedData);
|
||||
|
||||
// Make blob contents easier to understand
|
||||
const substituteBlob = (key: string, value: any) => {
|
||||
if (key.endsWith('Blob') && typeof value === 'number' && value === value >>> 0 && value < blobs.length) {
|
||||
key = key.slice(0, -4);
|
||||
value = blobs[value];
|
||||
value = parseBlob(key, value) || value;
|
||||
}
|
||||
return [key, value];
|
||||
};
|
||||
|
||||
const walkToParseBlob = (obj: any) => {
|
||||
const propKeys = Object.keys(obj);
|
||||
propKeys.forEach((propKey: string) => {
|
||||
const prop = obj[propKey];
|
||||
if (propKey.endsWith('Blob')) {
|
||||
const [newKey, newValue] = substituteBlob(propKey, prop);
|
||||
obj[newKey] = newValue;
|
||||
delete obj[propKey];
|
||||
} else if (Array.isArray(prop)) {
|
||||
prop.forEach((item) => {
|
||||
walkToParseBlob(item);
|
||||
});
|
||||
} else if (Object.prototype.toString.call(prop) === '[object Object]') {
|
||||
walkToParseBlob(prop);
|
||||
}
|
||||
});
|
||||
};
|
||||
nodeChanges.forEach((node: any) => {
|
||||
walkToParseBlob(node);
|
||||
});
|
||||
|
||||
return { version, blobs, nodeChanges };
|
||||
}
|
||||
|
||||
export function parseCanvasFigBytes(opts: { bytes: Uint8Array }) {
|
||||
const { version, nodeChanges, blobs } = parseCanvasFigBytesToNodeChanges(opts);
|
||||
const nodeMap: Record<string, FigmaNode> = {};
|
||||
const originalNodeChanges: FigmaNode[] = nodeChanges.map((node: any) => {
|
||||
const item = { ...node };
|
||||
const id = `${item.guid.sessionID}:${item.guid.localID}`;
|
||||
nodeMap[id] = item;
|
||||
return item;
|
||||
});
|
||||
|
||||
const nodes = new Map();
|
||||
|
||||
const orderByPosition = (
|
||||
{ parentIndex: { position: a } }: { parentIndex: { position: number } },
|
||||
{ parentIndex: { position: b } }: { parentIndex: { position: number } }
|
||||
) => {
|
||||
// @ts-ignore
|
||||
return (a < b) - (a > b);
|
||||
};
|
||||
|
||||
for (const node of nodeChanges) {
|
||||
const { sessionID, localID } = node.guid;
|
||||
nodes.set(`${sessionID}:${localID}`, node);
|
||||
}
|
||||
|
||||
for (const node of nodeChanges) {
|
||||
if (node.parentIndex) {
|
||||
const { sessionID, localID } = node.parentIndex.guid;
|
||||
const parent = nodes.get(`${sessionID}:${localID}`);
|
||||
if (parent) {
|
||||
parent.children ||= [];
|
||||
parent.children.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of nodeChanges) {
|
||||
if (node.children) {
|
||||
node.children.sort(orderByPosition);
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of nodeChanges) {
|
||||
delete node.parentIndex;
|
||||
}
|
||||
|
||||
const root = nodes.get('0:0');
|
||||
|
||||
return { version, root, blobs, backupNodeList: originalNodeChanges, backupNodeMap: nodeMap };
|
||||
}
|
||||
10
packages/figma/src/file.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { pickFile } from '@idraw/util';
|
||||
|
||||
export function pickFigmaFile(opts: { accept?: string; success: (data: { file: File }) => void; error?: (err: Error | any) => void }) {
|
||||
const { success, error } = opts;
|
||||
pickFile({
|
||||
accept: '*',
|
||||
success,
|
||||
error
|
||||
});
|
||||
}
|
||||
2
packages/figma/src/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { pickFigmaFile } from './file';
|
||||
export { figmaBytesToMap, figmaMapToIDrawData, figmaBytesToIDrawData } from './figma-object';
|
||||
448
packages/figma/src/types/figma.ts
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
// https://www.figma.com/developers/api#node-types
|
||||
|
||||
export type FigmaNodeType =
|
||||
| 'DOCUMENT'
|
||||
| 'CANVAS'
|
||||
| 'FRAME'
|
||||
| 'ROUNDED_RECTANGLE'
|
||||
| 'ELLIPSE'
|
||||
| 'TEXT'
|
||||
| 'VECTOR'
|
||||
| 'REGULAR_POLYGON'
|
||||
| 'LINE'
|
||||
| 'STAR'
|
||||
| 'SYMBOL'
|
||||
| 'INSTANCE'
|
||||
| 'BOOLEAN_OPERATION';
|
||||
|
||||
export type FigmaNode<T extends FigmaNodeType = 'CANVAS'> = FigmaNodeTypeMap[T];
|
||||
|
||||
export type FigmaColor = {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
};
|
||||
|
||||
export type FigmaSize = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export type FigmaObject = {
|
||||
nodeChanges: FigmaNode[];
|
||||
};
|
||||
|
||||
export type FigmaNodeMap = {
|
||||
[id: string]: FigmaNode;
|
||||
};
|
||||
|
||||
export type FigmaNodeTypeMap = {
|
||||
DOCUMENT: FigmaDocumentNode;
|
||||
CANVAS: FigmaCanvasNode;
|
||||
FRAME: FigmaFrameNode;
|
||||
ROUNDED_RECTANGLE: FigmaRoundedRectangleNode;
|
||||
ELLIPSE: FigmaRoundedEllipseNode;
|
||||
TEXT: FigmaTextNode;
|
||||
VECTOR: FigmaVectorNode;
|
||||
REGULAR_POLYGON: FigmaRegularPolygonNode;
|
||||
LINE: FigmaLineNode;
|
||||
STAR: FigmaStarNode;
|
||||
SYMBOL: FigmaSymbolNode;
|
||||
INSTANCE: FigmaInstanceNode;
|
||||
BOOLEAN_OPERATION: FigmaBooleanOperationNode;
|
||||
};
|
||||
|
||||
export type FigmaGUID = {
|
||||
localID: number;
|
||||
sessionID: number;
|
||||
};
|
||||
|
||||
export type FigmaTransform = {
|
||||
m00: number;
|
||||
m01: number;
|
||||
m02: number;
|
||||
m10: number;
|
||||
m11: number;
|
||||
m12: number;
|
||||
};
|
||||
|
||||
export type FigmaNodeBase = {
|
||||
opacity: number;
|
||||
name: string;
|
||||
guid: FigmaGUID;
|
||||
parentIndex?: {
|
||||
guid: FigmaGUID;
|
||||
position?: string;
|
||||
};
|
||||
transform: FigmaTransform;
|
||||
visible: boolean;
|
||||
locked: boolean;
|
||||
size: FigmaSize;
|
||||
overrideKey?: FigmaGUID;
|
||||
effects: FigmaEffect[];
|
||||
mask?: boolean;
|
||||
maskType?: string; // "ALPHA"
|
||||
};
|
||||
|
||||
export type FigmaEffect = {
|
||||
blendMode: string; // 'NORMAL'
|
||||
color: FigmaColor;
|
||||
offset: { x: number; y: number };
|
||||
radius: number;
|
||||
showShadowBehindNode: true;
|
||||
spread: number;
|
||||
type: string; // 'DROP_SHADOW'
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
export type FigmaPaint = FigmaFillPaint | FigmaStrokePaint;
|
||||
|
||||
export type FigmaFillPaint = FigmaFillPaintSolid | FigmaFillPaintGradientRadial | FigmaFillPaintGradientLinear | FigmaFillPaintImage;
|
||||
|
||||
export type FigmaFillPaintSolid = {
|
||||
blendMode: string;
|
||||
opacity: number;
|
||||
type: 'SOLID';
|
||||
visible: boolean;
|
||||
color: FigmaColor;
|
||||
};
|
||||
|
||||
export type FigmaFillPaintGradientRadial = {
|
||||
blendMode: string;
|
||||
opacity: number;
|
||||
type: 'GRADIENT_RADIAL';
|
||||
visible: boolean;
|
||||
transform: FigmaTransform;
|
||||
stops: Array<{
|
||||
color: FigmaColor;
|
||||
position: number;
|
||||
}>;
|
||||
stopsVar: Array<{
|
||||
color: FigmaColor;
|
||||
colorVar: {
|
||||
dataType: string; // 'COLOR';
|
||||
resolvedDataType: string; // 'COLOR';
|
||||
position: number;
|
||||
value: {
|
||||
colorValue: FigmaColor;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export type FigmaFillPaintGradientLinear = {
|
||||
blendMode: string;
|
||||
opacity: number;
|
||||
type: 'GRADIENT_LINEAR';
|
||||
visible: boolean;
|
||||
transform: FigmaTransform;
|
||||
stops: Array<{
|
||||
color: FigmaColor;
|
||||
position: number;
|
||||
}>;
|
||||
stopsVar: Array<{
|
||||
color: FigmaColor;
|
||||
colorVar: {
|
||||
dataType: string; // 'COLOR';
|
||||
resolvedDataType: string; // 'COLOR';
|
||||
position: number;
|
||||
value: {
|
||||
colorValue: FigmaColor;
|
||||
};
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
export type FigmaFillPaintImage = {
|
||||
blendMode: string; // 'NORMAL';
|
||||
opacity: number;
|
||||
type: 'IMAGE';
|
||||
image: {
|
||||
hash: Uint8Array;
|
||||
name: string;
|
||||
};
|
||||
imageScaleMode: string; //'FILL';
|
||||
imageShouldColorManage: boolean;
|
||||
imageThumbnail: {
|
||||
hash: Uint8Array;
|
||||
name: string;
|
||||
};
|
||||
originalImageHeight: number;
|
||||
originalImageWidth: number;
|
||||
rotation: number;
|
||||
scale: number;
|
||||
transform: FigmaTransform;
|
||||
visible: boolean;
|
||||
color: FigmaColor;
|
||||
};
|
||||
|
||||
export type FigmaFillGeometryItem = {
|
||||
commandsBlob?: Uint8Array;
|
||||
commands: Array<string | number>;
|
||||
styleID: number;
|
||||
windingRule: string; // 'NONZERO' | 'ODD'
|
||||
};
|
||||
|
||||
export type FigmaNodeFillBase = {
|
||||
fillGeometry: Array<FigmaFillGeometryItem>;
|
||||
fillPaints: Array<FigmaFillPaintSolid | FigmaFillPaintImage>;
|
||||
};
|
||||
|
||||
export type FigmaStrokeGeometryItem = {
|
||||
commandsBlob?: Uint8Array;
|
||||
commands: Array<string | number>;
|
||||
styleID: number;
|
||||
windingRule: string; // 'NONZERO';
|
||||
};
|
||||
|
||||
export type FigmaStrokePaint = {
|
||||
blendMode: string; // 'NORMAL';
|
||||
opacity: number;
|
||||
type: string; // 'SOLID';
|
||||
visible: boolean;
|
||||
color: FigmaColor;
|
||||
};
|
||||
|
||||
export type FigmaNodeStrokeBase = {
|
||||
strokeAlign: 'CENTER' | 'OUTSIDE' | 'INSIDE';
|
||||
strokeCap: string; // 'NONE';
|
||||
strokeGeometry: Array<FigmaStrokeGeometryItem>;
|
||||
strokeJoin: string; // 'MITER';
|
||||
strokePaints: Array<FigmaStrokePaint>;
|
||||
strokeWeight: number;
|
||||
};
|
||||
|
||||
export type FigmaNodeCornerBase = {
|
||||
cornerRadius: number;
|
||||
rectangleCornerRadiiIndependent: boolean;
|
||||
rectangleBottomLeftCornerRadius: number;
|
||||
rectangleBottomRightCornerRadius: number;
|
||||
rectangleTopLeftCornerRadius: number;
|
||||
rectangleTopRightCornerRadius: number;
|
||||
};
|
||||
|
||||
export type FigmaNodeBoxBorderBase = FigmaNodeStrokeBase &
|
||||
FigmaNodeCornerBase & {
|
||||
borderStrokeWeightsIndependent?: boolean;
|
||||
borderBottomWeight?: number;
|
||||
borderLeftWeight?: number;
|
||||
borderRightWeight?: number;
|
||||
borderTopWeight?: number;
|
||||
dashPattern: number[];
|
||||
};
|
||||
|
||||
export type FigmaDocumentNode = FigmaNodeBase & {
|
||||
type: 'DOCUMENT';
|
||||
children: FigmaNode[];
|
||||
};
|
||||
|
||||
export type FigmaCanvasNode = FigmaNodeBase &
|
||||
FigmaNodeFillBase &
|
||||
FigmaNodeStrokeBase & {
|
||||
type: 'CANVAS';
|
||||
backgroundColor: FigmaColor;
|
||||
children?: FigmaNode[];
|
||||
internalOnly?: boolean;
|
||||
};
|
||||
|
||||
export type FigmaBooleanOperationNode = FigmaNodeBase &
|
||||
FigmaNodeFillBase &
|
||||
FigmaNodeStrokeBase & {
|
||||
type: 'BOOLEAN_OPERATION';
|
||||
backgroundColor: FigmaColor;
|
||||
children?: FigmaNode[];
|
||||
internalOnly?: boolean;
|
||||
};
|
||||
|
||||
export type FigmaSymbolNode = FigmaNodeBase &
|
||||
FigmaNodeFillBase &
|
||||
FigmaNodeStrokeBase & {
|
||||
type: 'SYMBOL';
|
||||
backgroundColor: FigmaColor;
|
||||
children?: FigmaNode[];
|
||||
};
|
||||
|
||||
// TODO
|
||||
export type FigmaDerivedSymbolDataItem = Partial<FigmaNode> & {
|
||||
guidPath: {
|
||||
guids: Array<FigmaGUID>;
|
||||
};
|
||||
};
|
||||
|
||||
export type FigmaDerivedSymbolData = Array<FigmaDerivedSymbolDataItem>;
|
||||
|
||||
export type FigmaSymbolOverrideItem = {
|
||||
overriddenSymbolID?: FigmaGUID;
|
||||
guidPath: {
|
||||
guids: Array<FigmaGUID>;
|
||||
};
|
||||
} & Partial<Omit<FigmaNode, 'guidPath'>>;
|
||||
|
||||
export type FigmaInstanceNode = FigmaNodeBase &
|
||||
FigmaNodeFillBase &
|
||||
FigmaNodeStrokeBase & {
|
||||
type: 'INSTANCE';
|
||||
backgroundColor: FigmaColor;
|
||||
derivedSymbolData: FigmaDerivedSymbolData;
|
||||
symbolData: {
|
||||
symbolID: FigmaGUID;
|
||||
symbolOverrides: Array<FigmaSymbolOverrideItem>;
|
||||
};
|
||||
symbolDescription: string;
|
||||
};
|
||||
|
||||
export type FigmaFrameNode = FigmaNodeBase &
|
||||
FigmaNodeFillBase & {
|
||||
type: 'FRAME';
|
||||
children?: FigmaNode[];
|
||||
frameMaskDisabled?: boolean;
|
||||
};
|
||||
|
||||
export type FigmaRoundedRectangleNode = FigmaNodeBase &
|
||||
FigmaNodeBoxBorderBase &
|
||||
FigmaNodeFillBase & {
|
||||
type: 'ROUNDED_RECTANGLE';
|
||||
};
|
||||
|
||||
export type FigmaRoundedEllipseNode = FigmaNodeBase &
|
||||
FigmaNodeBoxBorderBase &
|
||||
FigmaNodeFillBase & {
|
||||
type: 'ELLIPSE';
|
||||
};
|
||||
|
||||
export type FigmaTextNode = FigmaNodeBase &
|
||||
FigmaNodeBoxBorderBase &
|
||||
FigmaNodeFillBase & {
|
||||
type: 'TEXT';
|
||||
textAlignHorizontal: 'LEFT' | 'RIGHT' | 'CENTER' | 'JUSTIFIED';
|
||||
textAlignVertical: 'TOP' | 'CENTER' | 'BOTTOM';
|
||||
textAutoResize: string; // 'NONE';
|
||||
textBidiVersion: number;
|
||||
textTracking: number;
|
||||
textUserLayoutVersion: number;
|
||||
textCase?: 'UPPER' | 'LOWER';
|
||||
textData: {
|
||||
characters: string;
|
||||
layoutSize: FigmaSize;
|
||||
fontMetaData: Array<{
|
||||
fontDigest: Uint8Array;
|
||||
fontLineHeight: number;
|
||||
fontStyle: string; // NORMAL
|
||||
fontWeight: number;
|
||||
key: {
|
||||
family: string;
|
||||
style: string;
|
||||
};
|
||||
}>;
|
||||
glyphs: Array<{
|
||||
advance: number;
|
||||
commandsBlob: number;
|
||||
firstCharacter: number;
|
||||
fontSize: number;
|
||||
position: { x: number; y: number };
|
||||
}>;
|
||||
};
|
||||
fontName: {
|
||||
family: string;
|
||||
postscript: string;
|
||||
style: string;
|
||||
};
|
||||
fontSize: number;
|
||||
fontVariantCaps: string;
|
||||
fontVariantCommonLigatures: boolean;
|
||||
fontVariantContextualLigatures: boolean;
|
||||
fontVariantDiscretionaryLigatures: boolean;
|
||||
fontVariantHistoricalLigatures: boolean;
|
||||
fontVariantNumericFigure: string;
|
||||
fontVariantNumericFraction: string;
|
||||
fontVariantNumericSpacing: string;
|
||||
fontVariantOrdinal: boolean;
|
||||
fontVariantPosition: string;
|
||||
fontVariantSlashedZero: boolean;
|
||||
fontVariations: any[]; // TODO
|
||||
fontVersion: string;
|
||||
lineHeight: {
|
||||
units: string;
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type FigmaRegularPolygonNode = FigmaNodeBase &
|
||||
FigmaNodeStrokeBase &
|
||||
FigmaNodeFillBase & {
|
||||
type: 'REGULAR_POLYGON';
|
||||
};
|
||||
|
||||
export type FigmaVectorNode = FigmaNodeBase &
|
||||
FigmaNodeStrokeBase &
|
||||
FigmaNodeFillBase & {
|
||||
type: 'VECTOR';
|
||||
vectorData: {
|
||||
normalizedSize: FigmaSize;
|
||||
styleOverrideTable: Array<{
|
||||
handleMirroring: string; // 'ANGLE_AND_LENGTH';
|
||||
styleID: number;
|
||||
}>;
|
||||
vectorNetwork: {
|
||||
regions: any[]; // TODO
|
||||
segments: Array<{
|
||||
start: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
vertex: number;
|
||||
};
|
||||
end: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
vertex: number;
|
||||
};
|
||||
styleID: number;
|
||||
}>;
|
||||
vertices: Array<{
|
||||
styleID: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type FigmaLineNode = FigmaNodeBase &
|
||||
FigmaNodeStrokeBase &
|
||||
FigmaNodeFillBase & {
|
||||
type: 'LINE';
|
||||
};
|
||||
|
||||
export type FigmaStarNode = FigmaNodeBase &
|
||||
FigmaNodeStrokeBase &
|
||||
FigmaNodeFillBase & {
|
||||
type: 'STAR';
|
||||
};
|
||||
|
||||
export interface FigmaCanvasFigObject {
|
||||
version: number;
|
||||
root: FigmaNode<'DOCUMENT'>;
|
||||
blobs: Array<{
|
||||
bytes: Uint8Array;
|
||||
}>;
|
||||
backupNodeList: FigmaNode[];
|
||||
backupNodeMap: Record<string, FigmaNode>;
|
||||
}
|
||||
|
||||
export type FigmaMap = {
|
||||
[key: string]: Uint8Array;
|
||||
} & {
|
||||
'canvas.fig': FigmaCanvasFigObject;
|
||||
'thumbnail.png': Uint8Array;
|
||||
'meta.json': any;
|
||||
'images/': Uint8Array;
|
||||
};
|
||||
|
||||
export type FigmaParseOptions<T extends FigmaNodeType = 'CANVAS'> = {
|
||||
figmaMap: FigmaMap;
|
||||
backupNodeMap: Record<string, FigmaNode>;
|
||||
instanceNodeMap: Record<string, FigmaNode>;
|
||||
overrideNodeMap?: Record<string, Partial<FigmaNode<T>>>;
|
||||
overrideProperties?: Partial<FigmaNode<T>>;
|
||||
};
|
||||
1
packages/figma/src/types/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './figma';
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
import { createUUID } from '@idraw/util';
|
||||
import type { ElementSize } from '@idraw/types';
|
||||
import type { LabComponent, LabComponentItem } from '../../../src';
|
||||
|
||||
function createButtonItem(variantName: string, size?: Partial<ElementSize>) {
|
||||
const componentItem: LabComponentItem = {
|
||||
uuid: createUUID(),
|
||||
type: 'component-item',
|
||||
name: `Button ${variantName}`,
|
||||
x: 50,
|
||||
y: 50,
|
||||
w: 100,
|
||||
h: 100,
|
||||
...(size || {}),
|
||||
detail: {
|
||||
bgColor: '#ff98001F',
|
||||
children: [
|
||||
{
|
||||
uuid: createUUID(),
|
||||
type: 'group',
|
||||
x: 8,
|
||||
y: 8,
|
||||
w: 80,
|
||||
h: 50,
|
||||
detail: {
|
||||
bgColor: '#0382761F',
|
||||
children: [
|
||||
{
|
||||
uuid: createUUID(),
|
||||
type: 'rect',
|
||||
x: 5,
|
||||
y: 8,
|
||||
w: 70,
|
||||
h: 32,
|
||||
detail: {
|
||||
bgColor: '#038276',
|
||||
borderRadius: 4
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: createUUID(),
|
||||
type: 'text',
|
||||
x: 5,
|
||||
y: 8,
|
||||
w: 70,
|
||||
h: 32,
|
||||
detail: {
|
||||
color: '#ffffff',
|
||||
fontSize: 14,
|
||||
text: 'Button'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// {
|
||||
// uuid: createUUID(),
|
||||
// type: 'circle',
|
||||
// x: -20,
|
||||
// y: 0,
|
||||
// w: 100,
|
||||
// h: 100,
|
||||
// detail: {
|
||||
// bgColor: '#ff9800'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// uuid: createUUID(),
|
||||
// type: 'circle',
|
||||
// x: 0,
|
||||
// y: 0,
|
||||
// w: 100,
|
||||
// h: 100,
|
||||
// detail: {
|
||||
// bgColor: '#ffc106'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// uuid: createUUID(),
|
||||
// type: 'circle',
|
||||
// x: 20,
|
||||
// y: 0,
|
||||
// w: 100,
|
||||
// h: 100,
|
||||
// detail: {
|
||||
// bgColor: '#cddc39'
|
||||
// }
|
||||
// },
|
||||
// {
|
||||
// uuid: createUUID(),
|
||||
// type: 'circle',
|
||||
// x: 40,
|
||||
// y: 0,
|
||||
// w: 100,
|
||||
// h: 100,
|
||||
// detail: {
|
||||
// bgColor: '#4caf50'
|
||||
// }
|
||||
// }
|
||||
]
|
||||
}
|
||||
};
|
||||
return componentItem;
|
||||
}
|
||||
|
||||
export function createButton(name: string, size?: Partial<ElementSize>) {
|
||||
const button: LabComponent = {
|
||||
uuid: createUUID(),
|
||||
type: 'component',
|
||||
name: `Button ${name}`,
|
||||
x: 50,
|
||||
y: 50,
|
||||
w: 360,
|
||||
h: 200,
|
||||
detail: {
|
||||
bgColor: '#aaaaaa54',
|
||||
default: createButtonItem('default', { angle: 30 }),
|
||||
variants: [
|
||||
// createButtonItem('primary', { x: 200, y: 50, angle: 30 }),
|
||||
createButtonItem('primary', { x: 200, y: 50 }),
|
||||
createButtonItem('secondary', { x: 50, y: 180 })
|
||||
]
|
||||
},
|
||||
...(size || {})
|
||||
};
|
||||
return button;
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import { createUUID } from '@idraw/util';
|
||||
import type { ElementSize } from '@idraw/types';
|
||||
import type { LabComponent, LabComponentItem } from '../../../src';
|
||||
|
||||
function createCheckboxItem(variantName: string, size?: Partial<ElementSize>) {
|
||||
const componentItem: LabComponentItem = {
|
||||
uuid: createUUID(),
|
||||
type: 'component-item',
|
||||
name: `Checkbox ${variantName}`,
|
||||
x: 50,
|
||||
y: 50,
|
||||
w: 100,
|
||||
h: 100,
|
||||
...(size || {}),
|
||||
detail: {
|
||||
children: [
|
||||
{
|
||||
uuid: createUUID(),
|
||||
type: 'rect',
|
||||
x: -40,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
bgColor: '#f44336'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: createUUID(),
|
||||
type: 'rect',
|
||||
x: -20,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
bgColor: '#ff9800'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: createUUID(),
|
||||
type: 'rect',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
bgColor: '#ffc106'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: createUUID(),
|
||||
type: 'rect',
|
||||
x: 20,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
bgColor: '#cddc39'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: createUUID(),
|
||||
type: 'rect',
|
||||
x: 40,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
bgColor: '#4caf50'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
return componentItem;
|
||||
}
|
||||
|
||||
export function createCheckbox(name: string, size?: Partial<ElementSize>) {
|
||||
const checkbox: LabComponent = {
|
||||
uuid: createUUID(),
|
||||
type: 'component',
|
||||
name: `Checkbox ${name}`,
|
||||
x: 50,
|
||||
y: 50,
|
||||
w: 360,
|
||||
h: 200,
|
||||
detail: {
|
||||
bgColor: '#aaaaaa54',
|
||||
default: createCheckboxItem('default'),
|
||||
variants: [createCheckboxItem('primary', { x: 200, y: 50 }), createCheckboxItem('secondary', { x: 50, y: 180 })]
|
||||
},
|
||||
...(size || {})
|
||||
};
|
||||
return checkbox;
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import type { LabData } from '../../src';
|
||||
import { createButton } from './components/button';
|
||||
import { createCheckbox } from './components/checkbox';
|
||||
|
||||
const data: LabData = {
|
||||
components: [
|
||||
createButton('001', { angle: 45 }),
|
||||
// createButton('001', {}),
|
||||
createButton('002', { x: 450 }),
|
||||
createCheckbox('001', { x: 50, y: 300 }),
|
||||
createCheckbox('002', { x: 450, y: 300 })
|
||||
],
|
||||
modules: [],
|
||||
pages: []
|
||||
};
|
||||
|
||||
export default data;
|
||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
|
@ -1,25 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<style></style>
|
||||
<meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0; padding: 0;
|
||||
/* background: #f0f0f088; */
|
||||
}
|
||||
/* canvas {
|
||||
background-image:
|
||||
linear-gradient(#aaaaaa40 1px, transparent 0),
|
||||
linear-gradient(90deg, #aaaaaa40 1px, transparent 0),
|
||||
linear-gradient(#aaa 1px, transparent 0),
|
||||
linear-gradient(90deg, #aaa 1px, transparent 0);
|
||||
background-size: 10px 10px, 10px 10px, 50px 50px, 50px 50px;
|
||||
background-color: #ffffff;
|
||||
} */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="lab"></div>
|
||||
</body>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</html>
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { Lab } from '../src/index';
|
||||
import data from './data';
|
||||
|
||||
const dom = document.querySelector('#lab') as HTMLDivElement;
|
||||
const root = createRoot(dom);
|
||||
|
||||
const App = () => {
|
||||
const style = { margin: 0, padding: 0 };
|
||||
const [width, setWidth] = useState<number>(window.innerWidth);
|
||||
const [height, setHeight] = useState<number>(window.innerHeight);
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', () => {
|
||||
setWidth(window.innerWidth);
|
||||
setHeight(window.innerHeight);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// const style = { margin: 40 };
|
||||
// const width = 800;
|
||||
// const height = 600;
|
||||
|
||||
return <Lab width={width} height={height} style={style} labData={data} />;
|
||||
};
|
||||
|
||||
root.render(<App />);
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"name": "@idraw/lab",
|
||||
"version": "0.4.0-beta.25",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.1.3",
|
||||
"@idraw/core": "workspace:^0.4.0-beta.25",
|
||||
"@idraw/util": "workspace:^0.4.0-beta.25",
|
||||
"antd": "^5.5.0",
|
||||
"classnames": "^2.3.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@idraw/types": "workspace:^0.4.0-beta.25",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import { createContext } from 'react';
|
||||
import type { Data } from '@idraw/types';
|
||||
import { LabData, LabState, LabAction, LabContext, LabDrawDataType } from './types';
|
||||
import { parseComponentsToDrawData } from './util/view-data';
|
||||
|
||||
export function createLabData(): LabData {
|
||||
return {
|
||||
components: [],
|
||||
modules: [],
|
||||
pages: []
|
||||
};
|
||||
}
|
||||
|
||||
function parseDrawData(drawDataType: LabDrawDataType, labData: LabData | null): Data {
|
||||
let drawData: Data = { elements: [] };
|
||||
if (drawDataType === 'component') {
|
||||
drawData = parseComponentsToDrawData(labData?.components || []);
|
||||
}
|
||||
return drawData;
|
||||
}
|
||||
|
||||
export function createLabContextState(opts?: Partial<LabState>): LabState {
|
||||
const activeDrawDataType: LabDrawDataType = 'component';
|
||||
const labData: LabData = opts?.labData || createLabData();
|
||||
const viewDrawData = parseDrawData(activeDrawDataType, labData);
|
||||
|
||||
return {
|
||||
labData: labData,
|
||||
activeDrawDataType: activeDrawDataType,
|
||||
themeMode: opts?.themeMode || 'light',
|
||||
viewDrawData: viewDrawData,
|
||||
viewDrawUUID: null
|
||||
};
|
||||
}
|
||||
|
||||
export function createLabReducer(state: LabState, action: LabAction): LabState {
|
||||
switch (action.type) {
|
||||
case 'updateThemeMode': {
|
||||
if (!action?.payload?.themeMode) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
...{
|
||||
themeMode: action?.payload?.themeMode
|
||||
}
|
||||
};
|
||||
}
|
||||
case 'updateLabData': {
|
||||
if (!action?.payload?.labData) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
...{
|
||||
labData: action?.payload?.labData
|
||||
}
|
||||
};
|
||||
}
|
||||
case 'switchDrawDataType': {
|
||||
if (!action?.payload?.activeDrawDataType) {
|
||||
return state;
|
||||
}
|
||||
const newState = {
|
||||
...state,
|
||||
...{
|
||||
activeDrawDataType: action?.payload.activeDrawDataType,
|
||||
viewDrawData: parseDrawData(action?.payload?.activeDrawDataType, state.labData)
|
||||
}
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const Context = createContext<LabContext>({
|
||||
state: createLabContextState(),
|
||||
dispatch: () => {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
export const Provider = Context.Provider;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
@import '../../css/variable.less';
|
||||
|
||||
@mod-name: ~'@{prefix}-icon';
|
||||
|
||||
.@{mod-name} {
|
||||
font-size: 16;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit;
|
||||
font-style: normal;
|
||||
line-height: 0;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
|
||||
svg {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
@import './theme/dark.less';
|
||||
@import './theme/light.less';
|
||||
|
||||
@import './icons/index.less';
|
||||
|
||||
@import './modules/header.less';
|
||||
@import './modules/dashboard.less';
|
||||
@import './modules/sketch.less';
|
||||
@import './modules/toolbar.less';
|
||||
@import './modules/panel-layer.less';
|
||||
@import './modules/split-pane.less';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { createPrefixName } from './variable';
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
@import "../variable.less";
|
||||
|
||||
@mod-xxx: ~'@{prefix}-mod-xxx';
|
||||
|
||||
.@{mod-xxx} {
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
@import '../variable.less';
|
||||
|
||||
@mod-dashboard: ~'@{prefix}-mod-dashboard';
|
||||
|
||||
// @mod-dashboard-header-height: 36px;
|
||||
|
||||
.@{mod-dashboard} {
|
||||
color: ~'var(--@{prefix}-text-color)';
|
||||
background: ~'var(--@{prefix}-bg-color)';
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0 1px #0000001a, 0px 0px 0.5px #0000002e, 0px 3px 8px #0000001a, 0px 1px 3px #0000001a;
|
||||
background: ~'var(--@{prefix}-bg-color)';
|
||||
|
||||
canvas {
|
||||
// background: ~'var(--@{prefix}-bg-color)';
|
||||
background-image: linear-gradient(#aaaaaa30 1px, transparent 0), linear-gradient(90deg, #aaaaaa30 1px, transparent 0),
|
||||
linear-gradient(#aaaaaa40 1px, transparent 0), linear-gradient(90deg, #aaaaaa40 1px, transparent 0);
|
||||
background-size: 10px 10px, 10px 10px, 50px 50px, 50px 50px;
|
||||
}
|
||||
|
||||
// .@{mod-dashboard}-toolbar-position {
|
||||
// position: absolute;
|
||||
// bottom: 50px;
|
||||
// left: 50%;
|
||||
// max-width: 800px;
|
||||
// min-width: 400px;
|
||||
// transform: translateX(-50%);
|
||||
// }
|
||||
|
||||
.@{mod-dashboard}-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid;
|
||||
border-color: ~'var(--@{prefix}-border-color)';
|
||||
// height: @mod-dashboard-header-height;
|
||||
}
|
||||
|
||||
.@{mod-dashboard}-content {
|
||||
position: absolute;
|
||||
// top: @mod-dashboard-header-height;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.@{mod-dashboard}-left {
|
||||
border-right: 1px solid;
|
||||
border-color: ~'var(--@{prefix}-border-color)';
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.@{mod-dashboard}-right {
|
||||
border-left: 1px solid;
|
||||
border-color: ~'var(--@{prefix}-border-color)';
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.@{mod-dashboard}-center {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
@import '../variable.less';
|
||||
|
||||
@mod-header: ~'@{prefix}-mod-header';
|
||||
|
||||
.@{mod-header} {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
color: ~'var(--@{prefix}-text-color)';
|
||||
background: ~'var(--@{prefix}-bg-color)';
|
||||
|
||||
.@{mod-header}-theme-switch {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
@import '../variable.less';
|
||||
|
||||
@mod-panel-layer: ~'@{prefix}-mod-panel-layer';
|
||||
|
||||
.@{mod-panel-layer} {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
||||
.@{mod-panel-layer}-header {
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
// border-bottom: 1px solid;
|
||||
// border-color: ~'var(--@{prefix}-border-color)';
|
||||
}
|
||||
|
||||
.@{mod-panel-layer}-content {
|
||||
flex: 1;
|
||||
// display: flex;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
|
||||
// fix antd style
|
||||
.ant-tree-switcher {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ant-tree-node-content-wrapper {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.@{mod-panel-layer}-footer {
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
border-top: 1px solid;
|
||||
border-color: ~'var(--@{prefix}-border-color)';
|
||||
}
|
||||
|
||||
.@{mod-panel-layer}-tabs {
|
||||
// width: 100%;
|
||||
// display: flex;
|
||||
// padding-bottom: 100px;
|
||||
|
||||
.@{mod-panel-layer}-tab-title {
|
||||
font-size: 16;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.@{mod-panel-layer}-tab-content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
@import '../variable.less';
|
||||
|
||||
@mod-xxx: ~'@{prefix}-mod-sketch';
|
||||
|
||||
.@{mod-xxx} {
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
// @import '../variable.less';
|
||||
|
||||
@mod-split-pane: ~'@{prefix}-mod-split-pane';
|
||||
|
||||
.@{mod-split-pane} {
|
||||
// background: #000;
|
||||
opacity: 0.2;
|
||||
z-index: 1;
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
-moz-background-clip: padding;
|
||||
-webkit-background-clip: padding;
|
||||
background-clip: padding-box;
|
||||
|
||||
&:hover {
|
||||
-webkit-transition: all 2s ease;
|
||||
transition: all 2s ease;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
height: 11px;
|
||||
margin: -5px 0;
|
||||
border-top: 5px solid rgba(255, 255, 255, 0);
|
||||
border-bottom: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: row-resize;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.horizontal:hover {
|
||||
border-top: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
width: 11px;
|
||||
margin: 0 -6px;
|
||||
border-left: 5px solid rgba(255, 255, 255, 0);
|
||||
border-right: 5px solid rgba(255, 255, 255, 0);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
&.vertical:hover {
|
||||
border-left: 5px solid rgba(0, 0, 0, 0.5);
|
||||
border-right: 5px solid rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.disabled:hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
@import '../variable.less';
|
||||
|
||||
@mod-toolbar: ~'@{prefix}-mod-toolbar';
|
||||
|
||||
.@{mod-toolbar} {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: row;
|
||||
padding: 0 12px;
|
||||
|
||||
// height: 50px;
|
||||
box-sizing: border-box;
|
||||
// background: ~'var(--@{prefix}-bg-color)';
|
||||
// border-radius: 16px;
|
||||
// box-shadow: 0 0 0 1px #0000001a, 0px 0px 0.5px #0000002e, 0px 3px 8px #0000001a, 0px 1px 3px #0000001a;
|
||||
// border: 1px solid;
|
||||
// border-color: ~'var(--@{prefix}-border-color)';
|
||||
|
||||
.@{mod-toolbar}-left {
|
||||
display: flex;
|
||||
}
|
||||
.@{mod-toolbar}-right {
|
||||
display: flex;
|
||||
}
|
||||
.@{mod-toolbar}-middle {
|
||||
display: flex;
|
||||
margin: 0 12px;
|
||||
.@{mod-toolbar}-mode-switch {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
// hack style for antd
|
||||
.ant-radio-button-wrapper {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
@import '../variable.less';
|
||||
|
||||
&.@{prefix}-theme {
|
||||
&.@{prefix}-theme-dark {
|
||||
--@{prefix}-bg-color: @gray-9;
|
||||
--@{prefix}-text-color: @gray-1;
|
||||
--@{prefix}-border-color: @gray-6;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
@import '../variable.less';
|
||||
|
||||
&.@{prefix}-theme {
|
||||
--@{prefix}-bg-color: @white;
|
||||
--@{prefix}-text-color: @gray-10;
|
||||
--@{prefix}-border-color: @gray-4;
|
||||
}
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
@prefix: idraw-lab;
|
||||
|
||||
@white: #ffffff;
|
||||
@black: #000000;
|
||||
|
||||
// https://ant.lab/docs/spec/colors-cn
|
||||
// https://ant.lab/docs/spec/dark-cn
|
||||
@gray-1: #f5f5f5;
|
||||
@gray-2: #f0f0f0;
|
||||
@gray-3: #d9d9d9;
|
||||
@gray-4: #bfbfbf;
|
||||
@gray-5: #8c8c8c;
|
||||
@gray-6: #595959;
|
||||
@gray-7: #434343;
|
||||
@gray-8: #262626;
|
||||
@gray-9: #1f1f1f;
|
||||
@gray-10: #141414;
|
||||
@dark-gray-1: #141414;
|
||||
@dark-gray-2: #1f1f1f;
|
||||
@dark-gray-3: #262626;
|
||||
@dark-gray-4: #434343;
|
||||
@dark-gray-5: #595959;
|
||||
@dark-gray-6: #8c8c8c;
|
||||
@dark-gray-7: #bfbfbf;
|
||||
@dark-gray-8: #d9d9d9;
|
||||
@dark-gray-9: #f0f0f0;
|
||||
@dark-gray-10: #f5f5f5;
|
||||
|
||||
@blue-1: #e6f7ff;
|
||||
@blue-2: #bae7ff;
|
||||
@blue-3: #91d5ff;
|
||||
@blue-4: #69c0ff;
|
||||
@blue-5: #40a9ff;
|
||||
@blue-6: #1890ff;
|
||||
@blue-7: #096dd9;
|
||||
@blue-8: #0050b3;
|
||||
@blue-9: #003a8c;
|
||||
@blue-10: #002766;
|
||||
@dark-blue-1: #111d2c;
|
||||
@dark-blue-2: #112a45;
|
||||
@dark-blue-3: #15395b;
|
||||
@dark-blue-4: #164c7e;
|
||||
@dark-blue-5: #1765ad;
|
||||
@dark-blue-6: #177ddc;
|
||||
@dark-blue-7: #3c9ae8;
|
||||
@dark-blue-8: #65b7f3;
|
||||
@dark-blue-9: #8dcff8;
|
||||
@dark-blue-10: #b7e3fa;
|
||||
|
||||
@red-1: #fff1f0;
|
||||
@red-2: #ffccc7;
|
||||
@red-3: #ffa39e;
|
||||
@red-4: #ff7875;
|
||||
@red-5: #ff4d4f;
|
||||
@red-6: #f5222d;
|
||||
@red-7: #cf1322;
|
||||
@red-8: #a8071a;
|
||||
@red-9: #820014;
|
||||
@red-10: #5c0011;
|
||||
@dark-red-1: #2a1215;
|
||||
@dark-red-2: #431418;
|
||||
@dark-red-3: #58181c;
|
||||
@dark-red-4: #791a1f;
|
||||
@dark-red-5: #a61d24;
|
||||
@dark-red-6: #d32029;
|
||||
@dark-red-7: #e84749;
|
||||
@dark-red-8: #f37370;
|
||||
@dark-red-9: #f89f9a;
|
||||
@dark-red-10: #fac8c3;
|
||||
|
||||
@yellow-1: #feffe6;
|
||||
@yellow-2: #ffffb8;
|
||||
@yellow-3: #fffb8f;
|
||||
@yellow-4: #fff566;
|
||||
@yellow-5: #ffec3d;
|
||||
@yellow-6: #fadb14;
|
||||
@yellow-7: #d4b106;
|
||||
@yellow-8: #ad8b00;
|
||||
@yellow-9: #876800;
|
||||
@yellow-10: #614700;
|
||||
@dark-yellow-1: #2b2611;
|
||||
@dark-yellow-2: #443b11;
|
||||
@dark-yellow-3: #595014;
|
||||
@dark-yellow-4: #7c6e14;
|
||||
@dark-yellow-5: #aa9514;
|
||||
@dark-yellow-6: #d8bd14;
|
||||
@dark-yellow-7: #e8d639;
|
||||
@dark-yellow-8: #f3ea62;
|
||||
@dark-yellow-9: #f8f48b;
|
||||
@dark-yellow-10: #fafab5;
|
||||
|
||||
@green-1: #f6ffed;
|
||||
@green-2: #d9f7be;
|
||||
@green-3: #b7eb8f;
|
||||
@green-4: #95de64;
|
||||
@green-5: #73d13d;
|
||||
@green-6: #52c41a;
|
||||
@green-7: #389e0d;
|
||||
@green-8: #237804;
|
||||
@green-9: #135200;
|
||||
@green-10: #092b00;
|
||||
@dark-green-1: #162312;
|
||||
@dark-green-2: #1d3712;
|
||||
@dark-green-3: #274916;
|
||||
@dark-green-4: #306317;
|
||||
@dark-green-5: #3c8618;
|
||||
@dark-green-6: #49aa19;
|
||||
@dark-green-7: #6abe39;
|
||||
@dark-green-8: #8fd460;
|
||||
@dark-green-9: #b2e58b;
|
||||
@dark-green-10: #d5f2bb;
|
||||
|
||||
@cyan-1: #e6fffb;
|
||||
@cyan-2: #b5f5ec;
|
||||
@cyan-3: #87e8de;
|
||||
@cyan-4: #5cdbd3;
|
||||
@cyan-5: #36cfc9;
|
||||
@cyan-6: #13c2c2;
|
||||
@cyan-7: #08979c;
|
||||
@cyan-8: #006d75;
|
||||
@cyan-9: #00474f;
|
||||
@cyan-10: #002329;
|
||||
@dark-cyan-1: #112123;
|
||||
@dark-cyan-2: #113536;
|
||||
@dark-cyan-3: #144848;
|
||||
@dark-cyan-4: #146262;
|
||||
@dark-cyan-5: #138585;
|
||||
@dark-cyan-6: #13a8a8;
|
||||
@dark-cyan-7: #33bcb7;
|
||||
@dark-cyan-8: #58d1c9;
|
||||
@dark-cyan-9: #84e2d8;
|
||||
@dark-cyan-10: #b2f1e8;
|
||||
|
||||
@gold-1: #fffbe6;
|
||||
@gold-2: #fff1b8;
|
||||
@gold-3: #ffe58f;
|
||||
@gold-4: #ffd666;
|
||||
@gold-5: #ffc53d;
|
||||
@gold-6: #faad14;
|
||||
@gold-7: #d48806;
|
||||
@gold-8: #ad6800;
|
||||
@gold-9: #874d00;
|
||||
@gold-10: #613400;
|
||||
@dark-gold-1: #2b2111;
|
||||
@dark-gold-2: #443111;
|
||||
@dark-gold-3: #594214;
|
||||
@dark-gold-4: #7c5914;
|
||||
@dark-gold-5: #aa7714;
|
||||
@dark-gold-6: #d89614;
|
||||
@dark-gold-7: #e8b339;
|
||||
@dark-gold-8: #f3cc62;
|
||||
@dark-gold-9: #f8df8b;
|
||||
@dark-gold-10: #faedb5;
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export const PREFIX = 'idraw-lab';
|
||||
|
||||
export function createPrefixName(modName: string) {
|
||||
return (...args: string[]) => {
|
||||
return [PREFIX, modName, ...args].join('-');
|
||||
};
|
||||
}
|
||||
|
|
@ -1,521 +0,0 @@
|
|||
import type { Data } from '@idraw/types';
|
||||
import { deepClone } from '@idraw/util';
|
||||
|
||||
const data: Data = {
|
||||
elements: [
|
||||
{
|
||||
uuid: 'xxx-0003',
|
||||
type: 'image',
|
||||
x: 100,
|
||||
y: 100,
|
||||
w: 100,
|
||||
h: 100,
|
||||
angle: 30,
|
||||
detail: {
|
||||
src: './images/lena.png'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'xxxx-0001',
|
||||
x: -50,
|
||||
y: -40,
|
||||
w: 100,
|
||||
h: 100,
|
||||
type: 'circle',
|
||||
detail: {
|
||||
background: '#f44336'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'xxx-0002',
|
||||
type: 'rect',
|
||||
x: 50,
|
||||
y: 50,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#2196f3'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'xxx-0004',
|
||||
type: 'image',
|
||||
x: 250,
|
||||
y: 250,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
src: './images/github.png?t=003'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'xxxx-0005',
|
||||
x: 0,
|
||||
y: 300,
|
||||
w: 100,
|
||||
h: 100,
|
||||
type: 'circle',
|
||||
detail: {
|
||||
background: '#009688'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'xxxx-0006',
|
||||
x: 300,
|
||||
y: 300,
|
||||
w: 100,
|
||||
h: 100,
|
||||
type: 'circle',
|
||||
detail: {
|
||||
background: '#673ab7'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'xxxx-0007',
|
||||
x: 300,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
type: 'circle',
|
||||
detail: {
|
||||
background: '#ffc107'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'xxxx-0008',
|
||||
x: 150,
|
||||
y: 150,
|
||||
w: 100,
|
||||
h: 100,
|
||||
type: 'circle',
|
||||
detail: {
|
||||
background: '#4caf50'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'xxxx-0009',
|
||||
x: 0,
|
||||
y: 150,
|
||||
w: 100,
|
||||
h: 100,
|
||||
type: 'circle',
|
||||
detail: {
|
||||
background: '#ff9800'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'xxxx-0010',
|
||||
x: 150,
|
||||
y: 50,
|
||||
w: 100,
|
||||
h: 100,
|
||||
type: 'circle',
|
||||
detail: {
|
||||
background: '#cddc39'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'text-0010',
|
||||
name: 'text-002',
|
||||
x: 300,
|
||||
y: 100,
|
||||
w: 100,
|
||||
h: 60,
|
||||
type: 'text',
|
||||
detail: {
|
||||
fontSize: 16,
|
||||
text: [0, 1, 2, 3, 4].map((i) => `Hello Text ${i}`).join('\r\n'),
|
||||
// text: [0, 1, 2, 3, 4].map(i => `Hello Text ${i}`).join(''),
|
||||
fontWeight: 'bold',
|
||||
color: '#666666',
|
||||
borderRadius: 30,
|
||||
borderWidth: 2,
|
||||
borderColor: '#ff5722'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'xxx-0011',
|
||||
type: 'svg',
|
||||
x: 400,
|
||||
y: 100,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
svg: `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M336 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" ></path><path d="M688 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" ></path><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2-44.3-18.7-84.1-45.6-118.3-79.8-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8c18.7-44.3 45.6-84.1 79.8-118.3 34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2 44.3 18.7 84.1 45.6 118.3 79.8 34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8c-18.7 44.3-45.6 84.1-79.8 118.2z"></path><path d="M664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-0.3-4.2-3.9-7.4-8.1-7.4H360c-4.6 0-8.2 3.8-8 8.4 4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6c0.2-4.6-3.4-8.4-8-8.4z" ></path></svg>`
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'xxx-0012',
|
||||
x: 400,
|
||||
y: 200,
|
||||
w: 150,
|
||||
h: 100,
|
||||
type: 'html',
|
||||
angle: 0,
|
||||
detail: {
|
||||
html: `
|
||||
<style>
|
||||
.btn-box {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.btn {
|
||||
line-height: 1.5715;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
background-image: none;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0 2px #00000004;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
height: 32px;
|
||||
padding: 4px 15px;
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
color: #000000d9;
|
||||
background: #fff;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background: #1890ff;
|
||||
border-color: #1890ff;
|
||||
text-shadow: 0 -1px 0 rgb(0 0 0 / 12%);
|
||||
box-shadow: 0 2px #0000000b;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<div class="btn-box">
|
||||
<button class="btn">
|
||||
<span> Hello Button</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-box">
|
||||
<button class="btn btn-primary">
|
||||
<span>Button Primary</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-001',
|
||||
x: 400,
|
||||
y: 400,
|
||||
w: 100,
|
||||
h: 100,
|
||||
type: 'group',
|
||||
detail: {
|
||||
background: '#1f1f1f',
|
||||
children: [
|
||||
{
|
||||
uuid: 'group-001-0014',
|
||||
type: 'circle',
|
||||
x: -40,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#f44336'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-001-0015',
|
||||
type: 'circle',
|
||||
x: -20,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#ff9800'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-001-0016',
|
||||
type: 'circle',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#ffc106'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-001-0017',
|
||||
type: 'circle',
|
||||
x: 20,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#cddc39'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-001-0018',
|
||||
type: 'circle',
|
||||
x: 40,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#4caf50'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-003',
|
||||
x: 550,
|
||||
y: 50,
|
||||
w: 173.20508075688775,
|
||||
// w: 100,
|
||||
h: 100,
|
||||
angle: 30,
|
||||
type: 'group',
|
||||
detail: {
|
||||
children: [
|
||||
{
|
||||
uuid: 'group-003-014',
|
||||
type: 'circle',
|
||||
x: -40,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#f44336'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-003-0015',
|
||||
type: 'circle',
|
||||
x: -20,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#ff9800'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-003-0016',
|
||||
type: 'circle',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#ffc106'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-003-0017',
|
||||
type: 'circle',
|
||||
x: 20,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#cddc39'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-003-0018',
|
||||
type: 'circle',
|
||||
x: 40,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#4caf50'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'xxxx-0017',
|
||||
type: 'image',
|
||||
x: 100,
|
||||
y: 300,
|
||||
w: 100,
|
||||
h: 100,
|
||||
angle: 30,
|
||||
detail: {
|
||||
src: './images/lena.png?v=0017'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-004',
|
||||
x: 550,
|
||||
y: 250,
|
||||
w: 375,
|
||||
h: 400,
|
||||
type: 'group',
|
||||
detail: {
|
||||
background: '#FFFFFF',
|
||||
children: [
|
||||
{
|
||||
uuid: 'groud-004-001',
|
||||
type: 'image',
|
||||
x: 200,
|
||||
y: 200,
|
||||
w: 100,
|
||||
h: 100,
|
||||
angle: 30,
|
||||
detail: {
|
||||
src: './images/lena.png'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'groud-004-002',
|
||||
type: 'group',
|
||||
x: 50,
|
||||
y: 50,
|
||||
w: 200,
|
||||
h: 200,
|
||||
angle: 30,
|
||||
detail: {
|
||||
background: '#f0f0f0',
|
||||
children: [
|
||||
{
|
||||
uuid: 'group-004-002-014',
|
||||
type: 'circle',
|
||||
x: -40,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#f44336'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-004-001-0015',
|
||||
type: 'circle',
|
||||
x: -20,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#ff9800'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-004-002-0016',
|
||||
type: 'circle',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#ffc106'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-004-002-0017',
|
||||
type: 'circle',
|
||||
x: 20,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#cddc39'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-004-002-0018',
|
||||
type: 'circle',
|
||||
x: 40,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#4caf50'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'groud-004-002-xxxx',
|
||||
type: 'group',
|
||||
x: 50,
|
||||
y: 100,
|
||||
w: 200,
|
||||
h: 100,
|
||||
angle: 30,
|
||||
detail: {
|
||||
background: '#666666',
|
||||
children: [
|
||||
{
|
||||
uuid: 'group-004-002-xxx-014',
|
||||
type: 'circle',
|
||||
x: -40,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#f44336'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-004-002-xxx-0015',
|
||||
type: 'circle',
|
||||
x: -20,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#ff9800'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-004-002-xxx-0016',
|
||||
type: 'circle',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#ffc106'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-004-002-xxx-0017',
|
||||
type: 'circle',
|
||||
x: 20,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#cddc39'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'group-004-002-xxx-0018',
|
||||
type: 'circle',
|
||||
x: 40,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#4caf50'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export function getData() {
|
||||
return deepClone(data);
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { iconClassName } from './util';
|
||||
import type { IconProps } from './util';
|
||||
|
||||
const Dark = (props: IconProps) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={classnames([iconClassName, className])} style={style}>
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor">
|
||||
<path d="M516.266667 938.666667h-38.4c-234.666667-21.333333-405.333333-230.4-384-465.066667 17.066667-204.8 179.2-366.933333 384-384 17.066667 0 34.133333 8.533333 42.666666 21.333333 8.533333 12.8 8.533333 34.133333-4.266666 46.933334-85.333333 115.2-59.733333 273.066667 55.466666 358.4 89.6 68.266667 213.333333 68.266667 302.933334 0 12.8-8.533333 29.866667-12.8 46.933333-4.266667 12.8 8.533333 21.333333 25.6 21.333333 42.666667-8.533333 115.2-64 217.6-153.6 290.133333-81.066667 59.733333-174.933333 93.866667-273.066666 93.866667zM396.8 187.733333c-123.733333 42.666667-213.333333 153.6-221.866667 290.133334-17.066667 187.733333 119.466667 354.133333 307.2 371.2 89.6 8.533333 179.2-17.066667 247.466667-76.8 46.933333-38.4 81.066667-89.6 102.4-145.066667-106.666667 38.4-226.133333 21.333333-320-46.933333-119.466667-93.866667-166.4-251.733333-115.2-392.533334z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dark;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { iconClassName } from './util';
|
||||
import type { IconProps } from './util';
|
||||
|
||||
const Hand = (props: IconProps) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={classnames([iconClassName, className])} style={style}>
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor">
|
||||
<path d="M841.065412 337.317647v352.015059c0 46.802824-23.070118 74.752-39.936 95.111529-14.095059 17.106824-21.443765 26.684235-21.443765 41.803294V933.647059a30.117647 30.117647 0 0 1-60.235294 0v-107.39953c0-37.526588 19.576471-61.199059 35.297882-80.173176 14.576941-17.648941 26.142118-31.563294 26.142118-56.681412V337.317647c0-18.793412-16.143059-36.502588-33.189647-36.502588-19.817412 0-24.033882 3.072-24.214588 3.19247-3.975529 5.360941-3.855059 34.273882-3.794824 57.584942 0.060235 11.384471 0.120471 24.094118-0.12047 38.068705-0.240941 16.504471-12.830118 28.792471-30.358589 29.696a30.117647 30.117647 0 0 1-29.876706-30.117647v-127.698823c0-18.492235-13.372235-32.406588-31.081411-32.406588-16.263529 0-28.190118 12.107294-29.635765 29.394823v120.651294a30.117647 30.117647 0 0 1-60.235294 0V268.047059l-0.120471-0.602353v-46.561882c0-3.975529-0.843294-38.671059-28.551529-38.671059-27.105882 0-31.201882 24.214588-31.201883 38.671059v42.345411c0 1.686588-0.662588 3.252706-0.963764 4.879059v156.250353a30.117647 30.117647 0 0 1-60.235294 0V262.686118c-2.891294-11.685647-11.324235-23.491765-28.069647-23.491765-17.227294 0-31.744 15.721412-31.744 34.334118v201.788235c0 0.421647-0.361412 0.783059-0.361412 1.204706v66.319059a30.117647 30.117647 0 0 1-60.235294 0v-50.236236c-10.601412-3.855059-25.961412-6.987294-34.755765-4.999529-7.107765 1.385412-14.275765 7.649882-18.733176 16.323765a43.309176 43.309176 0 0 0-0.542118 38.369882L345.148235 766.192941a31.563294 31.563294 0 0 1 2.108236 6.505412 32.888471 32.888471 0 0 0 35.418353 25.961412 30.238118 30.238118 0 0 1 33.310117 29.936941V933.647059a30.117647 30.117647 0 0 1-60.235294 0v-77.462588a92.521412 92.521412 0 0 1-66.680471-67.764706L187.934118 567.055059a104.448 104.448 0 0 1 1.927529-90.774588c12.890353-24.877176 35.418353-42.706824 60.295529-47.766589 12.649412-2.590118 29.756235-1.867294 46.682353 1.566118v-43.369412c0-0.602353 0.301176-1.144471 0.361412-1.746823v-111.435294c0-52.163765 41.321412-94.569412 91.979294-94.569412 12.047059 0 23.612235 2.409412 34.093177 6.746353 11.986824-38.791529 44.152471-63.668706 86.317176-63.668706 40.237176 0 71.800471 25.419294 83.666824 63.909647 10.721882-4.517647 22.467765-6.987294 34.876235-6.987294 41.502118 0 75.776 26.744471 87.160471 64.572235 11.023059-2.409412 22.226824-2.951529 32.286117-2.951529 50.718118 0 93.485176 44.272941 93.485177 96.737882z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Hand;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { iconClassName } from './util';
|
||||
import type { IconProps } from './util';
|
||||
|
||||
const Layer = (props: IconProps) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={classnames([iconClassName, className])} style={style}>
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor">
|
||||
<path d="M118.979048 637.074286l137.99619 66.243047 255.171048 123.587048 246.076952-119.222857 147.163429-70.485334a73.142857 73.142857 0 0 1-34.230857 97.109334l-327.119239 158.427428a73.142857 73.142857 0 0 1-63.780571 0L153.136762 734.305524A73.142857 73.142857 0 0 1 118.979048 637.074286z m786.090666-153.063619a73.142857 73.142857 0 0 1-33.913904 97.767619L544.01219 740.205714a73.142857 73.142857 0 0 1-63.780571 0L153.136762 581.778286A73.142857 73.142857 0 0 1 117.51619 487.862857l362.300953 170.886095 32.329143 15.652572 327.119238-158.427429 65.80419-31.939047zM544.036571 139.190857l327.094858 158.403048a73.142857 73.142857 0 0 1 0 131.657143l-327.094858 158.427428a73.142857 73.142857 0 0 1-63.780571 0L153.136762 429.251048a73.142857 73.142857 0 0 1 0-131.657143L480.256 139.215238a73.142857 73.142857 0 0 1 63.780571 0z m-31.890285 65.828572L185.027048 363.422476l327.119238 158.427429 327.119238-158.427429L512.146286 205.04381z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layer;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { iconClassName } from './util';
|
||||
import type { IconProps } from './util';
|
||||
|
||||
const Light = (props: IconProps) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={classnames([iconClassName, className])} style={style}>
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor">
|
||||
<path d="M512 768c-141.376 0-256-114.624-256-256s114.624-256 256-256 256 114.624 256 256-114.624 256-256 256z m0-85.333333a170.666667 170.666667 0 1 0 0-341.333334 170.666667 170.666667 0 0 0 0 341.333334zM469.333333 85.333333a42.666667 42.666667 0 1 1 85.333334 0v85.333334a42.666667 42.666667 0 1 1-85.333334 0V85.333333z m0 768a42.666667 42.666667 0 1 1 85.333334 0v85.333334a42.666667 42.666667 0 1 1-85.333334 0v-85.333334zM85.333333 554.666667a42.666667 42.666667 0 1 1 0-85.333334h85.333334a42.666667 42.666667 0 1 1 0 85.333334H85.333333z m768 0a42.666667 42.666667 0 1 1 0-85.333334h85.333334a42.666667 42.666667 0 1 1 0 85.333334h-85.333334zM161.834667 222.165333a42.666667 42.666667 0 0 1 60.330666-60.330666l64 64a42.666667 42.666667 0 0 1-60.330666 60.330666l-64-64z m576 576a42.666667 42.666667 0 0 1 60.330666-60.330666l64 64a42.666667 42.666667 0 0 1-60.330666 60.330666l-64-64z m-515.669334 64a42.666667 42.666667 0 0 1-60.330666-60.330666l64-64a42.666667 42.666667 0 0 1 60.330666 60.330666l-64 64z m576-576a42.666667 42.666667 0 0 1-60.330666-60.330666l64-64a42.666667 42.666667 0 0 1 60.330666 60.330666l-64 64z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Light;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { iconClassName } from './util';
|
||||
import type { IconProps } from './util';
|
||||
|
||||
const More = (props: IconProps) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={classnames([iconClassName, className])} style={style}>
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor">
|
||||
<path d="M512 42.666667a469.333333 469.333333 0 1 0 469.333333 469.333333A469.333333 469.333333 0 0 0 512 42.666667z m0 864a394.666667 394.666667 0 1 1 394.666667-394.666667 395.146667 395.146667 0 0 1-394.666667 394.666667z"></path>
|
||||
<path d="M304.906667 512m-66.666667 0a66.666667 66.666667 0 1 0 133.333333 0 66.666667 66.666667 0 1 0-133.333333 0Z"></path>
|
||||
<path d="M512 512m-66.666667 0a66.666667 66.666667 0 1 0 133.333334 0 66.666667 66.666667 0 1 0-133.333334 0Z"></path>
|
||||
<path d="M719.093333 512m-66.666666 0a66.666667 66.666667 0 1 0 133.333333 0 66.666667 66.666667 0 1 0-133.333333 0Z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default More;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { iconClassName } from './util';
|
||||
import type { IconProps } from './util';
|
||||
|
||||
const Mouse = (props: IconProps) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={classnames([iconClassName, className])} style={style}>
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor">
|
||||
<path d="M570.3 939.6c-11.7 0-23-6.5-28.6-17.7l-95.2-190.1-137 102.7c-9.7 7.3-22.6 8.4-33.5 3-10.8-5.4-17.7-16.4-17.7-28.5l-2.7-672.9c-0.1-12.7 7.4-24.2 19-29.4 11.6-5.2 25.1-3 34.5 5.5l496.4 449.2c9 8.1 12.6 20.6 9.4 32.2-3.2 11.7-12.7 20.5-24.6 22.9l-165.5 33.1L717.2 834c3.8 7.6 4.4 16.4 1.8 24.4-2.7 8.1-8.5 14.7-16 18.5l-118.3 59.2c-4.7 2.5-9.6 3.5-14.4 3.5zM457.8 651.3c2.4 0 4.9 0.3 7.3 0.9 9.2 2.2 17 8.3 21.3 16.8l98.1 195.8 61.1-30.6-96.8-193.3c-4.5-8.9-4.5-19.4-0.1-28.4s12.7-15.4 22.5-17.3l144.3-28.9-395.6-357.9 2.1 536.7 116.6-87.4c5.6-4.2 12.4-6.4 19.2-6.4z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mouse;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { iconClassName } from './util';
|
||||
import type { IconProps } from './util';
|
||||
|
||||
const Pen = (props: IconProps) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={classnames([iconClassName, className])} style={style}>
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor">
|
||||
<path d="M465.454545 510.138182a46.545455 46.545455 0 1 0 0 93.090909 46.545455 46.545455 0 0 0 0-93.090909z m-108.60606 46.545454a108.606061 108.606061 0 1 1 217.212121 0 108.606061 108.606061 0 0 1-217.212121 0z"></path>
|
||||
<path d="M432.531394 589.575758a31.030303 31.030303 0 0 1 0 43.907878l-270.925576 270.956606a31.030303 31.030303 0 1 1-43.907879-43.876848L388.654545 589.575758a31.030303 31.030303 0 0 1 43.876849 0z"></path>
|
||||
<path d="M470.109091 201.821091a31.030303 31.030303 0 0 1 32.830061 7.13697l310.30303 310.30303a31.030303 31.030303 0 0 1 7.105939 32.79903l-86.791757 231.486061a62.060606 62.060606 0 0 1-47.910788 39.408485L144.756364 913.128727a31.030303 31.030303 0 0 1-35.684849-35.715879l90.112-540.858181A62.060606 62.060606 0 0 1 238.62303 288.581818l231.486061-86.791757z m3.072 65.132606l-212.774788 79.747879-10.891636-29.013334 10.891636 29.044364-83.006061 498.036364 498.036364-83.006061 79.778909-212.774788-282.065454-282.065454z"></path>
|
||||
<path d="M561.214061 106.744242a62.060606 62.060606 0 0 1 87.784727 0l-21.938424 21.938425 21.938424-21.938425 266.395151 266.426182a62.060606 62.060606 0 0 1 0 87.784728l-102.151757 102.151757a31.030303 31.030303 0 0 1-43.907879-43.876848l102.182788-102.182788-266.395152-266.426182-102.182787 102.182788a31.030303 31.030303 0 1 1-43.907879-43.876849l102.182788-102.182788 21.938424 21.938425-21.938424-21.938425z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pen;
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { iconClassName } from './util';
|
||||
import type { IconProps } from './util';
|
||||
|
||||
const Mouse = (props: IconProps) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={classnames([iconClassName, className])} style={style}>
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor">
|
||||
<path d="M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z"></path>
|
||||
<path d="M921 867L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mouse;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { iconClassName } from './util';
|
||||
import type { IconProps } from './util';
|
||||
|
||||
const Setting = (props: IconProps) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<span className={classnames([iconClassName, className])} style={style}>
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor">
|
||||
<path d="M725.333333 341.333333a128 128 0 1 1 128-128 128 128 0 0 1-128 128z m0-170.666666a42.666667 42.666667 0 1 0 42.666667 42.666666 42.666667 42.666667 0 0 0-42.666667-42.666666z"></path>
|
||||
<path d="M640 256H85.333333a42.666667 42.666667 0 0 1 0-85.333333h554.666667a42.666667 42.666667 0 0 1 0 85.333333zM938.666667 256h-128a42.666667 42.666667 0 0 1 0-85.333333h128a42.666667 42.666667 0 0 1 0 85.333333zM512 640a128 128 0 1 1 128-128 128 128 0 0 1-128 128z m0-170.666667a42.666667 42.666667 0 1 0 42.666667 42.666667 42.666667 42.666667 0 0 0-42.666667-42.666667z"></path>
|
||||
<path d="M426.666667 554.666667H85.333333a42.666667 42.666667 0 0 1 0-85.333334h341.333334a42.666667 42.666667 0 0 1 0 85.333334zM938.666667 554.666667h-341.333334a42.666667 42.666667 0 0 1 0-85.333334h341.333334a42.666667 42.666667 0 0 1 0 85.333334zM298.666667 938.666667a128 128 0 1 1 128-128 128 128 0 0 1-128 128z m0-170.666667a42.666667 42.666667 0 1 0 42.666666 42.666667 42.666667 42.666667 0 0 0-42.666666-42.666667z"></path>
|
||||
<path d="M938.666667 853.333333H384a42.666667 42.666667 0 0 1 0-85.333333h554.666667a42.666667 42.666667 0 0 1 0 85.333333zM213.333333 853.333333H85.333333a42.666667 42.666667 0 0 1 0-85.333333h128a42.666667 42.666667 0 0 1 0 85.333333z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default Setting;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import type { CSSProperties } from 'react';
|
||||
import { createPrefixName } from '../../css';
|
||||
|
||||
const modName = 'icon';
|
||||
|
||||
const prefixName = createPrefixName(modName);
|
||||
|
||||
export const iconClassName = prefixName();
|
||||
|
||||
export interface IconProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import React, { useEffect, useReducer } from 'react';
|
||||
import ConfigProvider from 'antd/es/config-provider';
|
||||
import theme from 'antd/es/theme';
|
||||
import classnames from 'classnames';
|
||||
import { Dashboard } from './modules';
|
||||
import { createPrefixName } from './css';
|
||||
import { Provider, createLabContextState, createLabReducer } from './context';
|
||||
import type { LabData } from './types';
|
||||
import type { DashboardProps } from './modules';
|
||||
import './css/index.less';
|
||||
|
||||
const themeName = 'theme';
|
||||
const themePrefixName = createPrefixName(themeName);
|
||||
|
||||
export type LabProps = DashboardProps & {
|
||||
labData?: LabData;
|
||||
locale?: string; // TODO
|
||||
themeMode?: 'light' | 'dark';
|
||||
};
|
||||
|
||||
export const Lab = (props: LabProps) => {
|
||||
const { width = 1000, height = 600, style, className, labData, themeMode } = props;
|
||||
|
||||
const [state, dispatch] = useReducer(createLabReducer, createLabContextState({ labData, themeMode }));
|
||||
|
||||
useEffect(() => {
|
||||
if (labData) {
|
||||
dispatch({
|
||||
type: 'updateLabData',
|
||||
payload: { labData }
|
||||
});
|
||||
}
|
||||
}, [labData]);
|
||||
|
||||
return (
|
||||
<Provider value={{ state, dispatch }}>
|
||||
<ConfigProvider theme={{ algorithm: state.themeMode === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm }}>
|
||||
<Dashboard
|
||||
width={width}
|
||||
height={height}
|
||||
style={style}
|
||||
className={classnames([themePrefixName(), state?.themeMode === 'dark' ? themePrefixName('dark') : '', className])}
|
||||
/>
|
||||
</ConfigProvider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export * from './types';
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import React from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { createPrefixName } from '../../css';
|
||||
|
||||
const modName = 'mod-xxx';
|
||||
|
||||
const prefixName = createPrefixName(modName);
|
||||
|
||||
export interface ModProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const Mod = (props: ModProps) => {
|
||||
const { className, style } = props;
|
||||
return (
|
||||
<div style={style} className={classnames(prefixName(), className)}>
|
||||
Mod
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Toolbar } from '../toolbar';
|
||||
import { PanelLayer } from '../panel-layer';
|
||||
import { Header } from '../header';
|
||||
import { Sketch } from '../sketch';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { createPrefixName } from '../../css';
|
||||
import SplitPane from '../split-pane';
|
||||
|
||||
const modName = 'mod-dashboard';
|
||||
const leftSiderDefaultWidth = 240;
|
||||
const rightSiderDefaultWidth = 200;
|
||||
const headerHeight = 36;
|
||||
|
||||
const prefixName = createPrefixName(modName);
|
||||
|
||||
export interface DashboardProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const Dashboard = (props: DashboardProps) => {
|
||||
const { className, style, width, height } = props;
|
||||
|
||||
const [openLeftSider, setOpenLeftSider] = useState<boolean>(true);
|
||||
const [openRightSider, setOpenRightSider] = useState<boolean>(false);
|
||||
|
||||
const [leftWidth, setLeftWidth] = useState<number>(openLeftSider ? leftSiderDefaultWidth : 0);
|
||||
const [rightWidth, setRightWidth] = useState<number>(openRightSider ? rightSiderDefaultWidth : 0);
|
||||
const [centerWidth, setCenterWidth] = useState<number>(width - leftWidth - rightWidth);
|
||||
|
||||
useEffect(() => {
|
||||
const prevWidth = leftWidth + centerWidth + rightWidth;
|
||||
let newLeftWidth = Math.floor(width * (leftWidth / prevWidth));
|
||||
let newRightWidth = Math.floor(width * (rightWidth / prevWidth));
|
||||
|
||||
newLeftWidth = Math.min(newLeftWidth, leftSiderDefaultWidth);
|
||||
newRightWidth = Math.min(newRightWidth, rightSiderDefaultWidth);
|
||||
|
||||
const newCenterWidth = width - newLeftWidth - newRightWidth;
|
||||
setLeftWidth(newLeftWidth);
|
||||
setRightWidth(newRightWidth);
|
||||
setCenterWidth(newCenterWidth);
|
||||
}, [height, width]);
|
||||
|
||||
return (
|
||||
<div className={classnames(prefixName(), className)} style={{ ...style, ...{ width, height, padding: 0 } }}>
|
||||
<div className={prefixName('header')} style={{ height: headerHeight }}>
|
||||
<Header
|
||||
openLeftSider={openLeftSider}
|
||||
openRightSider={openRightSider}
|
||||
onClickToggleLayer={() => {
|
||||
const open = openLeftSider ? false : true;
|
||||
|
||||
let newLeftWidth = leftWidth;
|
||||
if (open) {
|
||||
newLeftWidth = leftSiderDefaultWidth;
|
||||
} else {
|
||||
newLeftWidth = 0;
|
||||
}
|
||||
setLeftWidth(newLeftWidth);
|
||||
setCenterWidth(width - newLeftWidth - rightWidth);
|
||||
setRightWidth(rightWidth);
|
||||
setOpenLeftSider(open);
|
||||
}}
|
||||
onClickToggleSetting={() => {
|
||||
const open = openRightSider ? false : true;
|
||||
let newRightWidth = rightWidth;
|
||||
if (open) {
|
||||
newRightWidth = rightSiderDefaultWidth;
|
||||
} else {
|
||||
newRightWidth = 0;
|
||||
}
|
||||
setLeftWidth(leftWidth);
|
||||
setCenterWidth(width - leftWidth - newRightWidth);
|
||||
setRightWidth(newRightWidth);
|
||||
setOpenRightSider(open);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={prefixName('content')} style={{ top: headerHeight }}>
|
||||
<SplitPane
|
||||
split="vertical"
|
||||
defaultSize={centerWidth + rightWidth}
|
||||
allowResize
|
||||
onChange={(px: number) => {
|
||||
setCenterWidth(px - rightWidth);
|
||||
setLeftWidth(width - px);
|
||||
}}
|
||||
pane1Style={{
|
||||
width: leftWidth
|
||||
}}
|
||||
pane2Style={{
|
||||
width: centerWidth + rightWidth
|
||||
}}
|
||||
>
|
||||
<div>{openLeftSider && <PanelLayer className={prefixName('left')} />}</div>
|
||||
<div style={{ width: centerWidth + rightWidth, display: 'flex', flexDirection: 'row' }}>
|
||||
<Sketch className={prefixName('center')} width={centerWidth} height={height - headerHeight} />
|
||||
<div className={prefixName('right')} style={{ width: rightWidth, height: height - headerHeight }}>
|
||||
Right
|
||||
</div>
|
||||
</div>
|
||||
</SplitPane>
|
||||
</div>
|
||||
{/* <Toolbar
|
||||
className={prefixName('toolbar-position')}
|
||||
openLeftSider={openLeftSider}
|
||||
openRightSider={openRightSider}
|
||||
onClickToggleLayer={() => {
|
||||
const open = openLeftSider ? false : true;
|
||||
|
||||
let newLeftWidth = leftWidth;
|
||||
if (open) {
|
||||
newLeftWidth = leftSiderDefaultWidth;
|
||||
} else {
|
||||
newLeftWidth = 0;
|
||||
}
|
||||
setLeftWidth(newLeftWidth);
|
||||
setCenterWidth(width - newLeftWidth - rightWidth);
|
||||
setRightWidth(rightWidth);
|
||||
setOpenLeftSider(open);
|
||||
}}
|
||||
onClickToggleSetting={() => {
|
||||
const open = openRightSider ? false : true;
|
||||
let newRightWidth = rightWidth;
|
||||
if (open) {
|
||||
newRightWidth = rightSiderDefaultWidth;
|
||||
} else {
|
||||
newRightWidth = 0;
|
||||
}
|
||||
setLeftWidth(leftWidth);
|
||||
setCenterWidth(width - leftWidth - newRightWidth);
|
||||
setRightWidth(newRightWidth);
|
||||
setOpenRightSider(open);
|
||||
}}
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Switch from 'antd/es/switch';
|
||||
import { createPrefixName } from '../../css';
|
||||
import IconDark from '../../icons/dark';
|
||||
import IconLight from '../../icons/light';
|
||||
import { Context } from '../../context';
|
||||
import { Toolbar } from '../toolbar';
|
||||
import type { ToolbarProps } from '../toolbar';
|
||||
|
||||
const modName = 'mod-header';
|
||||
|
||||
const prefixName = createPrefixName(modName);
|
||||
|
||||
export interface ModProps extends ToolbarProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const Header = (props: ModProps) => {
|
||||
const { className, style, openLeftSider, openRightSider, onClickToggleLayer, onClickToggleSetting } = props;
|
||||
const { state, dispatch } = useContext(Context);
|
||||
|
||||
return (
|
||||
<div style={style} className={classnames(prefixName(), className)}>
|
||||
<span>@idraw/lab</span>
|
||||
<Toolbar
|
||||
openLeftSider={openLeftSider}
|
||||
openRightSider={openRightSider}
|
||||
onClickToggleLayer={onClickToggleLayer}
|
||||
onClickToggleSetting={onClickToggleSetting}
|
||||
/>
|
||||
<Switch
|
||||
className={prefixName('theme', 'switch')}
|
||||
checkedChildren={<IconLight style={{ height: '100%' }} />}
|
||||
unCheckedChildren={<IconDark style={{ height: '100%' }} />}
|
||||
checked={state?.themeMode === 'light'}
|
||||
onChange={(checked: boolean) => {
|
||||
dispatch?.({
|
||||
type: 'updateThemeMode',
|
||||
payload: {
|
||||
themeMode: checked ? 'light' : 'dark'
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
export { Toolbar } from './toolbar/index';
|
||||
export type { ToolbarProps } from './toolbar/index';
|
||||
|
||||
export { Dashboard } from './dashboard/index';
|
||||
export type { DashboardProps } from './dashboard/index';
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
import { createPrefixName } from '../../css';
|
||||
|
||||
const modName = 'mod-panel-layer';
|
||||
export const prefixName = createPrefixName(modName);
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Tabs from 'antd/es/tabs';
|
||||
import type { TabsProps } from 'antd';
|
||||
import FileOutlined from '@ant-design/icons/FileOutlined';
|
||||
import AppstoreOutlined from '@ant-design/icons/AppstoreOutlined';
|
||||
import CalculatorOutlined from '@ant-design/icons/CalculatorOutlined';
|
||||
import { prefixName } from './config';
|
||||
import { LayerTree } from './layer-tree';
|
||||
import { Context } from '../../context';
|
||||
import { LabDrawDataType } from '../../types';
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
key: 'page',
|
||||
label: <FileOutlined className={prefixName('tab', 'title')} />
|
||||
},
|
||||
{
|
||||
key: 'module',
|
||||
label: <AppstoreOutlined className={prefixName('tab', 'title')} />
|
||||
},
|
||||
{
|
||||
key: 'component',
|
||||
label: <CalculatorOutlined className={prefixName('tab', 'title')} />
|
||||
}
|
||||
];
|
||||
|
||||
export interface PanelLayerProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const PanelLayer = (props: PanelLayerProps) => {
|
||||
const { className, style } = props;
|
||||
const { state, dispatch } = useContext(Context);
|
||||
|
||||
return (
|
||||
<div style={style} className={classnames(prefixName(), className)}>
|
||||
<div className={prefixName('header')}>
|
||||
<Tabs
|
||||
className={prefixName('tabs')}
|
||||
tabBarStyle={{ marginBottom: 0 }}
|
||||
activeKey={state?.activeDrawDataType as string}
|
||||
centered
|
||||
items={items}
|
||||
size="small"
|
||||
onTabClick={(activeKey: string) => {
|
||||
dispatch({ type: 'switchDrawDataType', payload: { activeDrawDataType: activeKey as LabDrawDataType } });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={prefixName('content')}>
|
||||
<LayerTree type={state.activeDrawDataType} />
|
||||
</div>
|
||||
<div className={prefixName('footer')}>footer</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import React, { useEffect, useContext } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Tree from 'antd/es/tree';
|
||||
import DownOutlined from '@ant-design/icons/DownOutlined';
|
||||
import { prefixName } from './config';
|
||||
import { Context } from '../../context';
|
||||
import { parseComponentViewTree } from '../../util/component';
|
||||
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { DataNode, TreeProps } from 'antd/es/tree';
|
||||
import type { LabDrawDataType } from '../../types';
|
||||
|
||||
const { DirectoryTree } = Tree;
|
||||
const baseName = 'layer-tree';
|
||||
|
||||
export interface LayerTreeProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
type: LabDrawDataType;
|
||||
}
|
||||
|
||||
export const LayerTree = (props: LayerTreeProps) => {
|
||||
const { className, style, type } = props;
|
||||
const { state } = useContext(Context);
|
||||
|
||||
const onSelect: TreeProps['onSelect'] = (selectedKeys, info) => {
|
||||
// TODO
|
||||
console.log('selected', selectedKeys, info);
|
||||
};
|
||||
|
||||
let treeData: DataNode[] = [];
|
||||
if (type === 'component') {
|
||||
treeData = parseComponentViewTree(state?.labData || null);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={style} className={classnames(prefixName(baseName), className)}>
|
||||
<DirectoryTree showLine blockNode switcherIcon={<DownOutlined />} icon={null} onSelect={onSelect} treeData={treeData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import React, { useEffect, useRef, useContext } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Core, MiddlewareScroller, MiddlewareSelector, MiddlewareScaler } from '@idraw/core';
|
||||
import { calcElementsContextSize } from '@idraw/util';
|
||||
import { createPrefixName } from '../../css';
|
||||
import { Context } from '../../context';
|
||||
import type { CSSProperties } from 'react';
|
||||
|
||||
const modName = 'mod-sketch';
|
||||
|
||||
const prefixName = createPrefixName(modName);
|
||||
|
||||
export interface DashboardProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const Sketch = (props: DashboardProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const refCore = useRef<Core | null>(null);
|
||||
const { className, style, width, height } = props;
|
||||
const devicePixelRatio = window.devicePixelRatio;
|
||||
|
||||
const { state } = useContext(Context);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref?.current) {
|
||||
if (!refCore?.current) {
|
||||
const options = {
|
||||
width,
|
||||
height,
|
||||
devicePixelRatio
|
||||
};
|
||||
const core = new Core(ref.current, options);
|
||||
core.use(MiddlewareScroller);
|
||||
core.use(MiddlewareSelector);
|
||||
core.use(MiddlewareScaler);
|
||||
refCore.current = core;
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!refCore?.current || !state.viewDrawData) {
|
||||
return;
|
||||
}
|
||||
const core = refCore.current;
|
||||
const contextSize = calcElementsContextSize(state.viewDrawData.elements, { viewWidth: width, viewHeight: height, extend: true });
|
||||
core.resize({
|
||||
width,
|
||||
height,
|
||||
devicePixelRatio,
|
||||
...contextSize
|
||||
});
|
||||
core.setData(state.viewDrawData);
|
||||
}, [state.viewDrawData, height, width]);
|
||||
|
||||
return <div ref={ref} className={classnames(prefixName(), className)} style={{ ...style, ...{ width, height, padding: 0 } }}></div>;
|
||||
};
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// Thanks to: https://github.com/tomkp/react-split-pane/blob/master/src/index.js
|
||||
import SplitPane from './split-pane';
|
||||
import Pane from './pane';
|
||||
|
||||
export default SplitPane;
|
||||
export { Pane };
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
// Thanks to: https://github.com/tomkp/react-split-pane/blob/master/src/Pane.js
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
|
||||
class Pane extends React.PureComponent {
|
||||
render() {
|
||||
const { children, className, split, style: styleProps, size, eleRef } = this.props;
|
||||
|
||||
const classes = ['Pane', split, className];
|
||||
|
||||
let style = {
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
outline: 'none'
|
||||
};
|
||||
|
||||
if (size !== undefined) {
|
||||
if (split === 'vertical') {
|
||||
style.width = size;
|
||||
} else {
|
||||
style.height = size;
|
||||
style.display = 'flex';
|
||||
}
|
||||
style.flex = 'none';
|
||||
}
|
||||
|
||||
style = Object.assign({}, style, styleProps || {});
|
||||
|
||||
return (
|
||||
<div ref={eleRef} className={classes.join(' ')} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Pane.propTypes = {
|
||||
// className: PropTypes.string.isRequired,
|
||||
// children: PropTypes.node.isRequired,
|
||||
// size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
// split: PropTypes.oneOf(['vertical', 'horizontal']),
|
||||
// style: stylePropType,
|
||||
// eleRef: PropTypes.func,
|
||||
// };
|
||||
|
||||
// Pane.defaultProps = {};
|
||||
|
||||
export default Pane;
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
// Thanks to: https://github.com/tomkp/react-split-pane/blob/master/src/Resizer.js
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { createPrefixName } from '../../css';
|
||||
|
||||
const modName = 'mod-split-pane';
|
||||
|
||||
const prefixName = createPrefixName(modName);
|
||||
|
||||
export const RESIZER_DEFAULT_CLASSNAME = prefixName();
|
||||
|
||||
class Resizer extends React.Component {
|
||||
render() {
|
||||
const { className, onClick, onDoubleClick, onMouseDown, onTouchEnd, onTouchStart, resizerClassName = RESIZER_DEFAULT_CLASSNAME, split, style } = this.props;
|
||||
const classes = [resizerClassName, split, className];
|
||||
|
||||
return (
|
||||
<span
|
||||
role="presentation"
|
||||
className={classes.join(' ')}
|
||||
style={style}
|
||||
onMouseDown={(event) => onMouseDown(event)}
|
||||
onTouchStart={(event) => {
|
||||
event.preventDefault();
|
||||
onTouchStart(event);
|
||||
}}
|
||||
onTouchEnd={(event) => {
|
||||
event.preventDefault();
|
||||
onTouchEnd(event);
|
||||
}}
|
||||
onClick={(event) => {
|
||||
if (onClick) {
|
||||
event.preventDefault();
|
||||
onClick(event);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={(event) => {
|
||||
if (onDoubleClick) {
|
||||
event.preventDefault();
|
||||
onDoubleClick(event);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Resizer.propTypes = {
|
||||
// className: PropTypes.string.isRequired,
|
||||
// onClick: PropTypes.func,
|
||||
// onDoubleClick: PropTypes.func,
|
||||
// onMouseDown: PropTypes.func.isRequired,
|
||||
// onTouchStart: PropTypes.func.isRequired,
|
||||
// onTouchEnd: PropTypes.func.isRequired,
|
||||
// split: PropTypes.oneOf(['vertical', 'horizontal']),
|
||||
// style: stylePropType,
|
||||
// resizerClassName: PropTypes.string.isRequired,
|
||||
// };
|
||||
// Resizer.defaultProps = {
|
||||
// resizerClassName: RESIZER_DEFAULT_CLASSNAME,
|
||||
// };
|
||||
|
||||
export default Resizer;
|
||||
|
|
@ -1,374 +0,0 @@
|
|||
// Thanks to: https://github.com/tomkp/react-split-pane/blob/master/src/SplitPane.js
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
// import PropTypes from 'prop-types';
|
||||
// import stylePropType from 'react-style-proptype';
|
||||
// import { polyfill } from 'react-lifecycles-compat';
|
||||
|
||||
import Pane from './pane';
|
||||
import Resizer, { RESIZER_DEFAULT_CLASSNAME } from './resizer';
|
||||
|
||||
function unFocus(document, window) {
|
||||
if (document.selection) {
|
||||
document.selection.empty();
|
||||
} else {
|
||||
try {
|
||||
window.getSelection().removeAllRanges();
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultSize(defaultSize, minSize, maxSize, draggedSize) {
|
||||
if (typeof draggedSize === 'number') {
|
||||
const min = typeof minSize === 'number' ? minSize : 0;
|
||||
const max = typeof maxSize === 'number' && maxSize >= 0 ? maxSize : Infinity;
|
||||
return Math.max(min, Math.min(max, draggedSize));
|
||||
}
|
||||
if (defaultSize !== undefined) {
|
||||
return defaultSize;
|
||||
}
|
||||
return minSize;
|
||||
}
|
||||
|
||||
function removeNullChildren(children) {
|
||||
return React.Children.toArray(children).filter((c) => c);
|
||||
}
|
||||
class SplitPane extends React.Component<any, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onMouseDown = this.onMouseDown.bind(this);
|
||||
this.onTouchStart = this.onTouchStart.bind(this);
|
||||
this.onMouseMove = this.onMouseMove.bind(this);
|
||||
this.onTouchMove = this.onTouchMove.bind(this);
|
||||
this.onMouseUp = this.onMouseUp.bind(this);
|
||||
|
||||
// order of setting panel sizes.
|
||||
// 1. size
|
||||
// 2. getDefaultSize(defaultSize, minsize, maxSize)
|
||||
|
||||
const { size, defaultSize, minSize, maxSize, primary } = props;
|
||||
|
||||
const initialSize = size !== undefined ? size : getDefaultSize(defaultSize, minSize, maxSize, null);
|
||||
|
||||
this.state = {
|
||||
active: false,
|
||||
resized: false,
|
||||
pane1Size: primary === 'first' ? initialSize : undefined,
|
||||
pane2Size: primary === 'second' ? initialSize : undefined,
|
||||
|
||||
// these are props that are needed in static functions. ie: gDSFP
|
||||
instanceProps: {
|
||||
size
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('mouseup', this.onMouseUp);
|
||||
document.addEventListener('mousemove', this.onMouseMove);
|
||||
document.addEventListener('touchmove', this.onTouchMove);
|
||||
this.setState(SplitPane.getSizeUpdate(this.props, this.state));
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
return SplitPane.getSizeUpdate(nextProps, prevState);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('mouseup', this.onMouseUp);
|
||||
document.removeEventListener('mousemove', this.onMouseMove);
|
||||
document.removeEventListener('touchmove', this.onTouchMove);
|
||||
}
|
||||
|
||||
onMouseDown(event) {
|
||||
const eventWithTouches = Object.assign({}, event, {
|
||||
touches: [{ clientX: event.clientX, clientY: event.clientY }]
|
||||
});
|
||||
this.onTouchStart(eventWithTouches);
|
||||
}
|
||||
|
||||
onTouchStart(event) {
|
||||
const { allowResize, onDragStarted, split } = this.props;
|
||||
if (allowResize) {
|
||||
unFocus(document, window);
|
||||
const position = split === 'vertical' ? event.touches[0].clientX : event.touches[0].clientY;
|
||||
|
||||
if (typeof onDragStarted === 'function') {
|
||||
onDragStarted();
|
||||
}
|
||||
this.setState({
|
||||
active: true,
|
||||
position
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMouseMove(event) {
|
||||
const eventWithTouches = Object.assign({}, event, {
|
||||
touches: [{ clientX: event.clientX, clientY: event.clientY }]
|
||||
});
|
||||
this.onTouchMove(eventWithTouches);
|
||||
}
|
||||
|
||||
onTouchMove(event) {
|
||||
const { allowResize, maxSize, minSize, onChange, split, step } = this.props;
|
||||
const { active, position } = this.state;
|
||||
|
||||
if (allowResize && active) {
|
||||
unFocus(document, window);
|
||||
const isPrimaryFirst = this.props.primary === 'first';
|
||||
const ref = isPrimaryFirst ? this.pane1 : this.pane2;
|
||||
const ref2 = isPrimaryFirst ? this.pane2 : this.pane1;
|
||||
if (ref) {
|
||||
const node = ref;
|
||||
const node2 = ref2;
|
||||
|
||||
if (node.getBoundingClientRect) {
|
||||
const width = node.getBoundingClientRect().width;
|
||||
const height = node.getBoundingClientRect().height;
|
||||
const current = split === 'vertical' ? event.touches[0].clientX : event.touches[0].clientY;
|
||||
const size = split === 'vertical' ? width : height;
|
||||
let positionDelta = position - current;
|
||||
if (step) {
|
||||
if (Math.abs(positionDelta) < step) {
|
||||
return;
|
||||
}
|
||||
// Integer division
|
||||
// eslint-disable-next-line no-bitwise
|
||||
positionDelta = ~~(positionDelta / step) * step;
|
||||
}
|
||||
let sizeDelta = isPrimaryFirst ? positionDelta : -positionDelta;
|
||||
|
||||
const pane1Order = parseInt(window.getComputedStyle(node).order);
|
||||
const pane2Order = parseInt(window.getComputedStyle(node2).order);
|
||||
if (pane1Order > pane2Order) {
|
||||
sizeDelta = -sizeDelta;
|
||||
}
|
||||
|
||||
let newMaxSize = maxSize;
|
||||
if (maxSize !== undefined && maxSize <= 0) {
|
||||
const splitPane = this.splitPane;
|
||||
if (split === 'vertical') {
|
||||
newMaxSize = splitPane.getBoundingClientRect().width + maxSize;
|
||||
} else {
|
||||
newMaxSize = splitPane.getBoundingClientRect().height + maxSize;
|
||||
}
|
||||
}
|
||||
|
||||
let newSize = size - sizeDelta;
|
||||
const newPosition = position - positionDelta;
|
||||
|
||||
if (newSize < minSize) {
|
||||
newSize = minSize;
|
||||
} else if (maxSize !== undefined && newSize > newMaxSize) {
|
||||
newSize = newMaxSize;
|
||||
} else {
|
||||
this.setState({
|
||||
position: newPosition,
|
||||
resized: true
|
||||
});
|
||||
}
|
||||
|
||||
if (onChange) onChange(newSize);
|
||||
|
||||
this.setState({
|
||||
draggedSize: newSize,
|
||||
[isPrimaryFirst ? 'pane1Size' : 'pane2Size']: newSize
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
const { allowResize, onDragFinished } = this.props;
|
||||
const { active, draggedSize } = this.state;
|
||||
if (allowResize && active) {
|
||||
if (typeof onDragFinished === 'function') {
|
||||
onDragFinished(draggedSize);
|
||||
}
|
||||
this.setState({ active: false });
|
||||
}
|
||||
}
|
||||
|
||||
// we have to check values since gDSFP is called on every render and more in StrictMode
|
||||
static getSizeUpdate(props, state) {
|
||||
const newState = {};
|
||||
const { instanceProps } = state;
|
||||
|
||||
if (instanceProps.size === props.size && props.size !== undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const newSize = props.size !== undefined ? props.size : getDefaultSize(props.defaultSize, props.minSize, props.maxSize, state.draggedSize);
|
||||
|
||||
if (props.size !== undefined) {
|
||||
newState.draggedSize = newSize;
|
||||
}
|
||||
|
||||
const isPanel1Primary = props.primary === 'first';
|
||||
|
||||
newState[isPanel1Primary ? 'pane1Size' : 'pane2Size'] = newSize;
|
||||
newState[isPanel1Primary ? 'pane2Size' : 'pane1Size'] = undefined;
|
||||
|
||||
newState.instanceProps = { size: props.size };
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
allowResize,
|
||||
children,
|
||||
className,
|
||||
onResizerClick,
|
||||
onResizerDoubleClick,
|
||||
paneClassName,
|
||||
pane1ClassName,
|
||||
pane2ClassName,
|
||||
paneStyle,
|
||||
pane1Style: pane1StyleProps,
|
||||
pane2Style: pane2StyleProps,
|
||||
resizerClassName,
|
||||
resizerStyle,
|
||||
split,
|
||||
style: styleProps
|
||||
} = this.props;
|
||||
|
||||
const { pane1Size, pane2Size } = this.state;
|
||||
|
||||
const disabledClass = allowResize ? '' : 'disabled';
|
||||
const resizerClassNamesIncludingDefault = resizerClassName ? `${resizerClassName} ${RESIZER_DEFAULT_CLASSNAME}` : resizerClassName;
|
||||
|
||||
const notNullChildren = removeNullChildren(children);
|
||||
|
||||
const style = {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
outline: 'none',
|
||||
overflow: 'hidden',
|
||||
MozUserSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
msUserSelect: 'text',
|
||||
userSelect: 'text',
|
||||
...styleProps
|
||||
};
|
||||
|
||||
if (split === 'vertical') {
|
||||
Object.assign(style, {
|
||||
flexDirection: 'row',
|
||||
left: 0,
|
||||
right: 0
|
||||
});
|
||||
} else {
|
||||
Object.assign(style, {
|
||||
bottom: 0,
|
||||
flexDirection: 'column',
|
||||
minHeight: '100%',
|
||||
top: 0,
|
||||
width: '100%'
|
||||
});
|
||||
}
|
||||
|
||||
const classes = ['SplitPane', className, split, disabledClass];
|
||||
|
||||
const pane1Style = { ...paneStyle, ...pane1StyleProps };
|
||||
const pane2Style = { ...paneStyle, ...pane2StyleProps };
|
||||
|
||||
const pane1Classes = ['Pane1', paneClassName, pane1ClassName].join(' ');
|
||||
const pane2Classes = ['Pane2', paneClassName, pane2ClassName].join(' ');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes.join(' ')}
|
||||
ref={(node) => {
|
||||
this.splitPane = node;
|
||||
}}
|
||||
style={style}
|
||||
>
|
||||
<Pane
|
||||
className={pane1Classes}
|
||||
key="pane1"
|
||||
eleRef={(node) => {
|
||||
this.pane1 = node;
|
||||
}}
|
||||
size={pane1Size}
|
||||
split={split}
|
||||
style={pane1Style}
|
||||
>
|
||||
{notNullChildren[0]}
|
||||
</Pane>
|
||||
<Resizer
|
||||
className={disabledClass}
|
||||
onClick={onResizerClick}
|
||||
onDoubleClick={onResizerDoubleClick}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onTouchStart={this.onTouchStart}
|
||||
onTouchEnd={this.onMouseUp}
|
||||
key="resizer"
|
||||
resizerClassName={resizerClassNamesIncludingDefault}
|
||||
split={split}
|
||||
style={resizerStyle || {}}
|
||||
/>
|
||||
<Pane
|
||||
className={pane2Classes}
|
||||
key="pane2"
|
||||
eleRef={(node) => {
|
||||
this.pane2 = node;
|
||||
}}
|
||||
size={pane2Size}
|
||||
split={split}
|
||||
style={pane2Style}
|
||||
>
|
||||
{notNullChildren[1]}
|
||||
</Pane>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// SplitPane.propTypes = {
|
||||
// allowResize: PropTypes.bool,
|
||||
// children: PropTypes.arrayOf(PropTypes.node).isRequired,
|
||||
// className: PropTypes.string,
|
||||
// primary: PropTypes.oneOf(['first', 'second']),
|
||||
// minSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
// maxSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
// defaultSize: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
// size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
// split: PropTypes.oneOf(['vertical', 'horizontal']),
|
||||
// onDragStarted: PropTypes.func,
|
||||
// onDragFinished: PropTypes.func,
|
||||
// onChange: PropTypes.func,
|
||||
// onResizerClick: PropTypes.func,
|
||||
// onResizerDoubleClick: PropTypes.func,
|
||||
// style: stylePropType,
|
||||
// resizerStyle: stylePropType,
|
||||
// paneClassName: PropTypes.string,
|
||||
// pane1ClassName: PropTypes.string,
|
||||
// pane2ClassName: PropTypes.string,
|
||||
// paneStyle: stylePropType,
|
||||
// pane1Style: stylePropType,
|
||||
// pane2Style: stylePropType,
|
||||
// resizerClassName: PropTypes.string,
|
||||
// step: PropTypes.number
|
||||
// };
|
||||
|
||||
// SplitPane.defaultProps = {
|
||||
// allowResize: true,
|
||||
// minSize: 50,
|
||||
// primary: 'first',
|
||||
// split: 'vertical',
|
||||
// paneClassName: '',
|
||||
// pane1ClassName: '',
|
||||
// pane2ClassName: ''
|
||||
// };
|
||||
|
||||
// polyfill(SplitPane);
|
||||
|
||||
export default SplitPane;
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Radio from 'antd/es/radio';
|
||||
import Button from 'antd/es/button';
|
||||
import { createPrefixName } from '../../css';
|
||||
import IconMouse from '../../icons/mouse';
|
||||
import IconPen from '../../icons/pen';
|
||||
import IconHand from '../../icons/hand';
|
||||
import IconScale from '../../icons/scale';
|
||||
import IconLayer from '../../icons/layer';
|
||||
import IconSetting from '../../icons/setting';
|
||||
import IconMore from '../../icons/more';
|
||||
|
||||
const RadioButton = Radio.Button;
|
||||
const RadioGroup = Radio.Group;
|
||||
const modName = 'mod-toolbar';
|
||||
|
||||
const prefixName = createPrefixName(modName);
|
||||
|
||||
export interface ToolbarProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
openLeftSider: boolean;
|
||||
openRightSider: boolean;
|
||||
onClickToggleLayer?: () => void;
|
||||
onClickToggleSetting?: () => void;
|
||||
}
|
||||
|
||||
export const Toolbar = (props: ToolbarProps) => {
|
||||
const { className, style, openLeftSider, openRightSider, onClickToggleLayer, onClickToggleSetting } = props;
|
||||
const [mode, setMode] = useState<string>('select');
|
||||
const iconStyle = { fontSize: 20 };
|
||||
|
||||
return (
|
||||
<div style={style} className={classnames(prefixName(), className)}>
|
||||
<div className={prefixName('left')}>
|
||||
<Button shape="circle" type={openLeftSider ? 'primary' : 'default'} icon={<IconLayer style={iconStyle} />} onClick={onClickToggleLayer} />
|
||||
</div>
|
||||
<RadioGroup className={classnames(prefixName('middle'), prefixName('mode-switch'))} value={mode} onChange={(e) => setMode(e.target.value)}>
|
||||
<RadioButton value="select">
|
||||
<IconMouse style={iconStyle} />
|
||||
</RadioButton>
|
||||
<RadioButton value="pen">
|
||||
<IconPen style={iconStyle} />
|
||||
</RadioButton>
|
||||
<RadioButton value="hand">
|
||||
<IconHand style={iconStyle} />
|
||||
</RadioButton>
|
||||
<RadioButton value="scale">
|
||||
<IconScale style={iconStyle} />
|
||||
</RadioButton>
|
||||
<RadioButton value="more">
|
||||
<IconMore style={iconStyle} />
|
||||
</RadioButton>
|
||||
</RadioGroup>
|
||||
<div className={prefixName('right')}>
|
||||
<Button shape="circle" type={openRightSider ? 'primary' : 'default'} icon={<IconSetting style={iconStyle} />} onClick={onClickToggleSetting} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import type { Dispatch } from 'react';
|
||||
import type { Data } from '@idraw/types';
|
||||
import { LabData, LabDrawDataType } from './data';
|
||||
|
||||
export interface LabState {
|
||||
activeDrawDataType: LabDrawDataType;
|
||||
labData: LabData;
|
||||
viewDrawData: Data;
|
||||
viewDrawUUID: string | null;
|
||||
themeMode: 'light' | 'dark';
|
||||
}
|
||||
|
||||
export type LabActionType = 'updateThemeMode' | 'updateLabData' | 'switchDrawDataType';
|
||||
|
||||
export type LabAction = {
|
||||
type: LabActionType;
|
||||
payload: Partial<LabState>;
|
||||
};
|
||||
|
||||
export type LabDispatch = Dispatch<LabAction>;
|
||||
|
||||
export interface LabContext {
|
||||
state: LabState;
|
||||
dispatch: LabDispatch;
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import type { Element, ElementType, ElementSize, ElementBaseDesc } from '@idraw/types';
|
||||
|
||||
export type LabItemType = 'component' | 'component-item' | 'module' | 'page';
|
||||
|
||||
export type LabDrawDataType = 'component' | 'module' | 'page';
|
||||
|
||||
export type LabComponentItem = ElementSize & {
|
||||
uuid: string;
|
||||
type: 'component-item';
|
||||
name: string;
|
||||
detail?: ElementBaseDesc & {
|
||||
children: Array<Element<ElementType> | LabComponentItem>;
|
||||
};
|
||||
};
|
||||
|
||||
export type LabComponent = ElementSize & {
|
||||
uuid: string;
|
||||
type: 'component';
|
||||
name: string;
|
||||
detail?: ElementBaseDesc & {
|
||||
default: LabComponentItem;
|
||||
variants: LabComponentItem[];
|
||||
};
|
||||
};
|
||||
|
||||
export type LabModule = ElementSize & {
|
||||
uuid: string;
|
||||
type: 'module';
|
||||
name: string;
|
||||
detail?: ElementBaseDesc & {
|
||||
children: Array<LabComponent>;
|
||||
};
|
||||
};
|
||||
|
||||
export type LabPage = ElementSize & {
|
||||
uuid: string;
|
||||
type: 'page';
|
||||
name: string;
|
||||
detail: ElementBaseDesc & {
|
||||
children: Array<LabModule>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface LabData {
|
||||
components: LabComponent[];
|
||||
modules: LabModule[];
|
||||
pages: LabPage[];
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export * from './data';
|
||||
export * from './context';
|
||||
export * from './view';
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import type { ElementType } from '@idraw/types';
|
||||
import type { LabItemType } from './data';
|
||||
|
||||
export interface ViewTreeNode {
|
||||
title: string;
|
||||
key: string;
|
||||
type: LabItemType | ElementType;
|
||||
children?: ViewTreeNode[];
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { ViewTreeNode, LabData, LabComponent } from '../types';
|
||||
import { parseComponentToViewTreeNode } from './view-tree';
|
||||
|
||||
export function parseComponentViewTree(labData: LabData | null): ViewTreeNode[] {
|
||||
const treeNodes: ViewTreeNode[] = [];
|
||||
labData?.components?.forEach((comp: LabComponent) => {
|
||||
const node = parseComponentToViewTreeNode(comp);
|
||||
treeNodes.push(node);
|
||||
});
|
||||
return treeNodes;
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
import { deepClone } from '@idraw/util';
|
||||
import type { Data, Element, ElementType, ElementBaseDesc } from '@idraw/types';
|
||||
import type { LabComponent, LabComponentItem } from '../types';
|
||||
|
||||
const baseDescKeys = ['borderWidth', 'borderColor', 'borderRadius', 'shadowColor', 'shadowOffsetX', 'shadowOffsetY', 'shadowBlur', 'color', 'bgColor'];
|
||||
|
||||
function parseElementBaseDesc(elem: LabComponent | LabComponentItem | Element<ElementType>): ElementBaseDesc {
|
||||
const baseDesc: ElementBaseDesc = {};
|
||||
if (elem?.detail) {
|
||||
Object.keys(elem.detail).forEach((name: string) => {
|
||||
if (baseDescKeys.includes(name)) {
|
||||
baseDesc[name as keyof ElementBaseDesc] = (elem.detail as any)?.[name];
|
||||
}
|
||||
});
|
||||
}
|
||||
return baseDesc;
|
||||
}
|
||||
|
||||
function parseComponentItemToElement(item: LabComponentItem): Element<'group'> {
|
||||
const elem: Element<'group'> = {
|
||||
uuid: item.uuid,
|
||||
name: item.name,
|
||||
type: 'group',
|
||||
x: item.x,
|
||||
y: item.y,
|
||||
w: item.w,
|
||||
h: item.h,
|
||||
angle: item.angle || 0,
|
||||
detail: {
|
||||
...parseElementBaseDesc(item),
|
||||
...{
|
||||
children: []
|
||||
}
|
||||
}
|
||||
};
|
||||
item.detail?.children?.forEach?.((child) => {
|
||||
if (child.type === 'component-item') {
|
||||
const childElem = parseComponentItemToElement(child);
|
||||
elem.detail.children.push(childElem);
|
||||
} else {
|
||||
const childElem = deepClone(child);
|
||||
elem.detail.children.push(childElem);
|
||||
}
|
||||
});
|
||||
return elem;
|
||||
}
|
||||
|
||||
function parseComponentToElement(comp: LabComponent): Element<'group'> {
|
||||
const elem: Element<'group'> = {
|
||||
uuid: comp.uuid,
|
||||
name: comp.name,
|
||||
type: 'group',
|
||||
x: comp.x,
|
||||
y: comp.y,
|
||||
w: comp.w,
|
||||
h: comp.h,
|
||||
angle: comp.angle || 0,
|
||||
detail: {
|
||||
...parseElementBaseDesc(comp),
|
||||
...{
|
||||
children: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (comp?.detail?.default) {
|
||||
elem.detail.children.push(parseComponentItemToElement(comp.detail.default));
|
||||
}
|
||||
if (comp?.detail?.variants && Array.isArray(comp?.detail?.variants)) {
|
||||
comp.detail.variants.forEach((item) => {
|
||||
elem.detail.children.push(parseComponentItemToElement(item));
|
||||
});
|
||||
}
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
export function parseComponentsToDrawData(components: LabComponent[]): Data {
|
||||
const data: Data = {
|
||||
elements: []
|
||||
};
|
||||
components.forEach((comp: LabComponent) => {
|
||||
const elem = parseComponentToElement(comp);
|
||||
data.elements.push(elem);
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import type { Element, ElementType } from '@idraw/types';
|
||||
import { ViewTreeNode, LabComponent, LabComponentItem } from '../types';
|
||||
|
||||
function parseElementToViewTreeNode(elem: Element<ElementType>): ViewTreeNode | null {
|
||||
let treeNode: ViewTreeNode | null = null;
|
||||
if (elem.uuid) {
|
||||
treeNode = {
|
||||
key: elem.uuid,
|
||||
title: elem.name || 'Unamed',
|
||||
type: elem.type,
|
||||
children: []
|
||||
};
|
||||
if (Array.isArray((elem as Element<'group'>)?.detail?.children)) {
|
||||
(elem as Element<'group'>).detail.children.forEach((child: Element<ElementType>) => {
|
||||
const childNode = parseElementToViewTreeNode(child);
|
||||
if (childNode) {
|
||||
treeNode?.children?.push(childNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return treeNode;
|
||||
}
|
||||
|
||||
function parseComponentItemToViewTreeNode(comp: LabComponentItem): ViewTreeNode {
|
||||
const treeNode: Required<ViewTreeNode> = {
|
||||
key: comp.uuid,
|
||||
title: comp.name || 'Unamed',
|
||||
type: comp.type,
|
||||
children: []
|
||||
};
|
||||
|
||||
if (comp?.detail?.children && Array.isArray(comp?.detail?.children)) {
|
||||
comp.detail.children.forEach((child) => {
|
||||
let childNode: ViewTreeNode | null = null;
|
||||
if (child.type === 'component') {
|
||||
childNode = parseComponentToViewTreeNode(child as LabComponent);
|
||||
} else {
|
||||
childNode = parseElementToViewTreeNode(child as Element<ElementType>);
|
||||
}
|
||||
if (childNode) {
|
||||
treeNode.children.push(childNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
return treeNode;
|
||||
}
|
||||
|
||||
export function parseComponentToViewTreeNode(comp: LabComponent): ViewTreeNode {
|
||||
const treeNode: Required<ViewTreeNode> = {
|
||||
key: comp.uuid,
|
||||
title: comp.name || 'Unamed',
|
||||
type: comp.type,
|
||||
children: []
|
||||
};
|
||||
|
||||
if (comp?.detail?.default) {
|
||||
const node = parseComponentItemToViewTreeNode(comp.detail.default);
|
||||
treeNode.children.push(node);
|
||||
}
|
||||
|
||||
if (Array.isArray(comp?.detail?.variants)) {
|
||||
comp?.detail?.variants?.forEach((child: LabComponentItem) => {
|
||||
const node = parseComponentItemToViewTreeNode(child);
|
||||
treeNode.children.push(node);
|
||||
});
|
||||
}
|
||||
|
||||
return treeNode;
|
||||
}
|
||||
928
pnpm-lock.yaml
|
|
@ -13,10 +13,6 @@ const packages = [
|
|||
dirName: 'renderer',
|
||||
globalName: 'iDrawRenderer'
|
||||
},
|
||||
// {
|
||||
// dirName: 'kernal',
|
||||
// globalName: 'iDrawKernal',
|
||||
// },
|
||||
{
|
||||
dirName: 'core',
|
||||
globalName: 'iDrawCore'
|
||||
|
|
@ -24,6 +20,10 @@ const packages = [
|
|||
{
|
||||
dirName: 'idraw',
|
||||
globalName: 'iDraw'
|
||||
},
|
||||
{
|
||||
dirName: 'figma',
|
||||
globalName: 'iDrawFigma'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||