@fulate/import 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/fulate/index.d.ts +30 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +745 -0
- package/dist/sketch/converter.d.ts +3 -0
- package/dist/sketch/image.d.ts +6 -0
- package/dist/sketch/index.d.ts +8 -0
- package/dist/sketch/parser.d.ts +2 -0
- package/dist/sketch/style.d.ts +8 -0
- package/dist/sketch/text.d.ts +3 -0
- package/dist/sketch/types.d.ts +184 -0
- package/dist/types.d.ts +10 -0
- package/dist/util/index.d.ts +25 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# `@fulate/import`
|
|
2
|
+
|
|
3
|
+
> 文档角色:`GUIDE`。权威来源:import adapter 源码与测试;当前文件 schema 和元素身份规则见 Core/UI 领域规范。
|
|
4
|
+
|
|
5
|
+
`@fulate/import` 负责把当前支持的外部格式映射为 Fulate canonical creation input。它不是旧 Fulate
|
|
6
|
+
版本兼容层,不提供旧 schema 协商、自动迁移或整树 validation;解析后的声明格式直接交给当前
|
|
7
|
+
owner。
|
|
8
|
+
|
|
9
|
+
## Fulate 文件
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
serializeSceneToJSON,
|
|
14
|
+
parseFileData,
|
|
15
|
+
restoreScene
|
|
16
|
+
} from "@fulate/import";
|
|
17
|
+
|
|
18
|
+
const json = serializeSceneToJSON(root);
|
|
19
|
+
const data = parseFileData(json);
|
|
20
|
+
restoreScene(root, data);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`serializeScene()`/`serializeSceneToJSON()` 保存当前 Root 配置和元素树。当前 file marker 为
|
|
24
|
+
`__fulate_file__`,`version` 为 `2`。`deserializeElement()`
|
|
25
|
+
按 `type` 选择当前内建或调用者提供的 factory;文件中的 `key` 由 producer 保留,Core 不生成第二
|
|
26
|
+
份身份。`exportToFile()` 与 `importFromFile()` 是浏览器文件 API 的薄适配。
|
|
27
|
+
`parseFileData()` 只执行 JSON parse;声明格式错误和 restore 错误直接传播。History 属于应用组合层,
|
|
28
|
+
成功恢复场景后由拥有 Select 的应用决定是否清空。
|
|
29
|
+
|
|
30
|
+
## 自定义 factory
|
|
31
|
+
|
|
32
|
+
通过 `ReadonlyMap<string, DeserializeFactory>` 或普通对象传入 custom factory。factory 直接返回
|
|
33
|
+
当前 Element;不需要包装 DTO、预检整棵图或保留旧 reader。
|
|
34
|
+
|
|
35
|
+
## Sketch
|
|
36
|
+
|
|
37
|
+
`importSketch(file)` 解析 Sketch 输入并返回 `{ fileData, images, warnings }`;
|
|
38
|
+
`importSketchFile(root, deserialize, filter?)` 负责文件选择、恢复和必要的导入警告。Sketch 的
|
|
39
|
+
外部数据只在 adapter 边界映射,恢复后的场景继续使用当前 Fulate schema 与 owner。
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Element, type ElementJSON, type Root, type SkinOptions } from "@fulate/core";
|
|
2
|
+
import type { ElementFilter } from "../util";
|
|
3
|
+
export type { ElementFilter };
|
|
4
|
+
export type DeserializeFactory = (data: Record<string, any>) => Element;
|
|
5
|
+
export type DeserializeFactories = ReadonlyMap<string, DeserializeFactory> | Readonly<Record<string, DeserializeFactory>>;
|
|
6
|
+
export interface FileData {
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
__fulate_file__: true;
|
|
9
|
+
version: 2;
|
|
10
|
+
root?: {
|
|
11
|
+
viewport?: {
|
|
12
|
+
x?: number;
|
|
13
|
+
y?: number;
|
|
14
|
+
scale?: number;
|
|
15
|
+
minScale?: number;
|
|
16
|
+
maxScale?: number;
|
|
17
|
+
};
|
|
18
|
+
skin?: SkinOptions;
|
|
19
|
+
width?: number;
|
|
20
|
+
height?: number;
|
|
21
|
+
};
|
|
22
|
+
children: ElementJSON[];
|
|
23
|
+
}
|
|
24
|
+
export declare function serializeScene(root: Root, filter?: ElementFilter): FileData;
|
|
25
|
+
export declare function serializeSceneToJSON(root: Root, filter?: ElementFilter): string;
|
|
26
|
+
export declare function deserializeElement(data: Record<string, any>, factories?: DeserializeFactories): Element;
|
|
27
|
+
export declare function restoreScene(root: Root, fileData: FileData, filter?: ElementFilter, factories?: DeserializeFactories): void;
|
|
28
|
+
export declare function parseFileData(json: string): FileData;
|
|
29
|
+
export declare function exportToFile(root: Root, filename?: string, filter?: ElementFilter): void;
|
|
30
|
+
export declare function importFromFile(root: Root, filter?: ElementFilter, factories?: DeserializeFactories): Promise<boolean>;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { ImportResult, Importer } from "./types";
|
|
2
|
+
export { importSketch, importSketchFile, SketchImporter } from "./sketch";
|
|
3
|
+
export { serializeScene, serializeSceneToJSON, deserializeElement, restoreScene, parseFileData, exportToFile, importFromFile } from "./fulate";
|
|
4
|
+
export type { FileData, DeserializeFactories, DeserializeFactory } from "./fulate";
|
|
5
|
+
export { restoreScene as restoreSceneBase } from "./util";
|
|
6
|
+
export type { ElementFilter, DeserializeFn, RestoreOptions } from "./util";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
import { Element, Layer, Shape } from "@fulate/core";
|
|
2
|
+
import { Circle, Group, Image, Line, LineTree, Polygon, Rectangle, RippleOverlay, ScrollView, Text, Triangle, VectorPath, Workspace } from "@fulate/ui";
|
|
3
|
+
import { loadAsync } from "jszip";
|
|
4
|
+
import { isNil, isUndefined } from "lodash-es";
|
|
5
|
+
//#region packages/import/src/sketch/parser.ts
|
|
6
|
+
async function parseSketchFile(data) {
|
|
7
|
+
const zip = await loadAsync(data instanceof File ? await data.arrayBuffer() : data);
|
|
8
|
+
const document = await readJSON(zip, "document.json");
|
|
9
|
+
const meta = await readJSON(zip, "meta.json");
|
|
10
|
+
const pages = [];
|
|
11
|
+
for (const ref of document.pages) {
|
|
12
|
+
const pagePath = `${ref._ref}.json`;
|
|
13
|
+
pages.push(await readJSON(zip, pagePath));
|
|
14
|
+
}
|
|
15
|
+
const images = /* @__PURE__ */ new Map();
|
|
16
|
+
const imagesFolder = zip.folder("images");
|
|
17
|
+
if (imagesFolder) {
|
|
18
|
+
const entries = [];
|
|
19
|
+
imagesFolder.forEach((relativePath, file) => {
|
|
20
|
+
if (!file.dir) entries.push({
|
|
21
|
+
name: relativePath,
|
|
22
|
+
file
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
for (const { name, file } of entries) images.set(`images/${name}`, await file.async("arraybuffer"));
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
document,
|
|
29
|
+
meta,
|
|
30
|
+
pages,
|
|
31
|
+
images
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function readJSON(zip, path) {
|
|
35
|
+
const entry = zip.file(path);
|
|
36
|
+
if (!entry) throw new Error(`Missing ${path} in sketch file`);
|
|
37
|
+
const text = await entry.async("text");
|
|
38
|
+
return JSON.parse(text);
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region packages/import/src/sketch/style.ts
|
|
42
|
+
function sketchColorToCSS(c) {
|
|
43
|
+
const r = Math.round(c.red * 255);
|
|
44
|
+
const g = Math.round(c.green * 255);
|
|
45
|
+
const b = Math.round(c.blue * 255);
|
|
46
|
+
if (c.alpha >= 1) return `rgb(${r},${g},${b})`;
|
|
47
|
+
return `rgba(${r},${g},${b},${parseFloat(c.alpha.toFixed(3))})`;
|
|
48
|
+
}
|
|
49
|
+
function convertStyle(style) {
|
|
50
|
+
const props = {};
|
|
51
|
+
const warnings = [];
|
|
52
|
+
if (!style) return {
|
|
53
|
+
props,
|
|
54
|
+
warnings
|
|
55
|
+
};
|
|
56
|
+
if (!isUndefined(style.windingRule)) props.fillRule = style.windingRule === 1 ? "evenodd" : "nonzero";
|
|
57
|
+
const opacity = style.contextSettings?.opacity ?? style.opacity;
|
|
58
|
+
if (!isUndefined(opacity) && opacity < 1) props.opacity = opacity;
|
|
59
|
+
const gradientFill = findFirstEnabled(style.fills, (f) => f.fillType === 1);
|
|
60
|
+
if (gradientFill?.gradient) props.backgroundColor = convertGradient(gradientFill.gradient);
|
|
61
|
+
else {
|
|
62
|
+
const fill = findFirstEnabled(style.fills, (f) => f.fillType === 0);
|
|
63
|
+
if (fill) props.backgroundColor = sketchColorToCSS(fill.color);
|
|
64
|
+
}
|
|
65
|
+
const gradientBorder = findFirstEnabled(style.borders, (b) => b.fillType === 1);
|
|
66
|
+
if (gradientBorder?.gradient) {
|
|
67
|
+
props.borderColor = convertGradient(gradientBorder.gradient);
|
|
68
|
+
props.borderWidth = gradientBorder.thickness;
|
|
69
|
+
props.borderPosition = convertBorderPosition(gradientBorder.position);
|
|
70
|
+
} else {
|
|
71
|
+
const border = findFirstEnabled(style.borders, (b) => b.fillType === 0);
|
|
72
|
+
if (border) {
|
|
73
|
+
props.borderColor = sketchColorToCSS(border.color);
|
|
74
|
+
props.borderWidth = border.thickness;
|
|
75
|
+
props.borderPosition = convertBorderPosition(border.position);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const shadow = findFirstEnabled(style.shadows);
|
|
79
|
+
if (shadow) props.shadow = convertShadow(shadow);
|
|
80
|
+
if (style.innerShadows?.some((s) => s.isEnabled)) warnings.push("Inner shadows not supported, skipped");
|
|
81
|
+
if (style.blur?.isEnabled) warnings.push("Blur effect not supported, skipped");
|
|
82
|
+
return {
|
|
83
|
+
props,
|
|
84
|
+
warnings
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function convertBorderPosition(pos) {
|
|
88
|
+
if (pos === 2) return "outside";
|
|
89
|
+
return "inside";
|
|
90
|
+
}
|
|
91
|
+
function convertShadow(s) {
|
|
92
|
+
return {
|
|
93
|
+
color: sketchColorToCSS(s.color),
|
|
94
|
+
blur: s.blurRadius,
|
|
95
|
+
offsetX: s.offsetX,
|
|
96
|
+
offsetY: s.offsetY
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function convertGradient(sg) {
|
|
100
|
+
const from = parseSketchNormalizedPoint(sg.from);
|
|
101
|
+
const to = parseSketchNormalizedPoint(sg.to);
|
|
102
|
+
return {
|
|
103
|
+
type: sg.gradientType === 0 ? "linear" : "radial",
|
|
104
|
+
from,
|
|
105
|
+
to,
|
|
106
|
+
...sg.gradientType === 1 ? {
|
|
107
|
+
center: from,
|
|
108
|
+
radius: .5
|
|
109
|
+
} : {},
|
|
110
|
+
stops: sg.stops.map((s) => ({
|
|
111
|
+
color: sketchColorToCSS(s.color),
|
|
112
|
+
position: s.position
|
|
113
|
+
}))
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function parseSketchNormalizedPoint(str) {
|
|
117
|
+
const match = str.match(/\{([^,]+),\s*([^}]+)\}/);
|
|
118
|
+
if (!match) return {
|
|
119
|
+
x: .5,
|
|
120
|
+
y: .5
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
x: parseFloat(match[1]),
|
|
124
|
+
y: parseFloat(match[2])
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function findFirstEnabled(items, filter) {
|
|
128
|
+
if (!items) return void 0;
|
|
129
|
+
return items.find((item) => item.isEnabled && (!filter || filter(item)));
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region packages/import/src/sketch/text.ts
|
|
133
|
+
function convertTextProps(layer) {
|
|
134
|
+
const result = {};
|
|
135
|
+
const attrStr = layer.attributedString;
|
|
136
|
+
if (!attrStr) return result;
|
|
137
|
+
result.text = attrStr.string;
|
|
138
|
+
result.overflow = "visible";
|
|
139
|
+
result.verticalAlign = "top";
|
|
140
|
+
result.wordWrap = false;
|
|
141
|
+
const dominant = getDominantAttribute(attrStr.attributes);
|
|
142
|
+
if (!dominant) return result;
|
|
143
|
+
const font = dominant.attributes.MSAttributedStringFontAttribute;
|
|
144
|
+
if (font) {
|
|
145
|
+
result.fontSize = font.attributes.size;
|
|
146
|
+
result.fontFamily = normalizeFontFamily(font.attributes.name);
|
|
147
|
+
if (isBoldFont(font.attributes.name)) result.fontWeight = "bold";
|
|
148
|
+
if (isItalicFont(font.attributes.name)) result.fontStyle = "italic";
|
|
149
|
+
}
|
|
150
|
+
const color = dominant.attributes.MSAttributedStringColorAttribute;
|
|
151
|
+
if (color) result.color = sketchColorToCSS(color);
|
|
152
|
+
const paragraph = dominant.attributes.paragraphStyle;
|
|
153
|
+
if (paragraph) {
|
|
154
|
+
result.textAlign = convertAlignment(paragraph.alignment);
|
|
155
|
+
if (paragraph.maximumLineHeight && result.fontSize) result.lineHeight = parseFloat((paragraph.maximumLineHeight / result.fontSize).toFixed(2));
|
|
156
|
+
}
|
|
157
|
+
if (dominant.attributes.underlineStyle) result.underline = true;
|
|
158
|
+
if (dominant.attributes.strikethroughStyle) result.strikethrough = true;
|
|
159
|
+
if (dominant.attributes.kerning) result.letterSpacing = dominant.attributes.kerning;
|
|
160
|
+
if (layer.style) {
|
|
161
|
+
const { props } = convertStyle(layer.style);
|
|
162
|
+
if (props.backgroundColor) result.color = props.backgroundColor;
|
|
163
|
+
if (props.borderColor && props.borderWidth) {
|
|
164
|
+
result.textStrokeColor = props.borderColor;
|
|
165
|
+
result.textStrokeWidth = props.borderWidth;
|
|
166
|
+
}
|
|
167
|
+
if (props.shadow) result.textShadow = props.shadow;
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
function getDominantAttribute(attrs) {
|
|
172
|
+
if (!attrs.length) return void 0;
|
|
173
|
+
if (attrs.length === 1) return attrs[0];
|
|
174
|
+
return attrs.reduce((a, b) => a.length >= b.length ? a : b);
|
|
175
|
+
}
|
|
176
|
+
function convertAlignment(alignment) {
|
|
177
|
+
switch (alignment) {
|
|
178
|
+
case 1: return "right";
|
|
179
|
+
case 2: return "center";
|
|
180
|
+
default: return "left";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function normalizeFontFamily(name) {
|
|
184
|
+
return name.replace(/-(Bold|Italic|Light|Medium|Regular|Thin|Black|Heavy|Semibold|UltraLight|ExtraBold|BoldItalic|MediumItalic|LightItalic)$/i, "").replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
185
|
+
}
|
|
186
|
+
function isBoldFont(name) {
|
|
187
|
+
return /bold|black|heavy|extrabold|semibold/i.test(name);
|
|
188
|
+
}
|
|
189
|
+
function isItalicFont(name) {
|
|
190
|
+
return /italic|oblique/i.test(name);
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region packages/import/src/util/index.ts
|
|
194
|
+
function defaultAppend(els, root) {
|
|
195
|
+
root.append(...els);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 通用场景恢复模板:
|
|
199
|
+
* 1. 应用 root 配置(resize、viewport、Skin)
|
|
200
|
+
* 2. reset viewport
|
|
201
|
+
* 3. 移除旧元素(由 filter 决定)
|
|
202
|
+
* 4. 反序列化并添加 children(由 append 决定)
|
|
203
|
+
* 5. requestRender
|
|
204
|
+
*/
|
|
205
|
+
function restoreScene$1(options) {
|
|
206
|
+
const { root, fileData, deserialize, append, filter } = options;
|
|
207
|
+
const toRemove = filter ? root.children.filter((child) => filter(child)) : [];
|
|
208
|
+
if (fileData.root) {
|
|
209
|
+
const { viewport, skin, width, height } = fileData.root;
|
|
210
|
+
if (skin) {
|
|
211
|
+
const { text, ...paint } = skin;
|
|
212
|
+
Object.assign(root.skin, paint);
|
|
213
|
+
if (text) Object.assign(root.skin.text, text);
|
|
214
|
+
}
|
|
215
|
+
if (!isNil(width) && !isNil(height)) root.resize(width, height);
|
|
216
|
+
if (viewport) root.viewport.restore({
|
|
217
|
+
x: viewport.x ?? 0,
|
|
218
|
+
y: viewport.y ?? 0,
|
|
219
|
+
scale: viewport.scale ?? 1
|
|
220
|
+
});
|
|
221
|
+
else root.viewport.reset();
|
|
222
|
+
} else root.viewport.reset();
|
|
223
|
+
if (toRemove.length) root.removeChild(...toRemove);
|
|
224
|
+
const els = fileData.children.map((data) => deserialize(data));
|
|
225
|
+
if (els.length) (append ?? defaultAppend)(els, root);
|
|
226
|
+
root.invalidateAll();
|
|
227
|
+
}
|
|
228
|
+
function pickFile(accept) {
|
|
229
|
+
return new Promise((resolve) => {
|
|
230
|
+
const input = document.createElement("input");
|
|
231
|
+
input.type = "file";
|
|
232
|
+
input.accept = accept;
|
|
233
|
+
input.onchange = () => resolve(input.files?.[0] ?? null);
|
|
234
|
+
input.click();
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function arrayBufferToDataURL(buf, mime) {
|
|
238
|
+
const bytes = new Uint8Array(buf);
|
|
239
|
+
let binary = "";
|
|
240
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
241
|
+
return `data:${mime};base64,${btoa(binary)}`;
|
|
242
|
+
}
|
|
243
|
+
function guessMimeType(path) {
|
|
244
|
+
switch (path.split(".").pop()?.toLowerCase()) {
|
|
245
|
+
case "jpg":
|
|
246
|
+
case "jpeg": return "image/jpeg";
|
|
247
|
+
case "png": return "image/png";
|
|
248
|
+
case "gif": return "image/gif";
|
|
249
|
+
case "webp": return "image/webp";
|
|
250
|
+
case "svg": return "image/svg+xml";
|
|
251
|
+
default: return "image/png";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region packages/import/src/sketch/image.ts
|
|
256
|
+
/**
|
|
257
|
+
* Extract the image ref path from a bitmap layer and resolve it
|
|
258
|
+
* to a data URL using the images extracted from the zip.
|
|
259
|
+
*/
|
|
260
|
+
function resolveImageSrc(layer, images) {
|
|
261
|
+
const ref = layer.image;
|
|
262
|
+
if (!ref) return void 0;
|
|
263
|
+
const refPath = ref._ref;
|
|
264
|
+
const buf = images.get(refPath);
|
|
265
|
+
if (!buf) return void 0;
|
|
266
|
+
return arrayBufferToDataURL(buf, guessMimeType(refPath));
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
269
|
+
//#region packages/import/src/sketch/converter.ts
|
|
270
|
+
function convertSketchToFileData(sketch) {
|
|
271
|
+
const warnings = [];
|
|
272
|
+
const imageDataURLs = /* @__PURE__ */ new Map();
|
|
273
|
+
const children = [];
|
|
274
|
+
const ctx = {
|
|
275
|
+
offsetX: 0,
|
|
276
|
+
offsetY: 0,
|
|
277
|
+
flipH: false,
|
|
278
|
+
flipV: false,
|
|
279
|
+
groupW: 0,
|
|
280
|
+
groupH: 0,
|
|
281
|
+
zipImages: sketch.images,
|
|
282
|
+
imageDataURLs,
|
|
283
|
+
warnings,
|
|
284
|
+
out: children
|
|
285
|
+
};
|
|
286
|
+
for (const page of sketch.pages) flattenLayers(page.layers ?? [], ctx);
|
|
287
|
+
return {
|
|
288
|
+
fileData: {
|
|
289
|
+
__fulate_file__: true,
|
|
290
|
+
version: 2,
|
|
291
|
+
children
|
|
292
|
+
},
|
|
293
|
+
images: imageDataURLs,
|
|
294
|
+
warnings
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
var MASK_SHAPE_TYPES = new Set([
|
|
298
|
+
"rectangle",
|
|
299
|
+
"circle",
|
|
300
|
+
"triangle",
|
|
301
|
+
"polygon",
|
|
302
|
+
"vectorPath"
|
|
303
|
+
]);
|
|
304
|
+
function flattenLayers(layers, ctx) {
|
|
305
|
+
for (let index = 0; index < layers.length;) {
|
|
306
|
+
const layer = layers[index];
|
|
307
|
+
if (!layer.isVisible || !layer.hasClippingMask) {
|
|
308
|
+
flattenLayer(layer, ctx);
|
|
309
|
+
index++;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if ((layer.clippingMaskMode ?? 0) !== 0) {
|
|
313
|
+
ctx.warnings.push(`[${layer.name}] alpha clipping mask not supported`);
|
|
314
|
+
flattenLayer(layer, ctx);
|
|
315
|
+
index++;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (layer.rotation || layer.isFlippedHorizontal || layer.isFlippedVertical || ctx.flipH || ctx.flipV) {
|
|
319
|
+
ctx.warnings.push(`[${layer.name}] transformed clipping mask not supported`);
|
|
320
|
+
flattenLayer(layer, ctx);
|
|
321
|
+
index++;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
let chainEnd = index + 1;
|
|
325
|
+
while (chainEnd < layers.length && !layers[chainEnd].shouldBreakMaskChain) chainEnd++;
|
|
326
|
+
const maskOutput = [];
|
|
327
|
+
flattenLayer(layer, {
|
|
328
|
+
...ctx,
|
|
329
|
+
out: maskOutput
|
|
330
|
+
});
|
|
331
|
+
const mask = maskOutput.length === 1 ? maskOutput[0] : void 0;
|
|
332
|
+
if (!mask || !MASK_SHAPE_TYPES.has(mask.type)) {
|
|
333
|
+
ctx.warnings.push(`[${layer.name}] clipping mask shape not supported`);
|
|
334
|
+
ctx.out.push(...maskOutput);
|
|
335
|
+
index++;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
mask.overflow = "hidden";
|
|
339
|
+
const children = [];
|
|
340
|
+
flattenLayers(layers.slice(index + 1, chainEnd), {
|
|
341
|
+
...ctx,
|
|
342
|
+
out: children
|
|
343
|
+
});
|
|
344
|
+
for (const child of children) {
|
|
345
|
+
child.left -= mask.left;
|
|
346
|
+
child.top -= mask.top;
|
|
347
|
+
}
|
|
348
|
+
if (children.length) mask.children = children;
|
|
349
|
+
ctx.out.push(mask);
|
|
350
|
+
index = chainEnd;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function flattenLayer(layer, ctx) {
|
|
354
|
+
if (!layer.isVisible) return;
|
|
355
|
+
const cls = layer._class;
|
|
356
|
+
if (cls === "artboard") {
|
|
357
|
+
const childCtx = {
|
|
358
|
+
...ctx,
|
|
359
|
+
offsetX: ctx.offsetX,
|
|
360
|
+
offsetY: ctx.offsetY,
|
|
361
|
+
flipH: false,
|
|
362
|
+
flipV: false,
|
|
363
|
+
groupW: 0,
|
|
364
|
+
groupH: 0
|
|
365
|
+
};
|
|
366
|
+
flattenLayers(layer.layers ?? [], childCtx);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (cls === "group") {
|
|
370
|
+
let cx = ctx.offsetX + layer.frame.x;
|
|
371
|
+
let cy = ctx.offsetY + layer.frame.y;
|
|
372
|
+
if (ctx.flipH) cx = ctx.offsetX + (ctx.groupW - layer.frame.x - layer.frame.width);
|
|
373
|
+
if (ctx.flipV) cy = ctx.offsetY + (ctx.groupH - layer.frame.y - layer.frame.height);
|
|
374
|
+
const childCtx = {
|
|
375
|
+
...ctx,
|
|
376
|
+
offsetX: cx,
|
|
377
|
+
offsetY: cy,
|
|
378
|
+
flipH: ctx.flipH !== !!layer.isFlippedHorizontal,
|
|
379
|
+
flipV: ctx.flipV !== !!layer.isFlippedVertical,
|
|
380
|
+
groupW: layer.frame.width,
|
|
381
|
+
groupH: layer.frame.height
|
|
382
|
+
};
|
|
383
|
+
flattenLayers(layer.layers ?? [], childCtx);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (cls === "shapeGroup") {
|
|
387
|
+
const vectorPath = convertShapeGroup(layer, ctx);
|
|
388
|
+
if (vectorPath) ctx.out.push(vectorPath);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const type = mapSketchClass(cls, layer, ctx.warnings);
|
|
392
|
+
if (!type) return;
|
|
393
|
+
const base = buildBase(layer, type, ctx);
|
|
394
|
+
switch (type) {
|
|
395
|
+
case "text":
|
|
396
|
+
delete base.backgroundColor;
|
|
397
|
+
delete base.borderColor;
|
|
398
|
+
delete base.borderWidth;
|
|
399
|
+
delete base.borderPosition;
|
|
400
|
+
delete base.shadow;
|
|
401
|
+
Object.assign(base, convertTextProps(layer));
|
|
402
|
+
break;
|
|
403
|
+
case "image":
|
|
404
|
+
assignImageSrc(base, layer, ctx);
|
|
405
|
+
break;
|
|
406
|
+
case "line":
|
|
407
|
+
assignLinePath(base, layer);
|
|
408
|
+
break;
|
|
409
|
+
case "polygon":
|
|
410
|
+
assignPolygonContour(base, layer);
|
|
411
|
+
break;
|
|
412
|
+
case "vectorPath":
|
|
413
|
+
assignVectorPath(base, layer);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
applyInheritedFlips(base, ctx);
|
|
417
|
+
ctx.out.push(base);
|
|
418
|
+
}
|
|
419
|
+
function applyInheritedFlips(base, ctx) {
|
|
420
|
+
if (ctx.flipH) base.scaleX = (base.scaleX ?? 1) * -1;
|
|
421
|
+
if (ctx.flipV) base.scaleY = (base.scaleY ?? 1) * -1;
|
|
422
|
+
}
|
|
423
|
+
function buildBase(layer, type, ctx) {
|
|
424
|
+
const { props, warnings: styleWarnings } = convertStyle(layer.style);
|
|
425
|
+
for (const w of styleWarnings) ctx.warnings.push(`[${layer.name}] ${w}`);
|
|
426
|
+
let left = ctx.offsetX + layer.frame.x;
|
|
427
|
+
let top = ctx.offsetY + layer.frame.y;
|
|
428
|
+
if (ctx.flipH) left = ctx.offsetX + (ctx.groupW - layer.frame.x - layer.frame.width);
|
|
429
|
+
if (ctx.flipV) top = ctx.offsetY + (ctx.groupH - layer.frame.y - layer.frame.height);
|
|
430
|
+
const base = {
|
|
431
|
+
type,
|
|
432
|
+
left,
|
|
433
|
+
top,
|
|
434
|
+
width: layer.frame.width,
|
|
435
|
+
height: layer.frame.height,
|
|
436
|
+
...props
|
|
437
|
+
};
|
|
438
|
+
if (layer.rotation) base.angle = -layer.rotation;
|
|
439
|
+
if (type === "rectangle" || type === "text" || type === "image") {
|
|
440
|
+
if (layer.fixedRadius) base.radius = layer.fixedRadius;
|
|
441
|
+
else if (layer.points?.length) {
|
|
442
|
+
const cr = layer.points[0].cornerRadius;
|
|
443
|
+
if (cr > 0) base.radius = cr;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (layer.isFlippedHorizontal) base.scaleX = -1;
|
|
447
|
+
if (layer.isFlippedVertical) base.scaleY = -1;
|
|
448
|
+
return base;
|
|
449
|
+
}
|
|
450
|
+
var CLASS_MAP = {
|
|
451
|
+
rectangle: "rectangle",
|
|
452
|
+
oval: "circle",
|
|
453
|
+
triangle: "triangle",
|
|
454
|
+
text: "text",
|
|
455
|
+
bitmap: "image",
|
|
456
|
+
star: "polygon",
|
|
457
|
+
polygon: "polygon",
|
|
458
|
+
slice: "",
|
|
459
|
+
hotspot: "",
|
|
460
|
+
MSImmutableHotspotLayer: ""
|
|
461
|
+
};
|
|
462
|
+
function mapSketchClass(cls, layer, warnings) {
|
|
463
|
+
if (cls in CLASS_MAP) {
|
|
464
|
+
const mapped = CLASS_MAP[cls];
|
|
465
|
+
if (!mapped) return null;
|
|
466
|
+
if (cls === "star") warnings.push(`[${layer.name}] "${cls}" mapped to polygon`);
|
|
467
|
+
return mapped;
|
|
468
|
+
}
|
|
469
|
+
if (cls === "shapePath") return resolveShapePath(layer, warnings);
|
|
470
|
+
if (cls === "shapeGroup") return null;
|
|
471
|
+
warnings.push(`[${layer.name}] unsupported layer class "${cls}", skipped`);
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
function resolveShapePath(layer, warnings) {
|
|
475
|
+
const points = layer.points;
|
|
476
|
+
if (!points || points.length < 2) {
|
|
477
|
+
warnings.push(`[${layer.name}] shapePath with <2 points, skipped`);
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
if (points.some(hasCurveHandle)) return "vectorPath";
|
|
481
|
+
if (layer.isClosed) return "polygon";
|
|
482
|
+
return "line";
|
|
483
|
+
}
|
|
484
|
+
function hasCurveHandle(point) {
|
|
485
|
+
return hasOutgoingHandle(point) || hasIncomingHandle(point);
|
|
486
|
+
}
|
|
487
|
+
function hasOutgoingHandle(point) {
|
|
488
|
+
return point.hasCurveFrom ?? !sketchPointsNearEqual(point.point, point.curveFrom);
|
|
489
|
+
}
|
|
490
|
+
function hasIncomingHandle(point) {
|
|
491
|
+
return point.hasCurveTo ?? !sketchPointsNearEqual(point.point, point.curveTo);
|
|
492
|
+
}
|
|
493
|
+
function sketchPointsNearEqual(a, b, eps = 1e-6) {
|
|
494
|
+
if (a === b) return true;
|
|
495
|
+
const pa = parseSketchPointRaw(a);
|
|
496
|
+
const pb = parseSketchPointRaw(b);
|
|
497
|
+
if (!pa || !pb) return false;
|
|
498
|
+
return Math.abs(pa.x - pb.x) < eps && Math.abs(pa.y - pb.y) < eps;
|
|
499
|
+
}
|
|
500
|
+
function parseSketchPointRaw(str) {
|
|
501
|
+
const match = str.match(/\{([^,]+),\s*([^}]+)\}/);
|
|
502
|
+
if (!match) return null;
|
|
503
|
+
return {
|
|
504
|
+
x: parseFloat(match[1]),
|
|
505
|
+
y: parseFloat(match[2])
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function convertShapeGroup(layer, ctx) {
|
|
509
|
+
const paths = (layer.layers ?? []).filter((child) => child.isVisible);
|
|
510
|
+
if (!(paths.length > 0 && paths.every((path) => path._class === "shapePath" && (path.booleanOperation === void 0 || path.booleanOperation === -1) && (path.points?.length ?? 0) >= 2))) {
|
|
511
|
+
ctx.warnings.push(`[${layer.name}] boolean or heterogeneous shapeGroup not supported, skipped`);
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
const base = buildBase(layer, "vectorPath", ctx);
|
|
515
|
+
base.path = paths.flatMap((path) => createVectorPathCommands(path, (point) => parseShapeGroupPoint(point, path)));
|
|
516
|
+
applyInheritedFlips(base, ctx);
|
|
517
|
+
return base;
|
|
518
|
+
}
|
|
519
|
+
function assignVectorPath(base, layer) {
|
|
520
|
+
const { width, height } = layer.frame;
|
|
521
|
+
base.path = createVectorPathCommands(layer, (point) => parseSketchPoint(point, width, height));
|
|
522
|
+
}
|
|
523
|
+
function createVectorPathCommands(layer, resolvePoint) {
|
|
524
|
+
const points = layer.points;
|
|
525
|
+
const commands = [{
|
|
526
|
+
type: "M",
|
|
527
|
+
point: resolvePoint(points[0].point)
|
|
528
|
+
}];
|
|
529
|
+
for (let index = 1; index < points.length; index++) appendSketchSegment(commands, points[index - 1], points[index], resolvePoint, false);
|
|
530
|
+
if (layer.isClosed) {
|
|
531
|
+
appendSketchSegment(commands, points[points.length - 1], points[0], resolvePoint, true);
|
|
532
|
+
commands.push({ type: "Z" });
|
|
533
|
+
}
|
|
534
|
+
return commands;
|
|
535
|
+
}
|
|
536
|
+
function appendSketchSegment(commands, from, to, resolvePoint, closing) {
|
|
537
|
+
const outgoing = hasOutgoingHandle(from);
|
|
538
|
+
const incoming = hasIncomingHandle(to);
|
|
539
|
+
if (outgoing || incoming) commands.push({
|
|
540
|
+
type: "C",
|
|
541
|
+
control1: resolvePoint(outgoing ? from.curveFrom : from.point),
|
|
542
|
+
control2: resolvePoint(incoming ? to.curveTo : to.point),
|
|
543
|
+
point: resolvePoint(to.point)
|
|
544
|
+
});
|
|
545
|
+
else if (!closing) commands.push({
|
|
546
|
+
type: "L",
|
|
547
|
+
point: resolvePoint(to.point)
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
function parseShapeGroupPoint(point, path) {
|
|
551
|
+
const { width, height, x: left, y: top } = path.frame;
|
|
552
|
+
let { x, y } = parseSketchPoint(point, width, height);
|
|
553
|
+
if (path.isFlippedHorizontal) x = width - x;
|
|
554
|
+
if (path.isFlippedVertical) y = height - y;
|
|
555
|
+
if (path.rotation) {
|
|
556
|
+
const centerX = width / 2;
|
|
557
|
+
const centerY = height / 2;
|
|
558
|
+
const radians = -path.rotation * Math.PI / 180;
|
|
559
|
+
const cos = Math.cos(radians);
|
|
560
|
+
const sin = Math.sin(radians);
|
|
561
|
+
const offsetX = x - centerX;
|
|
562
|
+
const offsetY = y - centerY;
|
|
563
|
+
x = centerX + offsetX * cos - offsetY * sin;
|
|
564
|
+
y = centerY + offsetX * sin + offsetY * cos;
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
x: left + x,
|
|
568
|
+
y: top + y
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function assignPolygonContour(base, layer) {
|
|
572
|
+
const points = layer.points;
|
|
573
|
+
if (!points?.length) return;
|
|
574
|
+
const { width, height } = layer.frame;
|
|
575
|
+
base.contour = points.map((p) => parseSketchPoint(p.point, width, height));
|
|
576
|
+
}
|
|
577
|
+
function assignLinePath(base, layer) {
|
|
578
|
+
const points = layer.points;
|
|
579
|
+
if (!points?.length) return;
|
|
580
|
+
const { width, height } = layer.frame;
|
|
581
|
+
let parsed = points.map((p) => parseSketchPoint(p.point, width, height));
|
|
582
|
+
if (base.angle) {
|
|
583
|
+
const cx = width / 2;
|
|
584
|
+
const cy = height / 2;
|
|
585
|
+
const rad = base.angle * Math.PI / 180;
|
|
586
|
+
const cos = Math.cos(rad);
|
|
587
|
+
const sin = Math.sin(rad);
|
|
588
|
+
parsed = parsed.map((p) => ({
|
|
589
|
+
x: cx + (p.x - cx) * cos - (p.y - cy) * sin,
|
|
590
|
+
y: cy + (p.x - cx) * sin + (p.y - cy) * cos
|
|
591
|
+
}));
|
|
592
|
+
delete base.angle;
|
|
593
|
+
}
|
|
594
|
+
const origin = parsed[0];
|
|
595
|
+
base.left += origin.x;
|
|
596
|
+
base.top += origin.y;
|
|
597
|
+
base.path = parsed.map((p) => ({
|
|
598
|
+
x: p.x - origin.x,
|
|
599
|
+
y: p.y - origin.y
|
|
600
|
+
}));
|
|
601
|
+
delete base.width;
|
|
602
|
+
delete base.height;
|
|
603
|
+
if (base.borderColor) {
|
|
604
|
+
base.strokeColor = base.borderColor;
|
|
605
|
+
delete base.borderColor;
|
|
606
|
+
}
|
|
607
|
+
if (base.borderWidth) {
|
|
608
|
+
base.strokeWidth = base.borderWidth;
|
|
609
|
+
delete base.borderWidth;
|
|
610
|
+
}
|
|
611
|
+
delete base.borderPosition;
|
|
612
|
+
}
|
|
613
|
+
function parseSketchPoint(str, frameW, frameH) {
|
|
614
|
+
const match = str.match(/\{([^,]+),\s*([^}]+)\}/);
|
|
615
|
+
if (!match) return {
|
|
616
|
+
x: 0,
|
|
617
|
+
y: 0
|
|
618
|
+
};
|
|
619
|
+
return {
|
|
620
|
+
x: parseFloat(match[1]) * frameW,
|
|
621
|
+
y: parseFloat(match[2]) * frameH
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function assignImageSrc(base, layer, ctx) {
|
|
625
|
+
const src = resolveImageSrc(layer, ctx.zipImages);
|
|
626
|
+
if (src) {
|
|
627
|
+
base.src = src;
|
|
628
|
+
const ref = layer.image?._ref;
|
|
629
|
+
if (ref) ctx.imageDataURLs.set(ref, src);
|
|
630
|
+
} else ctx.warnings.push(`[${layer.name}] image resource not found`);
|
|
631
|
+
}
|
|
632
|
+
//#endregion
|
|
633
|
+
//#region packages/import/src/sketch/index.ts
|
|
634
|
+
var SketchImporter = class {
|
|
635
|
+
async import(file) {
|
|
636
|
+
return importSketch(file);
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
async function importSketch(file) {
|
|
640
|
+
return convertSketchToFileData(await parseSketchFile(file));
|
|
641
|
+
}
|
|
642
|
+
async function importSketchFile(root, deserialize, filter) {
|
|
643
|
+
const file = await pickFile(".sketch");
|
|
644
|
+
if (!file) return false;
|
|
645
|
+
const result = await importSketch(file);
|
|
646
|
+
if (result.warnings.length) console.warn("[Sketch Import]", result.warnings);
|
|
647
|
+
restoreScene$1({
|
|
648
|
+
root,
|
|
649
|
+
fileData: result.fileData,
|
|
650
|
+
deserialize,
|
|
651
|
+
filter,
|
|
652
|
+
append: (els, root) => {
|
|
653
|
+
const layer = new Layer();
|
|
654
|
+
const workspace = new Workspace({
|
|
655
|
+
key: "workspace",
|
|
656
|
+
width: 1920,
|
|
657
|
+
height: 1080
|
|
658
|
+
});
|
|
659
|
+
layer.append(workspace, ...els);
|
|
660
|
+
root.append(layer);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
//#endregion
|
|
666
|
+
//#region packages/import/src/fulate/index.ts
|
|
667
|
+
var FILE_MARKER = "__fulate_file__";
|
|
668
|
+
var FILE_VERSION = 2;
|
|
669
|
+
var builtInConstructors = {
|
|
670
|
+
element: Element,
|
|
671
|
+
shape: Shape,
|
|
672
|
+
layer: Layer,
|
|
673
|
+
rectangle: Rectangle,
|
|
674
|
+
circle: Circle,
|
|
675
|
+
triangle: Triangle,
|
|
676
|
+
polygon: Polygon,
|
|
677
|
+
vectorPath: VectorPath,
|
|
678
|
+
text: Text,
|
|
679
|
+
image: Image,
|
|
680
|
+
workspace: Workspace,
|
|
681
|
+
group: Group,
|
|
682
|
+
scrollview: ScrollView,
|
|
683
|
+
line: Line,
|
|
684
|
+
lineTree: LineTree,
|
|
685
|
+
ripple: RippleOverlay
|
|
686
|
+
};
|
|
687
|
+
function normalizeType(type) {
|
|
688
|
+
return type.startsWith("f-") ? type.slice(2) : type;
|
|
689
|
+
}
|
|
690
|
+
var builtInFactories = Object.fromEntries(Object.entries(builtInConstructors).map(([type, Constructor]) => [type, (data) => {
|
|
691
|
+
const { type, children, ...options } = data;
|
|
692
|
+
return new Constructor(options);
|
|
693
|
+
}]));
|
|
694
|
+
function resolveFactory(type, factories) {
|
|
695
|
+
const normalized = normalizeType(type);
|
|
696
|
+
let custom;
|
|
697
|
+
if (factories instanceof Map) custom = factories.get(normalized);
|
|
698
|
+
else if (factories && Object.prototype.hasOwnProperty.call(factories, normalized)) custom = factories[normalized];
|
|
699
|
+
return custom ?? builtInFactories[normalized];
|
|
700
|
+
}
|
|
701
|
+
function serializeScene(root, filter) {
|
|
702
|
+
return {
|
|
703
|
+
[FILE_MARKER]: true,
|
|
704
|
+
version: FILE_VERSION,
|
|
705
|
+
root: root.toJSON(),
|
|
706
|
+
children: root.children.filter((child) => !filter || filter(child)).map((child) => child.toJSON())
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function serializeSceneToJSON(root, filter) {
|
|
710
|
+
return JSON.stringify(serializeScene(root, filter));
|
|
711
|
+
}
|
|
712
|
+
function deserializeElement(data, factories) {
|
|
713
|
+
const element = resolveFactory(data.type, factories)(data);
|
|
714
|
+
if (data.children) element.replaceChildren(...data.children.map((child) => deserializeElement(child, factories)));
|
|
715
|
+
return element;
|
|
716
|
+
}
|
|
717
|
+
function restoreScene(root, fileData, filter, factories) {
|
|
718
|
+
restoreScene$1({
|
|
719
|
+
root,
|
|
720
|
+
fileData,
|
|
721
|
+
deserialize: (data) => deserializeElement(data, factories),
|
|
722
|
+
filter
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
function parseFileData(json) {
|
|
726
|
+
return JSON.parse(json);
|
|
727
|
+
}
|
|
728
|
+
function exportToFile(root, filename = "fulate-design.json", filter) {
|
|
729
|
+
const json = serializeSceneToJSON(root, filter);
|
|
730
|
+
const blob = new Blob([json], { type: "application/json" });
|
|
731
|
+
const url = URL.createObjectURL(blob);
|
|
732
|
+
const anchor = document.createElement("a");
|
|
733
|
+
anchor.href = url;
|
|
734
|
+
anchor.download = filename;
|
|
735
|
+
anchor.click();
|
|
736
|
+
URL.revokeObjectURL(url);
|
|
737
|
+
}
|
|
738
|
+
async function importFromFile(root, filter, factories) {
|
|
739
|
+
const file = await pickFile(".json");
|
|
740
|
+
if (!file) return false;
|
|
741
|
+
restoreScene(root, parseFileData(await file.text()), filter, factories);
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
//#endregion
|
|
745
|
+
export { SketchImporter, deserializeElement, exportToFile, importFromFile, importSketch, importSketchFile, parseFileData, restoreScene, restoreScene$1 as restoreSceneBase, serializeScene, serializeSceneToJSON };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SketchLayer } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Extract the image ref path from a bitmap layer and resolve it
|
|
4
|
+
* to a data URL using the images extracted from the zip.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveImageSrc(layer: SketchLayer, images: Map<string, ArrayBuffer>): string | undefined;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Root } from "@fulate/core";
|
|
2
|
+
import type { Importer, ImportResult } from "../types";
|
|
3
|
+
import type { ElementFilter, DeserializeFn } from "../util";
|
|
4
|
+
export declare class SketchImporter implements Importer {
|
|
5
|
+
import(file: File | ArrayBuffer): Promise<ImportResult>;
|
|
6
|
+
}
|
|
7
|
+
export declare function importSketch(file: File | ArrayBuffer): Promise<ImportResult>;
|
|
8
|
+
export declare function importSketchFile(root: Root, deserialize: DeserializeFn, filter?: ElementFilter): Promise<boolean>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SketchColor, SketchStyle } from "./types";
|
|
2
|
+
import type { ShapeOption } from "@fulate/core";
|
|
3
|
+
export declare function sketchColorToCSS(c: SketchColor): string;
|
|
4
|
+
export interface StyleConvertResult {
|
|
5
|
+
props: Partial<ShapeOption>;
|
|
6
|
+
warnings: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function convertStyle(style: SketchStyle | undefined): StyleConvertResult;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
export interface SketchFile {
|
|
2
|
+
document: SketchDocument;
|
|
3
|
+
meta: SketchMeta;
|
|
4
|
+
pages: SketchPage[];
|
|
5
|
+
images: Map<string, ArrayBuffer>;
|
|
6
|
+
}
|
|
7
|
+
export interface SketchDocument {
|
|
8
|
+
_class: "document";
|
|
9
|
+
do_objectID: string;
|
|
10
|
+
assets: SketchAssets;
|
|
11
|
+
colorSpace: number;
|
|
12
|
+
pages: SketchPageRef[];
|
|
13
|
+
}
|
|
14
|
+
export interface SketchPageRef {
|
|
15
|
+
_class: "MSJSONFileReference";
|
|
16
|
+
_ref_class: "MSImmutablePage";
|
|
17
|
+
_ref: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SketchAssets {
|
|
20
|
+
_class: "assetCollection";
|
|
21
|
+
colors: SketchColor[];
|
|
22
|
+
gradients: any[];
|
|
23
|
+
images: any[];
|
|
24
|
+
}
|
|
25
|
+
export interface SketchMeta {
|
|
26
|
+
commit: string;
|
|
27
|
+
appVersion: string;
|
|
28
|
+
build: number;
|
|
29
|
+
app: string;
|
|
30
|
+
pagesAndArtboards: Record<string, {
|
|
31
|
+
name: string;
|
|
32
|
+
artboards: Record<string, {
|
|
33
|
+
name: string;
|
|
34
|
+
}>;
|
|
35
|
+
}>;
|
|
36
|
+
version: number;
|
|
37
|
+
}
|
|
38
|
+
export interface SketchPage {
|
|
39
|
+
_class: "page";
|
|
40
|
+
do_objectID: string;
|
|
41
|
+
name: string;
|
|
42
|
+
layers: SketchLayer[];
|
|
43
|
+
frame: SketchRect;
|
|
44
|
+
}
|
|
45
|
+
export interface SketchLayer {
|
|
46
|
+
_class: string;
|
|
47
|
+
do_objectID: string;
|
|
48
|
+
name: string;
|
|
49
|
+
frame: SketchRect;
|
|
50
|
+
isVisible: boolean;
|
|
51
|
+
rotation: number;
|
|
52
|
+
isFlippedHorizontal: boolean;
|
|
53
|
+
isFlippedVertical: boolean;
|
|
54
|
+
isLocked: boolean;
|
|
55
|
+
style?: SketchStyle;
|
|
56
|
+
layers?: SketchLayer[];
|
|
57
|
+
attributedString?: SketchAttributedString;
|
|
58
|
+
image?: SketchImageRef;
|
|
59
|
+
fixedRadius?: number;
|
|
60
|
+
points?: SketchCurvePoint[];
|
|
61
|
+
isClosed?: boolean;
|
|
62
|
+
booleanOperation?: number;
|
|
63
|
+
hasClippingMask?: boolean;
|
|
64
|
+
shouldBreakMaskChain?: boolean;
|
|
65
|
+
clippingMaskMode?: number;
|
|
66
|
+
}
|
|
67
|
+
export interface SketchRect {
|
|
68
|
+
_class: "rect";
|
|
69
|
+
x: number;
|
|
70
|
+
y: number;
|
|
71
|
+
width: number;
|
|
72
|
+
height: number;
|
|
73
|
+
}
|
|
74
|
+
export interface SketchStyle {
|
|
75
|
+
_class: "style";
|
|
76
|
+
windingRule?: number;
|
|
77
|
+
fills?: SketchFill[];
|
|
78
|
+
borders?: SketchBorder[];
|
|
79
|
+
shadows?: SketchShadow[];
|
|
80
|
+
innerShadows?: SketchShadow[];
|
|
81
|
+
blur?: SketchBlur;
|
|
82
|
+
opacity?: number;
|
|
83
|
+
contextSettings?: SketchGraphicsContextSettings;
|
|
84
|
+
}
|
|
85
|
+
export interface SketchGraphicsContextSettings {
|
|
86
|
+
_class: "graphicsContextSettings";
|
|
87
|
+
blendMode: number;
|
|
88
|
+
opacity: number;
|
|
89
|
+
}
|
|
90
|
+
export interface SketchFill {
|
|
91
|
+
_class: "fill";
|
|
92
|
+
isEnabled: boolean;
|
|
93
|
+
fillType: number;
|
|
94
|
+
color: SketchColor;
|
|
95
|
+
gradient?: SketchGradient;
|
|
96
|
+
}
|
|
97
|
+
export interface SketchBorder {
|
|
98
|
+
_class: "border";
|
|
99
|
+
isEnabled: boolean;
|
|
100
|
+
color: SketchColor;
|
|
101
|
+
thickness: number;
|
|
102
|
+
position: number;
|
|
103
|
+
fillType: number;
|
|
104
|
+
gradient?: SketchGradient;
|
|
105
|
+
}
|
|
106
|
+
export interface SketchShadow {
|
|
107
|
+
_class: "shadow" | "innerShadow";
|
|
108
|
+
isEnabled: boolean;
|
|
109
|
+
color: SketchColor;
|
|
110
|
+
blurRadius: number;
|
|
111
|
+
offsetX: number;
|
|
112
|
+
offsetY: number;
|
|
113
|
+
spread: number;
|
|
114
|
+
}
|
|
115
|
+
export interface SketchBlur {
|
|
116
|
+
_class: "blur";
|
|
117
|
+
isEnabled: boolean;
|
|
118
|
+
type: number;
|
|
119
|
+
radius: number;
|
|
120
|
+
}
|
|
121
|
+
export interface SketchColor {
|
|
122
|
+
_class: "color";
|
|
123
|
+
red: number;
|
|
124
|
+
green: number;
|
|
125
|
+
blue: number;
|
|
126
|
+
alpha: number;
|
|
127
|
+
}
|
|
128
|
+
export interface SketchGradient {
|
|
129
|
+
_class: "gradient";
|
|
130
|
+
gradientType: number;
|
|
131
|
+
from: string;
|
|
132
|
+
to: string;
|
|
133
|
+
stops: SketchGradientStop[];
|
|
134
|
+
}
|
|
135
|
+
export interface SketchGradientStop {
|
|
136
|
+
_class: "gradientStop";
|
|
137
|
+
color: SketchColor;
|
|
138
|
+
position: number;
|
|
139
|
+
}
|
|
140
|
+
export interface SketchAttributedString {
|
|
141
|
+
_class: "attributedString";
|
|
142
|
+
string: string;
|
|
143
|
+
attributes: SketchStringAttribute[];
|
|
144
|
+
}
|
|
145
|
+
export interface SketchStringAttribute {
|
|
146
|
+
_class: "stringAttribute";
|
|
147
|
+
location: number;
|
|
148
|
+
length: number;
|
|
149
|
+
attributes: {
|
|
150
|
+
MSAttributedStringFontAttribute: {
|
|
151
|
+
_class: "fontDescriptor";
|
|
152
|
+
attributes: {
|
|
153
|
+
name: string;
|
|
154
|
+
size: number;
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
MSAttributedStringColorAttribute?: SketchColor;
|
|
158
|
+
paragraphStyle?: {
|
|
159
|
+
_class: "paragraphStyle";
|
|
160
|
+
alignment?: number;
|
|
161
|
+
maximumLineHeight?: number;
|
|
162
|
+
minimumLineHeight?: number;
|
|
163
|
+
};
|
|
164
|
+
kerning?: number;
|
|
165
|
+
textStyleVerticalAlignmentKey?: number;
|
|
166
|
+
underlineStyle?: number;
|
|
167
|
+
strikethroughStyle?: number;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export interface SketchImageRef {
|
|
171
|
+
_class: "MSJSONFileReference" | "MSJSONOriginalDataReference";
|
|
172
|
+
_ref_class: "MSImageData";
|
|
173
|
+
_ref: string;
|
|
174
|
+
}
|
|
175
|
+
export interface SketchCurvePoint {
|
|
176
|
+
_class: "curvePoint";
|
|
177
|
+
point: string;
|
|
178
|
+
curveFrom: string;
|
|
179
|
+
curveTo: string;
|
|
180
|
+
hasCurveFrom?: boolean;
|
|
181
|
+
hasCurveTo?: boolean;
|
|
182
|
+
cornerRadius: number;
|
|
183
|
+
curveMode: number;
|
|
184
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FileData } from "./fulate";
|
|
2
|
+
export interface ImportResult {
|
|
3
|
+
fileData: FileData;
|
|
4
|
+
/** imageRef → data URL */
|
|
5
|
+
images: Map<string, string>;
|
|
6
|
+
warnings: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface Importer {
|
|
9
|
+
import(file: File | ArrayBuffer): Promise<ImportResult>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Root, Element } from "@fulate/core";
|
|
2
|
+
import type { FileData } from "../fulate";
|
|
3
|
+
export type ElementFilter = (element: Element) => boolean;
|
|
4
|
+
export type DeserializeFn = (data: any) => Element;
|
|
5
|
+
export interface RestoreOptions {
|
|
6
|
+
root: Root;
|
|
7
|
+
fileData: FileData;
|
|
8
|
+
deserialize: DeserializeFn;
|
|
9
|
+
/** 决定把反序列化后的元素添加到哪里、怎么添加 */
|
|
10
|
+
append?: (els: Element[], root: Root) => void;
|
|
11
|
+
/** 决定清除哪些旧元素,不传则不清除 */
|
|
12
|
+
filter?: ElementFilter;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 通用场景恢复模板:
|
|
16
|
+
* 1. 应用 root 配置(resize、viewport、Skin)
|
|
17
|
+
* 2. reset viewport
|
|
18
|
+
* 3. 移除旧元素(由 filter 决定)
|
|
19
|
+
* 4. 反序列化并添加 children(由 append 决定)
|
|
20
|
+
* 5. requestRender
|
|
21
|
+
*/
|
|
22
|
+
export declare function restoreScene(options: RestoreOptions): void;
|
|
23
|
+
export declare function pickFile(accept: string): Promise<File | null>;
|
|
24
|
+
export declare function arrayBufferToDataURL(buf: ArrayBuffer, mime: string): string;
|
|
25
|
+
export declare function guessMimeType(path: string): string;
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fulate/import",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "vitest run --config vitest.config.ts",
|
|
19
|
+
"test:watch": "vitest --config vitest.config.ts"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@fulate/share": "*",
|
|
23
|
+
"@fulate/core": "*",
|
|
24
|
+
"@fulate/ui": "*",
|
|
25
|
+
"jszip": "^3.10.1",
|
|
26
|
+
"lodash-es": "^4.17.22"
|
|
27
|
+
}
|
|
28
|
+
}
|