feat: add @idraw/figma

This commit is contained in:
chenshenhai 2024-06-08 17:32:56 +08:00
parent 506fcd2a82
commit 6786f97db6
90 changed files with 2374 additions and 3680 deletions

View 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>

View 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);
});

View 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"
}
}

View 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;
}

View 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);
});
}

View file

@ -0,0 +1 @@
export const figmaImageDir = 'images/';

View 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;
}

View 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';
}

View 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;
}

View 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]
};
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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 };
}

View 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
});
}

View file

@ -0,0 +1,2 @@
export { pickFigmaFile } from './file';
export { figmaBytesToMap, figmaMapToIDrawData, figmaBytesToIDrawData } from './figma-object';

View 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>>;
};

View file

@ -0,0 +1 @@
export * from './figma';

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

View file

@ -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>

View file

@ -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 />);

View file

@ -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"
}
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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';

View file

@ -1 +0,0 @@
export { createPrefixName } from './variable';

View file

@ -1,7 +0,0 @@
@import "../variable.less";
@mod-xxx: ~'@{prefix}-mod-xxx';
.@{mod-xxx} {
border: 1px solid #f0f0f0;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -1,7 +0,0 @@
@import '../variable.less';
@mod-xxx: ~'@{prefix}-mod-sketch';
.@{mod-xxx} {
border: 1px solid #f0f0f0;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -1,7 +0,0 @@
@import '../variable.less';
&.@{prefix}-theme {
--@{prefix}-bg-color: @white;
--@{prefix}-text-color: @gray-10;
--@{prefix}-border-color: @gray-4;
}

View file

@ -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;

View file

@ -1,7 +0,0 @@
export const PREFIX = 'idraw-lab';
export function createPrefixName(modName: string) {
return (...args: string[]) => {
return [PREFIX, modName, ...args].join('-');
};
}

View file

@ -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 &nbsp; 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);
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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';

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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';

View file

@ -1,4 +0,0 @@
import { createPrefixName } from '../../css';
const modName = 'mod-panel-layer';
export const prefixName = createPrefixName(modName);

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>;
};

View file

@ -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 };

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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;
}

View file

@ -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[];
}

View file

@ -1,3 +0,0 @@
export * from './data';
export * from './context';
export * from './view';

View file

@ -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[];
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

File diff suppressed because it is too large Load diff

View file

@ -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'
}
];