增加 psd2ui 源码

This commit is contained in:
onvia 2023-07-28 14:23:31 +08:00
parent 38357d6d80
commit 9174aea6e1
59 changed files with 6683 additions and 0 deletions

296
psd2ui-tools/README.md Normal file
View File

@ -0,0 +1,296 @@
## 属性
<a href="#Btn"> @Btn | @btn 按钮</a>
<a href="#ProgressBar"> @ProgressBar | @progressBar 进度条</a>
<a href="#Toggle"> @Toggle | @toggle 选项按钮</a>
<a href="#png9"> @.9 九宫格</a>
<a href="#ar"> @ar 锚点</a>
<a href="#size"> @size 尺寸</a>
<a href="#full"> @full 全屏</a>
<a href="#scale"> @scale 缩放</a>
<a href="#ignore"> @ignore | @ig 忽略导出图片和节点</a>
<a href="#ignorenode"> @ignorenode | @ignode 忽略导出节点</a>
<a href="#ignoreimg"> @ignoreimg | @igimg 忽略图片</a>
<a href="#img"> @img 图片选项</a>
<a href="#flip"> @flip 翻转图像</a>
<a href="#flipX"> @flipX 翻转图像 (flip 变种)</a>
<a href="#flipY"> @flipY 翻转图像 (flip 变种)</a>
### 组件
<a id="Btn"></a>
```
@Btn || @btn
作用图层: 所有图层
```
<a id="ProgressBar"></a>
```
@ProgressBar || @progressBar
作用图层: 组图层
@bar
bar 为 ProgressBar 的属性,类型为 Sprite
作用图层: 图像图层
```
<a id="Toggle"></a>
```
@Toggle || @toggle
作用图层: 组图层
@check
check 为 Toggle 的属性,类型为 Sprite
作用图层: 图像图层
```
### Field
<a id="png9"></a>
```
@.9{l:0,r:0,b:0,t:0}
九宫格
作用图层: 图像图层
参数:
l = left
r = right
b = bottom
t = top
ps:
l r 只填写其中一项,则为对称
b t 同上
不填写则默认为 0
```
```
@ar{x:0,y:0}
锚点
作用图层: 所有图层
参数:
参数都为可选
不填写则默认为 0.5
```
<a id="size"></a>
```
@size{w:100,h:100}
节点尺寸 非图片尺寸
作用图层: 所有图层
参数:
w?: 宽
h?: 高
只对填写的参数生效,未填写的则为计算到的值
无参数不生效
```
<a id="full"></a>
```
@full
节点设置为全屏尺寸
作用图层: 组图层
```
<a id="scale"></a>
```
@scale{x:1,y:1}
节点缩放
作用图层: 所有图层
参数:
x?: x 方向
y?: y 方向
只对填写的参数生效,未填写的则为 1
```
<a id="ignore"></a>
```
@ignore
@ig
忽略导出图像和节点
作用图层: 所有图层
```
<a id="ignorenode"></a>
```
@ignorenode
@ignode
忽略导出节点
作用图层: 所有图层
```
<a id="ignoreimg"></a>
```
@ignoreimg
@igimg
忽略导出图像
作用图层: 图像图层
```
<a id="img"></a>
```
@img{name:string,id:number,bind:number}
定制图片
作用图层:图像图层
参数:
id: number 可选 当前文档中图片唯一 id
name: string 可选 导出的图片名
bind: number 可选 绑定 图像 id
```
<a id="flip"></a>
```
@flip{bind: 0, x: 0, y: 0}
镜像图像
作用图层:图像图层
参数:
bind: number 必选 被绑定的图片 需要用 @img{id:number} 做标记
x: 0 | 1, 可选, 1 为 进行 x 方向镜像
y: 0 | 1, 可选, 1 为 进行 y 方向镜像
x,y 都缺省时,默认 x 方向镜像
注意:
@flip 的图层不会导出图像
```
<a id="flipX"></a>
```
@flipX{bind: 0}
flip 的变种 x 方向镜像图像
作用图层:图像图层
参数:
bind: number 必选 被绑定的图片 需要用 @img{id:number} 做标记
注意:
@flipX 的图层不会导出图像
```
<a id="flipY"></a>
```
@flipY{bind: 0}
flip 的变种 y 方向镜像图像vv
作用图层:图像图层
参数:
bind: number 必选 被绑定的图片 需要用 @img{id:number} 做标记
注意:
@flipY 的图层不会导出图像
```
### 说明
多个字段可作用在同一个图层上,按需使用
### 例如
```
节点名@Btn@size{w:100,h:100}
节点名@ar{x:1,y:1}@full@img{name:bg}
```
### 美术
- 智能图层 支持
- 图层样式
- 颜色叠加 图层图层不支持,文本图层支持
- 描边 文本图层支持
工具会把 画布外的图像也导出成图片,需要美术将 画布外 不需要导出的图像处理掉
需要美术将多个碎图组合的图像合并成智能图层使用
### 程序 配置
json 文件key 为组件名val 为 预制体参数
如下:
```
{
"cc.Label": {
"__type__": "e4f88adp3hERoJ48DZ2PSAl",
"_N$file":{
"__uuid__": "803c185c-9442-4b99-af1a-682f877539ab"
},
"_isSystemFontUsed": false,
"isFixNumber": true
}
}
```
##### 特殊配置
```
"textOffsetY":{
"default": -3,
"36": -3
}
textOffsetY label节点 Y 偏移
以字号为 key ,偏移值 为 val
如果没有配置 某些字号,则 使用 default 默认偏移值,如果没有配置 default 偏移为 0
```
### 已知bug
使用 强制导出图片选项时,输入为多个 psd 或含有多个 psd 文件的文件夹时,如果在不同 psd 含有相同 md5 的图像,则会在各自目录下生成相同 uuid 的图片
### CHANGELOG
- 增加只导出 图片功能
- 移除 @mirror 中的参数: {id}
- 移除 @flipX & @flipY 中的参数: {id}
- 使用 @flip 替换 @mirror
- @img 增加 {id, bind} 参数
- 增加 @scale

View File

@ -0,0 +1,9 @@
{
"ver": "1.3.2",
"uuid": "$PREFB_UUID",
"importer": "prefab",
"optimizationPolicy": "AUTO",
"asyncLoadAssets": false,
"readonly": false,
"subMetas": {}
}

View File

@ -0,0 +1,13 @@
{
"ver": "1.1.35",
"importer": "prefab",
"imported": true,
"uuid": "$PREFB_UUID",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "$NODE_NAME"
}
}

View File

@ -0,0 +1,38 @@
{
"ver": "2.3.7",
"uuid": "$SPRITE_FRAME_UUID",
"importer": "texture",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"width": $WIDTH,
"height": $HEIGHT,
"platformSettings": {},
"subMetas": {
"$FILE_NAME": {
"ver": "1.0.6",
"uuid": "$TEXTURE_UUID",
"importer": "sprite-frame",
"rawTextureUuid": "$SPRITE_FRAME_UUID",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": $WIDTH,
"height": $HEIGHT,
"rawWidth": $WIDTH,
"rawHeight": $HEIGHT,
"borderTop": $BORDER_TOP,
"borderBottom": $BORDER_BOTTOM,
"borderLeft": $BORDER_LEFT,
"borderRight": $BORDER_RIGHT,
"subMetas": {}
}
}
}

View File

@ -0,0 +1,74 @@
{
"ver": "1.0.22",
"importer": "image",
"imported": true,
"uuid": "$TEXTURE_UUID",
"files": [
".png",
".json"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "$TEXTURE_UUID@6c48a",
"displayName": "$FILE_NAME",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "$TEXTURE_UUID",
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0,
"isUuid": true
},
"ver": "1.0.21",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "$TEXTURE_UUID@f9941",
"displayName": "$FILE_NAME",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": $WIDTH,
"height": $HEIGHT,
"rawWidth": $WIDTH,
"rawHeight": $HEIGHT,
"borderTop": $BORDER_TOP,
"borderBottom": $BORDER_BOTTOM,
"borderLeft": $BORDER_LEFT,
"borderRight": $BORDER_RIGHT,
"packable": true,
"isUuid": true,
"imageUuidOrDatabaseUri": "$TEXTURE_UUID@6c48a",
"atlasUuid": ""
},
"ver": "1.0.9",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"hasAlpha": true,
"redirect": "$TEXTURE_UUID@f9941"
}
}

1
psd2ui-tools/dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

2960
psd2ui-tools/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
psd2ui-tools/package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "psd2ui",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc -b",
"watch": "tsc -w",
"rollup": "rollup -c",
"test": "node dist/index.js --input ./test/normal-test.psd --cache-remake --output ./out/",
"help": "node dist/index.js --h",
"test-dir": "node dist/index.js --input E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\test --output E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\out --cache E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\cache\\cache.json",
"test-psd": "node dist/index.js --input E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\test\\normal-test.psd --output E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\out --cache E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\cache\\cache.json",
"test-cache": "node dist/index.js --input E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\test\\normal-test.psd --output E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\out --cache E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\cache\\cache.json --project-assets E:\\Demo\\CC249JSTest\\assets --cache-remake",
"test-dir-no-output": "node dist/index.js --input E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\test --cache E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\cache\\cache.json",
"test-psd-no-output": "node dist/index.js --input E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\test\\normal-test.psd --cache E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\cache\\cache.json",
"test-init": "node dist/index.js --init --project-assets E:\\Demo\\CC249JSTest\\assets --cache E:\\Git\\ccc-framework-3d\\tools\\psd2ui\\cache\\cache.json",
"test-png9": "ts-node src/index.ts --engine-version v342 --pinyin --input ./test/png9.psd --output ./out/",
"test-png9-2": "node dist/index.js --engine-version v342 --pinyin --input ./test/png9.psd --output ./out/"
},
"author": "",
"license": "ISC",
"dependencies": {
"ag-psd": "^15.0.0",
"canvas": "^2.10.2",
"fs-extra": "^10.1.0",
"minimist": "^1.2.7",
"pinyin-pro": "^3.16.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.3",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.1.0",
"@rollup/plugin-typescript": "^11.1.2",
"@types/fs-extra": "^9.0.13",
"@types/node": "^18.11.9",
"cross-env": "^7.0.3",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.35.0",
"ts-node": "^10.9.1",
"tslib": "^2.6.0"
}
}

View File

@ -0,0 +1,28 @@
let path = require("path");
const commonjs = require('@rollup/plugin-commonjs')
const typescript = require('rollup-plugin-typescript2')
let resolve = require('@rollup/plugin-node-resolve').default;
let json = require("@rollup/plugin-json").default;
let terser = require("rollup-plugin-terser").terser;
const override = { compilerOptions: { module: 'ESNext' } }
module.exports = {
input: "./src/export.ts",
output: {
file: path.resolve(__dirname, "./dist/index.js"),
sourcemap: false,
format: "umd",
},
plugins: [
typescript({ tsconfig: './tsconfig.json', tsconfigOverride: override }),
json(),
// resolve(),
commonjs(),
terser()
]
}

View File

@ -0,0 +1,6 @@
export enum EditorVersion{
all,
v249,
v342,
}

View File

@ -0,0 +1,171 @@
import 'ag-psd/initialize-canvas'; // only needed for reading image data and thumbnails
import * as psd from 'ag-psd';
import fs from 'fs-extra';
import path from 'path';
import { imageMgr } from './assets-manager/ImageMgr';
import { fileUtils } from './utils/FileUtils';
import { parser } from './Parser';
import { PsdDocument } from './psd/PsdDocument';
import { PsdLayer } from './psd/PsdLayer';
import { PsdGroup } from './psd/PsdGroup';
import { PsdText } from './psd/PsdText';
import { Color } from './values/Color';
interface TextObject{
text: string;
fontSize: number;
color: string;
outlineWidth?: number;
outlineColor?: string;
}
class ExportImageMgr{
textObjects: TextObject[] = [];
test(){
const outDir = path.join(__dirname, "..", "out");
let psdPath = "./test-img-only/境界奖励-优化.psd";
this.parsePsd(psdPath, outDir);
}
async exec(args) {
// 检查参数
if (!this.checkArgs(args)) {
return;
}
// 判断输入是文件夹还是文件
let stat = fs.lstatSync(args.input);
let isDirectory = stat.isDirectory();
if (isDirectory) {
if (!args.output) {
args.output = path.join(args.input, "psd2ui")
}
this.parsePsdDir(args.input, args.output);
} else {
if (!args.output) {
let input_dir = path.dirname(args.input);
args.output = path.join(input_dir, "psd2ui")
}
this.parsePsd(args.input, args.output);
}
}
// 检查参数
checkArgs(args) {
if (!args.input) {
console.error(`请设置 --input`);
return false;
}
if (!fs.existsSync(args.input)) {
console.error(`输入路径不存在: ${args.input}`);
return false;
}
return true;
}
async parsePsdDir(dir: string, outDir: string) {
// 清空目录
fs.emptyDirSync(outDir);
let psds = fileUtils.filterFile(dir, (fileName) => {
let extname = path.extname(fileName);
if (extname == ".psd") {
return true;
}
return false;
});
for (let i = 0; i < psds.length; i++) {
const element = psds[i];
await this.parsePsd(element, outDir);
}
}
async parsePsd(psdPath: string, outDir: string) {
// 每开始一个新的 psd 清理掉上一个 psd 的图
imageMgr.clear();
this.textObjects.length = 0;
console.log(`=========================================`);
console.log(`处理 ${psdPath} 文件`);
let psdName = path.basename(psdPath, ".psd");
let buffer = fs.readFileSync(psdPath);
const psdFile = psd.readPsd(buffer)
let psdRoot = parser.parseLayer(psdFile) as PsdDocument;
psdRoot.name = psdName;
let prefabDir = path.join(outDir, psdName);
let textureDir = path.join(prefabDir, "textures");
fs.mkdirsSync(prefabDir); // 创建预制体根目录
fs.emptyDirSync(prefabDir);
fs.mkdirsSync(textureDir); //创建 图片目录
await this.saveImage(textureDir);
await this.saveTextFile(psdRoot,prefabDir);
console.log(`psd2ui ${psdPath} 处理完成`);
}
saveImage(out: string) {
let images = imageMgr.getAllImage();
let idx = 0;
images.forEach((psdImage, k) => {
// 查找镜像
let _layer = imageMgr.getSerialNumberImage(psdImage);
let name = `${_layer.imgName}_${idx}`
console.log(`保存图片 [${_layer.imgName}] 重命名为 [${name}] md5: ${_layer.md5}`);
let fullpath = path.join(out, `${name}.png`);
fs.writeFileSync(fullpath, _layer.imgBuffer);
idx++;
});
}
saveTextFile(psdRoot: PsdDocument,out: string){
this.scanText(psdRoot,psdRoot);
let textContent = JSON.stringify(this.textObjects,null,2);
let fullpath = path.join(out, `text.txt`);
fs.writeFileSync(fullpath, textContent,{encoding: "utf-8"});
}
scanText(layer: PsdLayer, psdRoot: PsdDocument) {
if (layer instanceof PsdGroup) {
for (let i = 0; i < layer.children.length; i++) {
const childLayer = layer.children[i];
this.scanText(childLayer, psdRoot);
}
} else if (layer instanceof PsdText) {
let textObj: TextObject = {
text: layer.text,
fontSize: layer.fontSize,
color: `#${layer.color.toHEX()}`
};
// 有描边
if (layer.outline) {
textObj.outlineWidth = layer.outline.width;
textObj.outlineColor = `#${layer.outline.color.toHEX()}`;
}
this.textObjects.push(textObj);
}
}
private static _instance:ExportImageMgr = null
public static getInstance(): ExportImageMgr{
if(!this._instance){
this._instance = new ExportImageMgr();
}
return this._instance;
}
}
export let exportImageMgr = ExportImageMgr.getInstance();

507
psd2ui-tools/src/Main.ts Normal file
View File

@ -0,0 +1,507 @@
//ag-psd 使用 参考 https://github.com/Agamnentzar/ag-psd/blob/HEAD/README_PSD.md
import 'ag-psd/initialize-canvas'; // only needed for reading image data and thumbnails
import * as psd from 'ag-psd';
import fs from 'fs-extra';
import path from 'path';
import { parser } from './Parser';
import { PsdDocument } from './psd/PsdDocument';
import { PsdLayer } from './psd/PsdLayer';
import { LayerType } from './psd/LayerType';
import { PsdGroup } from './psd/PsdGroup';
import { CCNode } from './engine/cc/CCNode';
import { PsdImage } from './psd/PsdImage';
import { PsdText } from './psd/PsdText';
import { CCSprite } from './engine/cc/CCSprite';
import { CCPrefabInfo } from './engine/cc/CCPrefabInfo';
import { CCPrefab } from './engine/cc/CCPrefab';
import { CCSize } from './engine/cc/values/CCSize';
import { CCVec2 } from './engine/cc/values/CCVec2';
import { CCComponent } from './engine/cc/CCComponent';
import { CCLabel } from './engine/cc/CCLabel';
import { CCLabelOutline } from './engine/cc/CCLabelOutline';
import { imageCacheMgr } from './assets-manager/ImageCacheMgr';
import { EditorVersion } from './EditorVersion';
import { Config, config } from './config';
import { fileUtils } from './utils/FileUtils';
import { imageMgr } from './assets-manager/ImageMgr';
import { exportImageMgr } from './ExportImageMgr';
import { CCUIOpacity } from './engine/cc/CCUIOpacity';
import { CCUITransform } from './engine/cc/CCUITransform';
import { CCVec3 } from './engine/cc/values/CCVec3';
/***
*
* - md5
*
* -
* - psd
* - md5 使 uuid
*
*/
console.log(`当前目录: `, __dirname);
export class Main {
spriteFrameMetaContent: string = "";
prefabMetaContent: string = "";
psdConfig: Config = null;
// 强制导出图片
isForceImg = false;
async test() {
console.log(`Main-> test`);
}
// 首先加载 meta 模板
async loadMetaTemplete() {
this.spriteFrameMetaContent = fs.readFileSync(path.join(__dirname, `../assets/cc/meta/CCSpriteFrame.meta.${EditorVersion[config.editorVersion]}`), "utf-8");
this.prefabMetaContent = fs.readFileSync(path.join(__dirname, `../assets/cc/meta/CCPrefab.meta.${EditorVersion[config.editorVersion]}`), "utf-8");
}
// 加载配置
async loadPsdConfig(filepath) {
if (!fs.existsSync(filepath)) {
console.log(`Main-> 配置 ${filepath} 不存在`);
return;
}
let psdConfig = fs.readFileSync(filepath, "utf-8");
this.psdConfig = JSON.parse(psdConfig);
// 合并配置
for (const key in this.psdConfig) {
if (key in config) {
if (typeof this.psdConfig[key] === 'object') {
config[key] = Object.assign({}, config[key], this.psdConfig[key]);
} else {
config[key] = this.psdConfig[key] || config[key];
}
}
}
}
async exec(args) {
args = mergeAlias(args);
if (args.help) {
console.log(`help:\n`, config.help);
return false;
}
// 只导出图片
if (args["img-only"]) {
exportImageMgr.exec(args);
return true;
}
let writeCache = async () => {
// 写入缓存
if (args.cache) {
fs.mkdirsSync(path.dirname(args.cache));
await imageCacheMgr.saveImageMap(args.cache);
}
}
// 设置引擎版本
if (args["engine-version"]) {
config.editorVersion = EditorVersion[args["engine-version"] as string];
}
console.log(`Main-> 数据版本 ${EditorVersion[config.editorVersion]}`);
if (args.init && (!args["project-assets"] || !args.cache)) {
console.log(`psd2ui --init 无法处理,请设置 --project-assets`);
return;
}
// 创建缓存文件
if (args.cache && !fs.existsSync(args.cache)) {
writeCache();
}
// 在没有缓存文件或者 指定重新缓存的时候,读取项目资源
if (args["project-assets"] && (args["cache-remake"] || args.init)) {
await imageCacheMgr.loadImages(args["project-assets"]);
// 先写入一次
writeCache();
if (args.init) {
console.log(`psd2ui 缓存完成`);
return;
}
}
// 检查参数
if (!this.checkArgs(args)) {
return;
}
if (args.cache) {
await imageCacheMgr.initWithPath(args.cache);
}
// 加载 meta 文件模板
await this.loadMetaTemplete();
if (args.config) {
await this.loadPsdConfig(args.config);
}
this.isForceImg = !!args["force-img"];
PsdLayer.isPinyin = args.pinyin;
// 判断输入是文件夹还是文件
let stat = fs.lstatSync(args.input);
let isDirectory = stat.isDirectory();
if (isDirectory) {
if (!args.output) {
args.output = path.join(args.input, "psd2ui")
}
this.parsePsdDir(args.input, args.output);
} else {
if (!args.output) {
let input_dir = path.dirname(args.input);
args.output = path.join(input_dir, "psd2ui")
}
this.parsePsd(args.input, args.output);
}
// 写入缓存
await writeCache();
console.log(`psd2ui 导出完成`);
}
// 检查参数
checkArgs(args) {
if (!args.input) {
console.error(`请设置 --input`);
return false;
}
if (!fs.existsSync(args.input)) {
console.error(`输入路径不存在: ${args.input}`);
return false;
}
if (args["engine-version"]) {
let editorVersion = EditorVersion[args["engine-version"] as string];
switch (editorVersion) {
case EditorVersion.v249:
case EditorVersion.v342:
break;
default:
console.log(`暂未实现该引擎版本 ${args["engine-version"]}`);
return false;
}
}
return true;
}
async parsePsdDir(dir: string, outDir: string) {
// 清空目录
// fs.emptyDirSync(outDir);
let psds = fileUtils.filterFile(dir, (fileName) => {
let extname = path.extname(fileName);
if (extname == ".psd") {
return true;
}
return false;
});
for (let i = 0; i < psds.length; i++) {
const element = psds[i];
await this.parsePsd(element, outDir);
}
}
async parsePsd(psdPath: string, outDir: string) {
// 每开始一个新的 psd 清理掉上一个 psd 的图
imageMgr.clear();
console.log(`=========================================`);
console.log(`处理 ${psdPath} 文件`);
let psdName = path.basename(psdPath, ".psd");
let buffer = fs.readFileSync(psdPath);
const psdFile = psd.readPsd(buffer)
let psdRoot = parser.parseLayer(psdFile) as PsdDocument;
psdRoot.name = psdName;
let prefabDir = path.join(outDir, psdName);
let textureDir = path.join(prefabDir, "textures");
fs.mkdirsSync(prefabDir); // 创建预制体根目录
// fs.emptyDirSync(prefabDir);
fs.mkdirsSync(textureDir); //创建 图片目录
await this.saveImage(textureDir);
await this.buildPrefab(psdRoot);
await this.savePrefab(psdRoot, prefabDir);
console.log(`psd2ui ${psdPath} 处理完成`);
}
buildPrefab(psdRoot: PsdDocument) {
let prefab = new CCPrefab();
psdRoot.pushObject(prefab);
let data = this.createCCNode(psdRoot, psdRoot);
prefab.data = { __id__: data.idx };
// 后期处理
this.postUIObject(psdRoot, psdRoot);
}
createCCNode(layer: PsdLayer, psdRoot: PsdDocument) {
let node = new CCNode(psdRoot);
layer.uiObject = node;
node._name = layer.name; //layer.attr?.name || layer.name;
node._active = !layer.hidden;
node._opacity = layer.opacity;
if (config.editorVersion >= EditorVersion.v342) {
// 3.4.x
if (layer.opacity !== 255) {
let uiOpacity = new CCUIOpacity();
uiOpacity._opacity = layer.opacity;
uiOpacity.updateWithLayer(layer);
node.addComponent(uiOpacity);
}
}
// 劫持尺寸设置,使用 psd 中配置的尺寸,这里不对原数据进行修改
let size = new CCSize(layer.size.width, layer.size.height);
if (layer.attr?.comps.size) {
let _attrSize = layer.attr.comps.size;
size.width = _attrSize.w ?? size.width;
size.height = _attrSize.h ?? size.height;
}
// 对缩放进行处理
size.width = Math.round(Math.abs(size.width / layer.scale.x));
size.height = Math.round(Math.abs(size.height / layer.scale.y));
// 配置的位置 Y 偏移
let offsetY = 0;
if (layer instanceof PsdText) {
offsetY = layer.offsetY;
}
node._contentSize = size;
// 更新一下位置 // 根据图层名字设置 锚点,位置, 因为没有对原始数据进行修改,所以这里不考虑 缩放
layer.updatePositionWithAR();
// 2.4.9
node._trs.setPosition(layer.position.x, layer.position.y + offsetY, 0);
node._trs.setRotation(0, 0, 0, 1);
node._trs.setScale(layer.scale.x, layer.scale.y, layer.scale.z);
node._anchorPoint = new CCVec2(layer.anchorPoint.x, layer.anchorPoint.y);
if (config.editorVersion >= EditorVersion.v342) {
// 3.4.x
node._lpos = new CCVec3(layer.position.x, layer.position.y + offsetY, 0);
node._lrot = new CCVec3(0, 0, 0);
node._lscale = new CCVec3(layer.scale.x, layer.scale.y, layer.scale.z);
node._euler = new CCVec3();
// 3.4.x
let uiTransform = new CCUITransform();
uiTransform._contentSize = size;
uiTransform._anchorPoint = node._anchorPoint;
uiTransform.updateWithLayer(layer);
node.addComponent(uiTransform);
}
//
if (layer instanceof PsdGroup) {
for (let i = 0; i < layer.children.length; i++) {
const childLayer = layer.children[i];
let childNode = this.createCCNode(childLayer, psdRoot);
childNode && node.addChild(childNode);
}
} else if (layer instanceof PsdImage) {
let sprite = new CCSprite();
node.addComponent(sprite);
sprite._materials.push({
__uuid__: config.SpriteFrame_Material
});
sprite.updateWithLayer(layer);
if (layer.isIgnore()) {
// 忽略图像
} else {
// 查找绑定的图像
let _layer = imageMgr.getSerialNumberImage(layer);
// 使用已缓存的 图片 的 uuid
let imageWarp = imageCacheMgr.get(_layer.md5);
sprite.setSpriteFrame(imageWarp ? imageWarp.textureUuid : _layer.textureUuid);
}
this.applyConfig(sprite);
} else if (layer instanceof PsdText) {
let label = new CCLabel();
node.addComponent(label);
node._color.set(layer.color);
label._color.set(layer.color);
label._materials.push({
__uuid__: config.Label_Material
});
label.updateWithLayer(layer);
this.applyConfig(label);
// 有描边
if (layer.outline) {
let labelOutline = new CCLabelOutline();
node.addComponent(labelOutline);
labelOutline.updateWithLayer(layer);
this.applyConfig(labelOutline);
}
}
// Button / Toggle / ProgressBar
if (layer.attr) {
for (const key in layer.attr.comps) {
if (Object.prototype.hasOwnProperty.call(layer.attr.comps, key) && layer.attr.comps[key]) {
let ctor = config.CompMappings[key] as any;
if (ctor) {
let comp: CCComponent = new ctor();
node.addComponent(comp);
comp.updateWithLayer(layer);
this.applyConfig(comp);
}
}
}
}
this.createPrefabInfo(layer, psdRoot);
return node;
}
createPrefabInfo(layer: PsdLayer, psdRoot: PsdDocument) {
let node = layer.uiObject as CCNode;
let prefabInfo = new CCPrefabInfo();
let idx = psdRoot.pushObject(prefabInfo);
node._prefab = { __id__: idx };
}
// 后处理
postUIObject(layer: PsdLayer, psdRoot: PsdDocument) {
}
saveImage(out: string) {
let images = imageMgr.getAllImage();
images.forEach((psdImage, k) => {
// 查找镜像
let _layer = imageMgr.getSerialNumberImage(psdImage);
// 查找已缓存的相同图像
let imageWarp = imageCacheMgr.get(_layer.md5);
// 不是强制导出的话,判断是否已经导出过
if (!this.isForceImg) {
// 判断是否已经导出过相同 md5 的资源,不再重复导出
if (imageWarp?.isOutput) {
console.log(`已有相同资源,不再导出 [${psdImage.imgName}] md5: ${psdImage.md5}`);
return;
}
}
console.log(`保存图片 [${_layer.imgName}] md5: ${_layer.md5}`);
imageWarp && (imageWarp.isOutput = true);
let fullpath = path.join(out, `${_layer.imgName}.png`);
fs.writeFileSync(fullpath, _layer.imgBuffer);
this.saveImageMeta(_layer, fullpath);
});
}
saveImageMeta(layer: PsdImage, fullpath: string) {
let _layer = imageMgr.getSerialNumberImage(layer);
let imageWarp = imageCacheMgr.get(_layer.md5);
if (!imageWarp) {
imageWarp = _layer;
}
// 2.4.9 =-> SPRITE_FRAME_UUID
let meta = this.spriteFrameMetaContent.replace(/\$SPRITE_FRAME_UUID/g, imageWarp.uuid)
meta = meta.replace(/\$TEXTURE_UUID/g, imageWarp.textureUuid);
meta = meta.replace(/\$FILE_NAME/g, _layer.imgName);
meta = meta.replace(/\$WIDTH/g, _layer.textureSize.width as any);
meta = meta.replace(/\$HEIGHT/g, _layer.textureSize.height as any);
let s9 = _layer.s9 || {
b: 0, t: 0, l: 0, r: 0,
};
meta = meta.replace(/\$BORDER_TOP/g, s9.t as any);
meta = meta.replace(/\$BORDER_BOTTOM/g, s9.b as any);
meta = meta.replace(/\$BORDER_LEFT/g, s9.l as any);
meta = meta.replace(/\$BORDER_RIGHT/g, s9.r as any);
fs.writeFileSync(fullpath + `.meta`, meta);
}
savePrefab(psdDoc: PsdDocument, out) {
let fullpath = path.join(out, `${psdDoc.name}.prefab`);
fs.writeFileSync(fullpath, JSON.stringify(psdDoc.objectArray, null, 2));
this.savePrefabMeta(psdDoc, fullpath);
}
savePrefabMeta(psdDoc: PsdDocument, fullpath) {
let meta = this.prefabMetaContent.replace(/\$PREFB_UUID/g, psdDoc.uuid)
fs.writeFileSync(fullpath + `.meta`, meta);
}
applyConfig(comp: CCComponent) {
if (!this.psdConfig) {
return;
}
if (comp.__type__ in this.psdConfig) {
let compConfig = this.psdConfig[comp.__type__];
for (const key in compConfig) {
if (Object.prototype.hasOwnProperty.call(compConfig, key)) {
const element = compConfig[key];
comp[key] = element;
}
}
}
}
}
/** 合并别名 */
function mergeAlias(args) {
// 如果是 json 对象参数
if (args.json) {
let base64 = args.json;
// 解码 json
args = JSON.parse(Buffer.from(base64, "base64").toString());
// // 编码
// let jsonContent = JSON.stringify(args);
// let base64 = Buffer.from(jsonContent).toString("base64");
}
args.help = args.help || args.h;
args.input = args.input || args.in;
args.output = args.output || args.out;
args["engine-version"] = args["engine-version"] || args.ev;
args["project-assets"] = args["project-assets"] || args.p;
args["cache-remake"] = args["cache-remake"] || args.crm;
args["force-img"] = args["force-img"] || args.fimg;
args.pinyin = args.pinyin || args.py;
args.cache = args.cache || args.c;
args.init = args.init || args.i;
args.config = args.config;
return args;
}

107
psd2ui-tools/src/Parser.ts Normal file
View File

@ -0,0 +1,107 @@
import { imageCacheMgr } from "./assets-manager/ImageCacheMgr";
import { imageMgr } from "./assets-manager/ImageMgr";
import { LayerType } from "./psd/LayerType";
import { PsdDocument } from "./psd/PsdDocument";
import { PsdGroup } from "./psd/PsdGroup";
import { PsdImage } from "./psd/PsdImage";
import { PsdAttr, PsdLayer } from "./psd/PsdLayer";
import { PsdText } from "./psd/PsdText";
import { PsdLayerSource } from "./_declare";
export class Parser {
/** 解析图层类型 */
parseLayerType(source: PsdLayerSource) {
if ("children" in source) {
if ("width" in source && "height" in source) {
// Document
return LayerType.Doc;
} else {
// Group
return LayerType.Group;
}
} else if ("text" in source) {
// Text
return LayerType.Text;
}
// else if ('placedLayer' in layer) {
// // 智能对象
// }
return LayerType.Image;
}
parseLayer(source: any, parent?: PsdGroup, rootDoc?: PsdDocument) {
let layer: PsdLayer = null;
let layerType = this.parseLayerType(source);
switch (layerType) {
case LayerType.Doc:
case LayerType.Group: {
let group: PsdGroup = null
// Group
if (layerType == LayerType.Group) {
group = new PsdGroup(source, parent, rootDoc);
if(group.attr.comps.ignorenode || group.attr.comps.ignore){
return null;
}
} else {
// Document
group = new PsdDocument(source);
}
for (let i = 0; i < source.children.length; i++) {
const childSource = source.children[i];
let child = this.parseLayer(childSource, group, rootDoc || group as PsdDocument);
if (child) {
if (!child.attr.comps.ignorenode && !child.attr.comps.ignore) {
// 没有进行忽略节点的时候才放入列表
group.children.push(child);
}
} else {
console.error(`图层解析错误`);
}
}
layer = group;
}
break;
case LayerType.Image: {
//
if (!source.canvas) {
console.error(`Parser-> 空图层 ${source?.name}`);
return null;
}
// Image
let image = layer = new PsdImage(source, parent, rootDoc);
imageMgr.add(image);
// 没有设置忽略且不说镜像的情况下才进行缓存
if (!image.isIgnore() && ! image.isBind()) {
if (!imageCacheMgr.has(image.md5)) {
imageCacheMgr.set(image.md5, {
uuid: image.uuid,
textureUuid: image.textureUuid,
});
}
}
}
break;
case LayerType.Text: {
// Text
layer = new PsdText(source, parent, rootDoc);
}
break;
default:
break;
}
layer.layerType = layerType;
layer.parseSource();
layer.onCtor();
return layer;
}
}
export const parser = new Parser();

View File

@ -0,0 +1,6 @@
export interface PsdLayerSource {
[k: string]: any;
}

View File

@ -0,0 +1,137 @@
import { config } from "./config";
import { EditorVersion } from "./EditorVersion";
import { UIObject } from "./engine/UIObject";
/** 禁止序列化 */
export let nonserialization = (target: any,propertyKey: string)=>{
if(!target.__unserialization){
target.__unserialization = [];
}
target.__unserialization.push(propertyKey);
// if(!target.toJSON){
// // JSON.stringify 自动调用
// target.toJSON = function(){
// let data:Record<any,any> = {};
// for (const key in this) {
// if (Object.prototype.hasOwnProperty.call(this, key)) {
// // @ts-ignore
// if(this.__unserialization.indexOf(key) !== -1){
// continue;
// }
// // 判断编辑器版本
// if(this._version && !this._version[key][EditorVersion[config.editorVersion]]){
// continue;
// }
// const value = this[key];
// data[key] = value;
// }
// }
// return data;
// }
// }
}
export function cctype(type: string){
return (target: Function) => {
Object.defineProperty(target.prototype,"$__type__",{
value: type,
enumerable: true,
})
}
}
let _extends = {};
let _class_attrs = {};
let _target_map_ = {};
let __verIdx = 0;
let _printID = -1;
function checkTag(target){
if(target.constructor.__ver_tag_id__ === undefined || _target_map_[target.constructor.__ver_tag_id__] != target){
target.constructor.__ver_tag_id__ = `${__verIdx}`;
_target_map_[target.constructor.__ver_tag_id__] = target;
__verIdx ++;
}
return target.constructor.__ver_tag_id__;
}
function _assign(target,source){
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if(key in target){
continue;
}
target[key] = source[key];
}
}
}
function assign(target,... sources){
for (let i = 0; i < sources.length; i++) {
_assign(target,sources[i]);
}
}
export function ccversion(version: number){
return (target: any,propertyKey: string)=>{
let _class_name_ = target.constructor.name;
_class_name_ = checkTag(target);
!_class_attrs[_class_name_] && (_class_attrs[_class_name_] = {});
let _class_obj = _class_attrs[_class_name_];
if(!_class_obj[propertyKey]){
_class_obj[propertyKey] = {};
}
if(EditorVersion.all === version){
for (const key in EditorVersion) {
_class_obj[propertyKey][EditorVersion[key]] = true;
}
}else{
_class_obj[propertyKey][EditorVersion[version]] = true;
}
var base = getSuper(target.constructor);
// (base === Object || base === UIObject) && (base = null);
if(base){
let parent = checkTag(base.prototype);
!_extends[_class_name_] && (_extends[_class_name_] = parent);
var _super = getSuper(base);
let superIdx = 1;
while (_super) {
// if(_super === Object || _super === UIObject) {
// // _super = null;
// break;
// }
let super_tag = checkTag(_super.prototype);
!_extends[parent] && (_extends[parent] = super_tag);
_super = getSuper(_super);
superIdx++;
}
while (parent) {
if(parent in _class_attrs){
assign(_class_obj,_class_attrs[parent]);
}
parent = _extends[parent];
}
}
if(!target._version){
target._version = {};
}
target._version[_class_name_] = _class_attrs[_class_name_] = _class_obj;
}
}
function getSuper (ctor) {
var proto = ctor.prototype; // binded function do not have prototype
var dunderProto = proto && Object.getPrototypeOf(proto);
return dunderProto && dunderProto.constructor;
}

View File

@ -0,0 +1,165 @@
import fs from "fs-extra";
import path from "path";
import { config } from "../config";
import { EditorVersion } from "../EditorVersion";
import { fileUtils } from "../utils/FileUtils";
export interface ImageWarp {
path?: string;
uuid: string;
textureUuid: string;
isOutput?: boolean
}
export class ImageCacheMgr {
private _imageMap: Map<string, ImageWarp> = new Map();
private _cachePath: string = null;
initWithPath(_path: string) {
if (!fs.existsSync(_path)) {
console.log(`ImageCacheMgr-> 文件不存在: ${_path}`);
return;
}
this._cachePath = _path;
let content = fs.readFileSync(_path, "utf-8");
this.initWithFile(content);
}
initWithFile(file: string) {
let json = JSON.parse(file);
this.initWithJson(json);
}
initWithJson(json: any) {
for (const key in json) {
if (Object.prototype.hasOwnProperty.call(json, key)) {
this._imageMap.set(key, json[key]);
}
}
}
set(md5: string, warp: ImageWarp) {
this._imageMap.set(md5, warp);
}
has(md5: string): boolean {
return this._imageMap.has(md5);
}
get(md5: string): ImageWarp {
return this._imageMap.get(md5);
}
async saveImageMap(_path?: string) {
if (!_path) {
_path = this._cachePath;
}
if (!_path) {
console.log(`ImageCacheMgr-> 缓存路径 [${_path}] 不存在,无法保存 `);
return;
}
let obj = Object.create(null);
this._imageMap.forEach((v, k) => {
obj[k] = v;
});
let content = JSON.stringify(obj, null, 2);
await fileUtils.writeFile(_path, content);
}
// 获取已存在的图片,生成 md5: uuid 映射,
loadImages(dir: string) {
if (this._imageMap.size > 0) {
console.error(`ImageCacheMgr-> 暂时只能在 启动时加载`);
return;
}
let pngs = fileUtils.filterFile(dir, (fileName) => {
let extname = path.extname(fileName);
if (extname == ".png") {
return true;
}
return false;
});
if(!pngs){
return;
}
for (let i = 0; i < pngs.length; i++) {
const png = pngs[i];
let md5 = fileUtils.getMD5(png);
console.log(`ImageCacheMgr->缓存 `, png);
let imageWarp = this._loadImageMetaWarp(`${png}.meta`);
if (imageWarp) {
this.set(md5, imageWarp);
}
}
}
private _loadImageMetaWarp(_path: string) {
let content = fs.readFileSync(_path, { encoding: "utf-8" });
let imageWarp: ImageWarp = null;
switch (config.editorVersion) {
case EditorVersion.v249:
imageWarp = this._loadImageMeta249(content, _path);
break;
case EditorVersion.v342:
imageWarp = this._loadImageMeta34x(content, _path);
break;
default:
console.log(`ImageCacheMgr-> 暂未实现 ${EditorVersion[config.editorVersion]} 版本`);
break;
}
return imageWarp;
}
private _loadImageMeta249(metaContent: any, _path: string) {
let filename = path.basename(_path, ".png.meta");
let fullpath = path.join(path.dirname(_path), `${filename}.png`);
let metaJson = JSON.parse(metaContent);
if (!metaJson?.subMetas?.[filename]) {
return null;
}
let imageWarp: ImageWarp = {
path: fullpath,
textureUuid: metaJson.subMetas[filename].uuid,
uuid: metaJson.uuid,
isOutput: true,
}
return imageWarp;
}
private _loadImageMeta34x(metaContent: any, _path: string) {
let filename = path.basename(_path, ".png.meta");
let fullpath = path.join(path.dirname(_path), `${filename}.png`);
let metaJson = JSON.parse(metaContent);
if (!metaJson?.subMetas?.["6c48a"]) {
return null;
}
let uuid = metaJson.subMetas["6c48a"].uuid.replace("@6c48a", "");
let imageWarp: ImageWarp = {
path: fullpath,
textureUuid: uuid,
uuid: uuid,
isOutput: true,
}
return imageWarp;
}
private static _instance: ImageCacheMgr = null
public static getInstance(): ImageCacheMgr {
if (!this._instance) {
this._instance = new ImageCacheMgr();
}
return this._instance;
}
}
export const imageCacheMgr = ImageCacheMgr.getInstance();

View File

@ -0,0 +1,59 @@
import { PsdImage } from "../psd/PsdImage";
class ImageMgr{
// 镜像图像管理
private _imageIdKeyMap: Map<number,PsdImage> = new Map();
// 当前 psd 所有的图片
private _imageArray: Map<string,PsdImage> = new Map();
add(psdImage: PsdImage){
// 不忽略导出图片
if(!psdImage.isIgnore() && !psdImage.isBind()){
if(!this._imageArray.has(psdImage.md5)){
this._imageArray.set(psdImage.md5,psdImage);
}
}
if(typeof psdImage.attr.comps.img?.id != "undefined"){
let id = psdImage.attr.comps.img.id;
if(this._imageIdKeyMap.has(id)){
console.warn(`ImageMgr-> ${psdImage.source.name} 已有相同 @img{id:${id}},请检查 psd 图层`);
}
this._imageIdKeyMap.set(id,psdImage);
}
}
getAllImage(){
return this._imageArray;
}
/** 尝试获取有编号的图像图层 */
getSerialNumberImage(psdImage: PsdImage){
let bind = psdImage.attr.comps.flip?.bind ?? psdImage.attr.comps.img?.bind;
if(typeof bind != 'undefined'){
if(this._imageIdKeyMap.has(bind)){
return this._imageIdKeyMap.get(bind)
}else{
console.warn(`ImageMgr-> ${psdImage.source.name} 未找到绑定的图像 {${bind}},请检查 psd 图层`);
}
}
return psdImage;
}
clear(){
this._imageIdKeyMap.clear();
this._imageArray.clear()
}
private static _instance:ImageMgr = null
public static getInstance(): ImageMgr{
if(!this._instance){
this._instance = new ImageMgr();
}
return this._instance;
}
}
export const imageMgr = ImageMgr.getInstance();

View File

@ -0,0 +1,60 @@
import { EditorVersion } from "./EditorVersion";
import { CCButton } from "./engine/cc/CCButton";
import { CCComponent } from "./engine/cc/CCComponent";
import { CCProgressBar } from "./engine/cc/CCProgressBar";
import { CCToggle } from "./engine/cc/CCToggle";
export class Config {
readonly help = `
--help |
--init | --project-assets --cache
--force-img | 使
--input | psd init [dir or psd]
--output | --input [dir]
--engine-version | [v249 | v342]
--project-assets | [dir]
--cache-remake |
--cache | [file-full-path]
--config | [file-full-path]
--pinyin |
--img-only |
--json | json 使 base64
`
editorVersion: EditorVersion = EditorVersion.v249;
DEFAULT_SPRITE_FRAME_MATERIAL = {
[EditorVersion.v249]: "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432",
[EditorVersion.v342]: "",
}
DEFAULT_LABEL_MATERIAL = {
[EditorVersion.v249]: "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432",
[EditorVersion.v342]: "",
}
get SpriteFrame_Material() {
return this.DEFAULT_SPRITE_FRAME_MATERIAL[config.editorVersion];
}
get Label_Material() {
return this.DEFAULT_LABEL_MATERIAL[config.editorVersion];
}
CompMappings: Record<string, typeof CCComponent> = {
"Btn": CCButton,
"ProgressBar": CCProgressBar,
"Toggle": CCToggle,
}
// text 文本 Y 偏移
textOffsetY = {
default: 0,
"36": 0,
}
// text 文本 行高偏移,默认为 0 ,行高默认为 字体大小
textLineHeightOffset = 0;
}
export const config = new Config();

View File

@ -0,0 +1,44 @@
import { config } from "../config";
import { EditorVersion } from "../EditorVersion";
import { utils } from "../utils/Utils";
import { nonserialization } from "../_decorator";
export class UIObject{
@nonserialization
uuid: string = "";
@nonserialization
idx: number = 0;
constructor(){
this.uuid = utils.uuid();
}
toJSON(){
let data:Record<any,any> = {};
for (const key in this) {
if (Object.prototype.hasOwnProperty.call(this, key)) {
// @ts-ignore
if(this.__unserialization && this.__unserialization.indexOf(key) !== -1){
continue;
}
// @ts-ignore
let ver_tag = this.constructor.__ver_tag_id__;
// 判断编辑器版本
// @ts-ignore
if(this._version && this._version[ver_tag]?.[key]){
// @ts-ignore
if(!this._version[ver_tag][key][EditorVersion[config.editorVersion]]){
continue;
}
}
const value = this[key];
data[key] = value;
}
}
return data;
}
}

View File

@ -0,0 +1,54 @@
import { EditorVersion } from "../../EditorVersion";
import { PsdLayer } from "../../psd/PsdLayer";
import { cctype, ccversion } from "../../_decorator";
import { CCComponent } from "./CCComponent";
import { CCIDObject } from "./CCObject";
@cctype("cc.Button")
export class CCButton extends CCComponent{
// 2.4.x
@ccversion(EditorVersion.v249)
duration: number = 0.1;
// 2.4.x
@ccversion(EditorVersion.v249)
zoomScale: number = 1.2;
@ccversion(EditorVersion.all)
clickEvents = [];
// 2.4.x
@ccversion(EditorVersion.v249)
_N$interactable: boolean = true;
// 2.4.x
@ccversion(EditorVersion.v249)
_N$enableAutoGrayEffect: boolean = false;
// 2.4.x
@ccversion(EditorVersion.v249)
_N$transition: number = 3;
// 2.4.x
@ccversion(EditorVersion.v249)
transition: number = 3;
// 2.4.x
@ccversion(EditorVersion.v249)
_N$target: CCIDObject = null;
// 3.4.x
@ccversion(EditorVersion.v342)
_interactable = true;
// 3.4.x
@ccversion(EditorVersion.v342)
_transition = 3;
// 3.4.x
@ccversion(EditorVersion.v342)
_duration = 0.1;
// 3.4.x
@ccversion(EditorVersion.v342)
_zoomScale = 1.2;
// 3.4.x
@ccversion(EditorVersion.v342)
_target: CCIDObject = null;
updateWithLayer(psdLayer: PsdLayer) {
}
}

View File

@ -0,0 +1,16 @@
import { utils } from "../../utils/Utils";
import { cctype, nonserialization } from "../../_decorator";
import { UIObject } from "../UIObject";
import { CCIDObject } from "./CCObject";
// @cctype("cc.CompPrefabInfo")
export class CCCompPrefabInfo extends UIObject{
__type__: string = "cc.CompPrefabInfo";
fileId: string = "";
constructor(){
super();
this.fileId = utils.compressUuid(this.uuid);
}
}

View File

@ -0,0 +1,22 @@
import { EditorVersion } from "../../EditorVersion";
import { PsdLayer } from "../../psd/PsdLayer";
import { ccversion, nonserialization } from "../../_decorator";
import { CCIDObject, CCObject } from "./CCObject";
export abstract class CCComponent extends CCObject{
@ccversion(EditorVersion.all)
_enabled: boolean = true;
@ccversion(EditorVersion.all)
node: CCIDObject = null;
@ccversion(EditorVersion.all)
_id: string = "";
// 3.4.x
@ccversion(EditorVersion.v342)
__prefab: CCIDObject = null;
abstract updateWithLayer(psdLayer: PsdLayer);
}

View File

@ -0,0 +1,112 @@
import { config } from "../../config";
import { EditorVersion } from "../../EditorVersion";
import { PsdLayer } from "../../psd/PsdLayer";
import { PsdText } from "../../psd/PsdText";
import { cctype, ccversion } from "../../_decorator";
import { CCComponent } from "./CCComponent";
import { CCUUIDObject } from "./CCObject";
import { CCColor } from "./values/CCColor";
@cctype("cc.Label")
export class CCLabel extends CCComponent{
@ccversion(EditorVersion.all)
_srcBlendFactor: number = 770; // 3.4.x = 2
@ccversion(EditorVersion.all)
_dstBlendFactor: number = 771; // 3.4.x = 4
@ccversion(EditorVersion.all)
_string: string = "";
@ccversion(EditorVersion.all)
_fontSize: number = 0;
@ccversion(EditorVersion.all)
_lineHeight: number = 0;
@ccversion(EditorVersion.all)
_enableWrapText: boolean = true;
@ccversion(EditorVersion.all)
_isSystemFontUsed: boolean = true;
@ccversion(EditorVersion.all)
_spacingX: number = 0;
@ccversion(EditorVersion.all)
_underlineHeight: number = 0;
@ccversion(EditorVersion.v249)
_materials: CCUUIDObject[] = [];
// 2.4.x
@ccversion(EditorVersion.v249)
_N$string: string = "";
// 2.4.x
@ccversion(EditorVersion.v249)
_N$file: any = null;
// 2.4.x
@ccversion(EditorVersion.v249)
_batchAsBitmap: boolean = false;
// 2.4.x
@ccversion(EditorVersion.v249)
_styleFlags: number = 0;
// 2.4.x
@ccversion(EditorVersion.v249)
_N$horizontalAlign: number = 1;
// 2.4.x
@ccversion(EditorVersion.v249)
_N$verticalAlign: number = 1;
// 2.4.x
@ccversion(EditorVersion.v249)
_N$fontFamily: string = "Arial";
// 2.4.x
@ccversion(EditorVersion.v249)
_N$overflow: number = 0;
// 2.4.x
@ccversion(EditorVersion.v249)
_N$cacheMode: number = 0;
// 3.4.x
@ccversion(EditorVersion.v342)
_visFlags: number = 0;
// 3.4.x
@ccversion(EditorVersion.v342)
_customMaterial: any = null;
// 3.4.x
@ccversion(EditorVersion.v342)
_color: CCColor = new CCColor(255,255,255,255);
// 3.4.x
@ccversion(EditorVersion.v342)
_overflow: number = 0;
// // 3.4.x
@ccversion(EditorVersion.v342)
_cacheMode = 0;
@ccversion(EditorVersion.v342)
_horizontalAlign = 1;
@ccversion(EditorVersion.v342)
_verticalAlign = 1;
@ccversion(EditorVersion.v342)
_actualFontSize = 0;
@ccversion(EditorVersion.v342)
_isItalic = false;
@ccversion(EditorVersion.v342)
_isBold = false;
@ccversion(EditorVersion.v342)
_isUnderline = false;
updateWithLayer(psdLayer: PsdText) {
this._fontSize = psdLayer.fontSize;
// this._actualFontSize = this._fontSize;
this._string = this._N$string = psdLayer.text;
this._lineHeight = this._fontSize + config.textLineHeightOffset;
if(config.editorVersion >= EditorVersion.v342){
this._srcBlendFactor = 2;
this._dstBlendFactor = 4;
}
}
}

View File

@ -0,0 +1,22 @@
import { EditorVersion } from "../../EditorVersion";
import { PsdText } from "../../psd/PsdText";
import { cctype, ccversion } from "../../_decorator";
import { CCComponent } from "./CCComponent";
import { CCUUIDObject } from "./CCObject";
import { CCColor } from "./values/CCColor";
@cctype("cc.LabelOutline")
export class CCLabelOutline extends CCComponent{
@ccversion(EditorVersion.all)
_color: CCColor = new CCColor(255,255,255,255);
@ccversion(EditorVersion.all)
_width: number = 1;
updateWithLayer(psdLayer: PsdText) {
this._width = psdLayer.outline.width;
this._color.set(psdLayer.outline.color);
}
}

View File

@ -0,0 +1,147 @@
import { config } from "../../config";
import { EditorVersion } from "../../EditorVersion";
import { PsdDocument } from "../../psd/PsdDocument";
import { cctype, ccversion, nonserialization } from "../../_decorator";
import { CCComponent } from "./CCComponent";
import { CCCompPrefabInfo } from "./CCCompPrefabInfo";
import { CCIDObject, CCObject } from "./CCObject";
import { CCColor } from "./values/CCColor";
import { CCSize } from "./values/CCSize";
import { CCTypedArray } from "./values/CCTypedArray";
import { CCVec2 } from "./values/CCVec2";
import { CCVec3 } from "./values/CCVec3";
@cctype("cc.Node")
export class CCNode extends CCObject{
@ccversion(EditorVersion.all)
_parent: CCIDObject = null;
@ccversion(EditorVersion.all)
_children: CCIDObject[] = [];
@ccversion(EditorVersion.all)
_active: boolean = true;
@ccversion(EditorVersion.all)
_components: CCIDObject[] = [];
@ccversion(EditorVersion.all)
_prefab: CCIDObject = null;
@ccversion(EditorVersion.all)
_id: string = "";
// 2.4.x
@ccversion(EditorVersion.v249)
_opacity: number = 255;
// 2.4.x
@ccversion(EditorVersion.v249)
_color: CCColor = new CCColor(255,255,255,255);
// 2.4.x
@ccversion(EditorVersion.v249)
_contentSize: CCSize = new CCSize();
// 2.4.x
@ccversion(EditorVersion.v249)
_anchorPoint: CCVec2 = new CCVec2(0,0);
// 2.4.x
@ccversion(EditorVersion.v249)
_trs: CCTypedArray = new CCTypedArray();
// 2.4.x
@ccversion(EditorVersion.v249)
_eulerAngles: CCVec3 = new CCVec3();
// 2.4.x
@ccversion(EditorVersion.v249)
_skewX: number = 0;
// 2.4.x
@ccversion(EditorVersion.v249)
_skewY: number = 0;
// 2.4.x
@ccversion(EditorVersion.v249)
_is3DNode: boolean = false;
// 2.4.x
@ccversion(EditorVersion.v249)
_groupIndex: number = 0;
// 2.4.x
@ccversion(EditorVersion.v249)
groupIndex: number = 0;
// 2.4.x
@ccversion(EditorVersion.v249)
_renderEnable: boolean = false;
// 2.4.x
@ccversion(EditorVersion.v249)
_bfsRenderFlag: boolean = false;
// 3.4.x
@ccversion(EditorVersion.v342)
_lpos: CCVec3 = new CCVec3();
// 3.4.x
@ccversion(EditorVersion.v342)
_lrot: CCVec3 = new CCVec3();
// 3.4.x
@ccversion(EditorVersion.v342)
_lscale: CCVec3 = new CCVec3();
// 3.4.x
@ccversion(EditorVersion.v342)
_euler: CCVec3 = new CCVec3();
// 3.4.x
@ccversion(EditorVersion.v342)
_layer: number = 33554432;
@nonserialization
psdDoc: PsdDocument = null;
@nonserialization
components: CCComponent[] = [];
@nonserialization
children: CCNode[] = [];
constructor(psdDoc: PsdDocument){
super();
if(psdDoc){
this.psdDoc = psdDoc;
psdDoc.pushObject(this);
}
}
addComponent(comp: CCComponent){
comp.node = {__id__: this.idx }
let compIdx = this.psdDoc.pushObject(comp);
this._components.push({ __id__: compIdx});
this.components.push(comp);
if(config.editorVersion >= EditorVersion.v342){
this.addCompPrefabInfo(comp)
}
}
addCompPrefabInfo(comp: CCComponent){
let compInfo = new CCCompPrefabInfo();
let compIdx = this.psdDoc.pushObject(compInfo);
comp.__prefab = {__id__: compIdx }
}
addChild(child: CCNode){
this._children.push({ __id__: child.idx});
child._parent = { __id__: this.idx };
this.children.push(child);
}
}

View File

@ -0,0 +1,29 @@
import { EditorVersion } from "../../EditorVersion";
import { utils } from "../../utils/Utils";
import { ccversion, nonserialization } from "../../_decorator";
import { UIObject } from "../UIObject";
export type CCIDObject = { __id__: number };
export type CCUUIDObject = { __uuid__: string, __expectedType__?: string};
export class CCObject extends UIObject{
@ccversion(EditorVersion.all)
__type__: string;
@ccversion(EditorVersion.all)
_name: string = "";
@ccversion(EditorVersion.all)
_objFlags: number = 0;
constructor(){
super();
// @ts-ignore
this.__type__ = this.$__type__
}
}

View File

@ -0,0 +1,26 @@
import { EditorVersion } from "../../EditorVersion";
import { cctype, ccversion } from "../../_decorator";
import { CCIDObject, CCObject } from "./CCObject";
@cctype("cc.Prefab")
export class CCPrefab extends CCObject{
@ccversion(EditorVersion.all)
_native: string = "";
@ccversion(EditorVersion.all)
data: CCIDObject = null;
@ccversion(EditorVersion.all)
optimizationPolicy: number = 0;
@ccversion(EditorVersion.all)
asyncLoadAssets: boolean = false;
// 2.4.x
@ccversion(EditorVersion.v249)
readonly: boolean = false;
// // 3.4.x
@ccversion(EditorVersion.v342)
persistent: boolean = false;
}

View File

@ -0,0 +1,30 @@
import { EditorVersion } from "../../EditorVersion";
import { utils } from "../../utils/Utils";
import { cctype, ccversion, nonserialization } from "../../_decorator";
import { UIObject } from "../UIObject";
import { CCIDObject } from "./CCObject";
// @cctype("cc.PrefabInfo")
export class CCPrefabInfo extends UIObject{
@ccversion(EditorVersion.all)
__type__: string = "cc.PrefabInfo";
@ccversion(EditorVersion.all)
root: CCIDObject = { __id__: 1 };
@ccversion(EditorVersion.all)
asset: CCIDObject = { __id__: 0};
@ccversion(EditorVersion.all)
fileId: string = "";
@ccversion(EditorVersion.all)
sync: boolean = false;
constructor(){
super();
this.fileId = utils.compressUuid(this.uuid);
}
}

View File

@ -0,0 +1,86 @@
import { EditorVersion } from "../../EditorVersion";
import { PsdGroup } from "../../psd/PsdGroup";
import { PsdLayer } from "../../psd/PsdLayer";
import { cctype, ccversion } from "../../_decorator";
import { CCComponent } from "./CCComponent";
import { CCNode } from "./CCNode";
import { CCIDObject } from "./CCObject";
import { CCSprite } from "./CCSprite";
@cctype("cc.ProgressBar")
export class CCProgressBar extends CCComponent{
// 2.4.x
@ccversion(EditorVersion.v249)
_N$totalLength: number = 0;
// 2.4.x
@ccversion(EditorVersion.v249)
_N$barSprite: CCIDObject = null;
// 2.4.x
@ccversion(EditorVersion.v249)
_N$mode: number = 0;
// 2.4.x
@ccversion(EditorVersion.v249)
_N$progress: number = 1;
// 2.4.x
@ccversion(EditorVersion.v249)
_N$reverse: boolean = false;
// 3.4.x
@ccversion(EditorVersion.v342)
_barSprite: CCIDObject = null;
// 3.4.x
@ccversion(EditorVersion.v342)
_mode = 0;
// 3.4.x
@ccversion(EditorVersion.v342)
_totalLength = 0;
// 3.4.x
@ccversion(EditorVersion.v342)
_progress = 1;
// 3.4.x
@ccversion(EditorVersion.v342)
_reverse = false;
setBar(sprite: CCSprite){
this._barSprite = this._N$barSprite = {
__id__: sprite.idx
}
}
updateWithLayer(psdLayer: PsdGroup) {
if(!psdLayer.children){
console.error(`CCProgressBar-> 只能作用在 组图层 上`);
return;
}
outer: for (let i = 0; i < psdLayer.children.length; i++) {
const child = psdLayer.children[i];
if(child.attr.comps.bar){
let node = child.uiObject as CCNode;
// 暂时只有横向进度条
this._totalLength = this._N$totalLength = node._contentSize.width;
for (let j = 0; j < node.components.length; j++) {
const comp = node.components[j];
if(comp instanceof CCSprite){
this.setBar(comp);
break outer;
}
}
}
}
}
}

View File

@ -0,0 +1,93 @@
import { config } from "../../config";
import { EditorVersion } from "../../EditorVersion";
import { PsdImage } from "../../psd/PsdImage";
import { cctype, ccversion } from "../../_decorator";
import { CCComponent } from "./CCComponent";
import { CCIDObject, CCUUIDObject } from "./CCObject";
import { CCColor } from "./values/CCColor";
import { CCVec2 } from "./values/CCVec2";
@cctype("cc.Sprite")
export class CCSprite extends CCComponent {
// 2.4.x
@ccversion(EditorVersion.v249)
_materials: CCUUIDObject[] = [];
@ccversion(EditorVersion.all)
_srcBlendFactor: number = 770; // 3.4.x = 2
@ccversion(EditorVersion.all)
_dstBlendFactor: number = 771; // 3.4.x = 4
@ccversion(EditorVersion.all)
_spriteFrame: CCUUIDObject = null;
@ccversion(EditorVersion.all)
_type: number = 0;
@ccversion(EditorVersion.all)
_sizeMode: number = 1;
@ccversion(EditorVersion.all)
_fillType: number = 0;
@ccversion(EditorVersion.all)
_fillCenter: CCVec2 = new CCVec2();
@ccversion(EditorVersion.all)
_fillStart: number = 0;
@ccversion(EditorVersion.all)
_fillRange: number = 0;
@ccversion(EditorVersion.all)
_isTrimmedMode: boolean = true;
@ccversion(EditorVersion.all)
_atlas = null;
// 3.4.x
@ccversion(EditorVersion.v342)
_visFlags: number = 0;
// 3.4.x
@ccversion(EditorVersion.v342)
_customMaterial: any = null;
// 3.4.x
@ccversion(EditorVersion.v342)
_color: CCColor = new CCColor(255,255,255,255);
// 3.4.x
@ccversion(EditorVersion.v342)
_useGrayscale: boolean = false;
use9() {
this._type = 1;
this._sizeMode = 0;
}
updateWithLayer(psdLayer: PsdImage) {
if (psdLayer.s9) {
this.use9();
}
if (Math.abs(psdLayer.scale.x) != 1 || Math.abs(psdLayer.scale.y) != 1) {
this._sizeMode = 0;
}
if(config.editorVersion >= EditorVersion.v342){
this._srcBlendFactor = 2;
this._dstBlendFactor = 4;
}
}
setSpriteFrame(uuid: string){
if(config.editorVersion >= EditorVersion.v342){
this._spriteFrame = {__uuid__: `${uuid}@f9941`,__expectedType__ : "cc.SpriteFrame"};
}else{
this._spriteFrame = {__uuid__: uuid};
}
}
}

View File

@ -0,0 +1,62 @@
import { EditorVersion } from "../../EditorVersion";
import { PsdGroup } from "../../psd/PsdGroup";
import { PsdLayer } from "../../psd/PsdLayer";
import { cctype, ccversion } from "../../_decorator";
import { CCButton } from "./CCButton";
import { CCComponent } from "./CCComponent";
import { CCNode } from "./CCNode";
import { CCIDObject } from "./CCObject";
import { CCSprite } from "./CCSprite";
@cctype("cc.Toggle")
export class CCToggle extends CCButton{
// 2.4.x
@ccversion(EditorVersion.v249)
_N$isChecked = true;
// 2.4.x
@ccversion(EditorVersion.v249)
toggleGroup = null;
// 2.4.x
@ccversion(EditorVersion.v249)
checkMark: CCIDObject = null;
@ccversion(EditorVersion.all)
checkEvents = [];
// 3.4.x
@ccversion(EditorVersion.v342)
_isChecked = true;
// 3.4.x
@ccversion(EditorVersion.v342)
_checkMark: CCIDObject = null;
setCheckMark(sprite: CCSprite){
this._checkMark = this.checkMark = {
__id__: sprite.idx
}
}
updateWithLayer(psdLayer: PsdGroup) {
if(!psdLayer.children){
console.error(`CCToggle-> 只能作用在 组图层 上`);
return;
}
outer: for (let i = 0; i < psdLayer.children.length; i++) {
const child = psdLayer.children[i];
if(child.attr.comps.check){
let node = child.uiObject as CCNode;
for (let j = 0; j < node.components.length; j++) {
const comp = node.components[j];
if(comp instanceof CCSprite){
this.setCheckMark(comp);
break outer;
}
}
}
}
}
}

View File

@ -0,0 +1,15 @@
import { EditorVersion } from "../../EditorVersion";
import { PsdLayer } from "../../psd/PsdLayer";
import { cctype, ccversion } from "../../_decorator";
import { CCComponent } from "./CCComponent";
// 3.4.x
@cctype("cc.UIOpacity")
export class CCUIOpacity extends CCComponent{
@ccversion(EditorVersion.v342)
_opacity = 255;
updateWithLayer(psdLayer: PsdLayer) {
}
}

View File

@ -0,0 +1,22 @@
import { EditorVersion } from "../../EditorVersion";
import { PsdLayer } from "../../psd/PsdLayer";
import { cctype, ccversion } from "../../_decorator";
import { CCComponent } from "./CCComponent";
import { CCIDObject, CCObject } from "./CCObject";
import { CCSize } from "./values/CCSize";
import { CCVec2 } from "./values/CCVec2";
// 3.4.x
@cctype("cc.UITransform")
export class CCUITransform extends CCComponent{
@ccversion(EditorVersion.v342)
_contentSize: CCSize = new CCSize();
@ccversion(EditorVersion.v342)
_anchorPoint: CCVec2 = new CCVec2(0,0);
updateWithLayer(psdLayer: PsdLayer) {
}
}

View File

@ -0,0 +1,7 @@
import { Color } from "../../../values/Color";
import { cctype } from "../../../_decorator";
export class CCColor extends Color{
__type__: string = "cc.Color";
}

View File

@ -0,0 +1,8 @@
import { Size } from "../../../values/Size";
import { cctype } from "../../../_decorator";
@cctype("cc.Size")
export class CCSize extends Size{
__type__: string = "cc.Size";
}

View File

@ -0,0 +1,28 @@
import { cctype } from "../../../_decorator";
import { CCVec3 } from "./CCVec3";
@cctype("TypedArray")
export class CCTypedArray{
__type__: string = "TypedArray";
ctor: string = "Float64Array";
array: number [] = [];
setPosition(x: number,y: number,z: number){
this.array[0] = x;
this.array[1] = y;
this.array[2] = z;
}
setRotation(x: number,y: number,z: number,w: number){
this.array[3] = x;
this.array[4] = y;
this.array[5] = z;
this.array[6] = w;
}
setScale(x: number,y: number,z: number){
this.array[7] = x;
this.array[8] = y;
this.array[9] = z;
}
}

View File

@ -0,0 +1,8 @@
import { Vec2 } from "../../../values/Vec2";
import { cctype } from "../../../_decorator";
@cctype("cc.Vec2")
export class CCVec2 extends Vec2{
__type__: string = "cc.Vec2";
}

View File

@ -0,0 +1,8 @@
import { Vec3 } from "../../../values/Vec3";
import { cctype } from "../../../_decorator";
export class CCVec3 extends Vec3{
__type__: string = "cc.Vec3";
}

View File

@ -0,0 +1,2 @@
export * from './index'

20
psd2ui-tools/src/index.ts Normal file
View File

@ -0,0 +1,20 @@
import minimist from 'minimist';
import { Main } from './Main';
import { Texture9Utils } from './utils/Texture9Utils';
// ##################
// 输入
const oldArgs = process.argv.slice(2);
const args = minimist(oldArgs);
let main = new Main();
if (oldArgs.length) {
main.exec(args);
} else {
// 测试
main.test();
}
// ##################

View File

@ -0,0 +1,6 @@
export enum LayerType{
Doc,
Group,
Text,
Image
}

View File

@ -0,0 +1,46 @@
import { UIObject } from "../engine/UIObject";
import { Rect } from "../values/Rect";
import { Size } from "../values/Size";
import { PsdGroup } from "./PsdGroup";
export class PsdDocument extends PsdGroup{
/** 当前文档所有的图片 */
images: Map<string,any> = new Map();
objectMap: Map<string,number> = new Map();
objectArray: UIObject[] = [];
constructor(source: any){
super(source,null,null);
this.size = new Size(source.width,source.height);
this.rect = new Rect(0, this.size.width, 0, this.size.height);
}
pushObject(uiObject: UIObject){
let idx = this.objectArray.length;
uiObject.idx = idx;
this.objectMap.set(uiObject.uuid,idx);
this.objectArray.push(uiObject);
return idx;
}
getObjectIdx(uuid: string){
let idx = this.objectMap.get(uuid);
return idx;
}
getObject(uuid: string){
let idx = this.objectMap.get(uuid);
if(idx < this.objectArray.length){
return this.objectArray[idx];
}
return null;
}
onCtor(): void {
super.onCtor();
}
}

View File

@ -0,0 +1,49 @@
import { Rect } from "../values/Rect";
import { PsdLayer } from "./PsdLayer";
export class PsdGroup extends PsdLayer {
declare children: PsdLayer[];
declare parent: PsdGroup;
constructor(source: any, parent: PsdLayer, rootDoc: PsdLayer) {
super(source, parent, rootDoc);
this.children = [];
if (rootDoc) {
this.rect = new Rect(0, rootDoc.size.width, 0, rootDoc.size.height);
}
}
parseSource(): boolean {
super.parseSource();
if(!this.attr?.comps.full){
this.resize();
this.computeBasePosition();
}
return true;
}
resize() {
let left = Number.MAX_SAFE_INTEGER;
let right = Number.MIN_SAFE_INTEGER;
let top = Number.MAX_SAFE_INTEGER;
let bottom = Number.MIN_SAFE_INTEGER;
for (let i = 0; i < this.children.length; i++) {
const element = this.children[i];
let _rect = element.rect;
left = Math.min(_rect.left, left);
right = Math.max(_rect.right, right);
top = Math.min(_rect.top, top);
bottom = Math.max(_rect.bottom, bottom);
}
this.rect.left = left;
this.rect.right = right;
this.rect.top = top;
this.rect.bottom = bottom;
}
onCtor() {
}
}

View File

@ -0,0 +1,98 @@
import { PsdGroup } from "./PsdGroup";
import { PsdLayer } from "./PsdLayer";
import { utils } from "../utils/Utils";
import canvas from 'canvas';
import { Border, Texture9Utils } from "../utils/Texture9Utils";
import { Size } from "../values/Size";
import { fileUtils } from "../utils/FileUtils";
import { Vec3 } from "../values/Vec3";
export class PsdImage extends PsdLayer {
declare parent: PsdGroup;
declare textureUuid: string;
declare md5: string;
declare imgBuffer: Buffer;
declare textureSize: Size;
declare imgName: string;
declare s9: Border;
constructor(source: any, parent: PsdLayer, rootDoc: PsdLayer) {
super(source, parent, rootDoc);
this.textureUuid = utils.uuid();
// img name
this.imgName = this.attr.comps.img?.name || this.name
// .9
if (this.attr.comps['.9']) {
let s9 = this.attr.comps['.9'];
this.s9 = Texture9Utils.safeBorder(this.source.canvas, s9 as any);
let newCanvas = Texture9Utils.split(this.source.canvas, s9 as any);
this.source.canvas = newCanvas;
}
let canvas: canvas.Canvas = this.source.canvas;
this.imgBuffer = canvas.toBuffer('image/png');
this.md5 = fileUtils.getMD5(this.imgBuffer);
this.textureSize = new Size(canvas.width, canvas.height);
this.scale = new Vec3((this.isFilpX() ? -1 : 1) * this.scale.x, (this.isFilpY() ? -1 : 1) * this.scale.y, 1);
}
onCtor() {
}
isIgnore() {
//
if (this.attr.comps.ignore || this.attr.comps.ignoreimg) {
return true;
}
return false;
}
/** 是否是镜像图片 */
isBind() {
return typeof this.attr.comps.flip?.bind !== 'undefined'
|| typeof this.attr.comps.img?.bind !== 'undefined';
}
/** 是否是 x 方向镜像图片 */
isFilpX() {
return typeof this.attr.comps.flipX?.bind !== 'undefined';
}
/** 是否是 y 方向镜像图片 */
isFilpY() {
return typeof this.attr.comps.flipY?.bind !== 'undefined';
}
// 根据锚点计算坐标
updatePositionWithAR() {
if (!this.parent) {
return;
}
let parent = this.parent;
while (parent) {
this.position.x -= parent.position.x;
this.position.y -= parent.position.y;
parent = parent.parent;
}
// this.position.x = this.position.x - this.parent.size.width * this.parent.anchorPoint.x + this.size.width * this.anchorPoint.x;
// this.position.y = this.position.y - this.parent.size.height * this.parent.anchorPoint.y + this.size.height * this.anchorPoint.y;
// 如果是镜像图片,则特殊处理
let arX = (this.isFilpX() ? (1 - this.anchorPoint.x) : this.anchorPoint.x);
let arY = (this.isFilpY() ? (1 - this.anchorPoint.y) : this.anchorPoint.y);
this.position.x = this.position.x - this.rootDoc.size.width * this.rootDoc.anchorPoint.x + this.size.width * arX;
this.position.y = this.position.y - this.rootDoc.size.height * this.rootDoc.anchorPoint.y + this.size.height * arY;
}
}

View File

@ -0,0 +1,298 @@
import { PsdLayerSource } from "../_declare";
import { LayerType } from "./LayerType";
import { Size } from "../values/Size";
import { Vec2 } from "../values/Vec2";
import { utils } from "../utils/Utils";
import { UIObject } from "../engine/UIObject";
import { Rect } from "../values/Rect";
import { Color } from "../values/Color";
import { Vec3 } from "../values/Vec3";
import { pinyin } from "pinyin-pro";
/**
*
* "name@Type{prop: 1,prop2: 2}"
* Type = btn | bar | (toggle @check) | .9 |
*
*/
export interface PsdAttr {
name: string,
comps: {
Btn?: {};
btn?: {};
ProgressBar?: {};
progressBar?: {};
bar?: {};
Toggle?: {};
toggle?: {};
check?: {};
".9"?: { l?: number, r?: number, b?: number, t?: number };
ar?: { x?: number, y?: number };
// 忽略导出节点和图片
ignore?: {};
ig?: {};
// 忽略导出节点
ignorenode?: {};
ignode?: {};
// 忽略导出图片
ignoreimg?: {};
igimg?: {};
full?: {};
size?: { w?: number, h?: number };
scale?: { x?: number, y?: number };
img?: { id?: number, name?: string, bind?: number }
flip?: { bind: number, x?: number, y?: number }
flipX?: { bind: number }
flipY?: { bind: number }
// position?:{x?: number,y?: number};
// pos?:{x?: number,y?: number};
}
}
export abstract class PsdLayer {
static isPinyin = false;
declare uuid: string;
declare rootDoc: PsdLayer;
declare name: string;
declare source: PsdLayerSource;
declare parent: PsdLayer;
declare position: Vec2;
declare size: Size;
declare rect: Rect;
declare anchorPoint: Vec2;
declare hidden: boolean;
declare opacity: number;
declare layerType: LayerType;
declare uiObject: UIObject;
declare attr: PsdAttr; // 解析名字获得各项属性
declare color: Color;
declare scale: Vec3;
constructor(source: any, parent: PsdLayer, rootDoc: PsdLayer) {
this.uuid = utils.uuid();
this.source = source;
this.parent = parent;
this.rootDoc = rootDoc;
this.name = source.name;
this.position = new Vec2();
this.size = new Size();
this.rect = new Rect(source);
// this.anchorPoint = new Vec2();
this.anchorPoint = new Vec2(0.5, 0.5);
this.hidden = false;
this.opacity = 255;
this.color = new Color(255, 255, 255, 255);
console.log(`PsdLayer->解析到图层 `, this.name);
this.attr = this.parseNameRule(this.name);
// // 更新名字
this.name = this.chineseToPinyin(this.attr?.name || this.name);
// 使用配置的缩放系数
let _scale = this.attr?.comps.scale;
this.scale = new Vec3(_scale?.x ?? 1, _scale?.y ?? 1, 1);
}
abstract onCtor();
parseNameRule(name: string) {
if (!name) {
return;
}
name = name.trim();
let fragments = name.split("@");
if (fragments.length === 0) {
console.error(`PsdLayer-> 名字解析错误`);
return;
}
let obj: PsdAttr = {
name: fragments[0]?.replace(/\.|>|\/|\ /g, "_") ?? "unknow",
comps: {},
}
for (let i = 1; i < fragments.length; i++) {
const fragment = fragments[i].trim();
let attr = {};
let startIdx = fragment.indexOf("{");
let comp = fragment;
if (startIdx != -1) {
let endIdx = fragment.indexOf("}");
if (endIdx == -1) {
console.log(`PsdLayer->${name} 属性 解析错误`);
continue;
}
let attrStr = fragment.substring(startIdx + 1, endIdx);
comp = fragment.substr(0, startIdx);
attrStr = attrStr.trim();
let attrs = attrStr.split(",");
attrs.forEach((str) => {
str = str.trim();
let strs = str.split(":");
if (!strs.length) {
console.log(`PsdLayer->${name} 属性 解析错误`);
return;
}
strs.map((v) => {
return v.trim();
});
attr[strs[0]] = utils.isNumber(strs[1]) ? parseFloat(strs[1]) : strs[1];
});
}
comp = comp.trim();
comp = comp.replace(":", ""); // 防呆,删除 key 中的冒号,
obj.comps[comp] = attr;
}
// 获取别名的值
obj.comps.ignore = obj.comps.ignore || obj.comps.ig;
obj.comps.ignorenode = obj.comps.ignorenode || obj.comps.ignode;
obj.comps.ignoreimg = obj.comps.ignoreimg || obj.comps.igimg;
obj.comps.Btn = obj.comps.Btn || obj.comps.btn;
obj.comps.ProgressBar = obj.comps.ProgressBar || obj.comps.progressBar;
obj.comps.Toggle = obj.comps.Toggle || obj.comps.toggle;
// 图片名中文转拼音
if (obj.comps.img) {
if (obj.comps.img.name) {
obj.comps.img.name = this.chineseToPinyin(obj.comps.img.name);
}
}
// 将mirror filpX filpY 进行合并
if (obj.comps.flip || obj.comps.flipX || obj.comps.flipY) {
obj.comps.flip = Object.assign({}, obj.comps.flip, obj.comps.flipX, obj.comps.flipY);
if (obj.comps.flipX) {
obj.comps.flip.x = 1;
}
if (obj.comps.flipY) {
obj.comps.flip.y = 1;
}
// x,y 都缺省时,默认 x 方向镜像
if (typeof obj.comps.flip.bind !== 'undefined') {
if (!obj.comps.flip.y) {
obj.comps.flip.x = 1;
}
// 只有作为镜像图片使用的时候才反向赋值
// 反向赋值,防止使用的时候值错误
if (obj.comps.flip.x) {
obj.comps.flipX = Object.assign({}, obj.comps.flipX, obj.comps.flip);
}
if (obj.comps.flip.y) {
obj.comps.flipY = Object.assign({}, obj.comps.flipY, obj.comps.flip);
}
}
}
// 检查冲突
if (obj.comps.full && obj.comps.size) {
console.warn(`PsdLayer->${obj.name} 同时存在 @full 和 @size`);
}
return obj;
}
/** 解析数据 */
parseSource() {
let _source = this.source;
// psd文档
if (!this.parent) {
return false;
}
this.hidden = _source.hidden;
this.opacity = Math.round(_source.opacity * 255);
// 获取锚点
let ar = this.attr.comps.ar;
if (ar) {
this.anchorPoint.x = ar.x ?? this.anchorPoint.x;
this.anchorPoint.y = ar.y ?? this.anchorPoint.y;
}
this.computeBasePosition();
return true;
}
/** 解析 effect */
parseEffects() {
// 颜色叠加 暂时搞不定
// if(this.source.effects?.solidFill){
// let solidFills = this.source.effects?.solidFill;
// for (let i = 0; i < solidFills.length; i++) {
// const solidFill = solidFills[i];
// if(solidFill.enabled){
// let color = solidFill.color;
// this.color = new Color(color.r,color.g,color.b,solidFill.opacity * 255);
// }
// }
// }
}
/** 中文转拼音 */
chineseToPinyin(text: string) {
if (!text || !PsdLayer.isPinyin) {
return text;
}
let names = pinyin(text, {
toneType: "none",
type: "array"
});
names = names.map((text) => {
return text.slice(0, 1).toUpperCase() + text.slice(1).toLowerCase();
});
return names.join("");
}
// 计算初始坐标 左下角 0,0 为锚点
computeBasePosition() {
if (!this.rootDoc) {
return;
}
let _rect = this.rect;
let width = (_rect.right - _rect.left);
let height = (_rect.bottom - _rect.top);
this.size.width = width;
this.size.height = height;
// 位置 左下角为锚点
let x = _rect.left;
let y = (this.rootDoc.size.height - _rect.bottom);
this.position.x = x;
this.position.y = y;
}
// 根据锚点计算坐标
updatePositionWithAR() {
if (!this.parent) {
return;
}
let parent = this.parent;
while (parent) {
this.position.x -= parent.position.x;
this.position.y -= parent.position.y;
parent = parent.parent;
}
// this.position.x = this.position.x - this.parent.size.width * this.parent.anchorPoint.x + this.size.width * this.anchorPoint.x;
// this.position.y = this.position.y - this.parent.size.height * this.parent.anchorPoint.y + this.size.height * this.anchorPoint.y;
this.position.x = this.position.x - this.rootDoc.size.width * this.rootDoc.anchorPoint.x + this.size.width * this.anchorPoint.x;
this.position.y = this.position.y - this.rootDoc.size.height * this.rootDoc.anchorPoint.y + this.size.height * this.anchorPoint.y;
}
}

View File

@ -0,0 +1,67 @@
import { config } from "../config";
import { Color } from "../values/Color";
import { Vec2 } from "../values/Vec2";
import { PsdGroup } from "./PsdGroup";
import { PsdLayer } from "./PsdLayer";
export class PsdText extends PsdLayer{
declare parent: PsdGroup;
declare text: string;
declare fontSize: number;
declare font: string;
declare outline: { width: number, color: Color }; // 描边
declare offsetY: number;
parseSource(): boolean {
super.parseSource();
let textSource = this.source.text;
let style = textSource.style;
if(style){
let fillColor = style.fillColor;
if(fillColor){
this.color = new Color(fillColor.r,fillColor.g,fillColor.b,fillColor.a * 255);
}
}
this.text = textSource.text;
this.fontSize = style.fontSize;
this.offsetY = config.textOffsetY[this.fontSize] || config.textOffsetY["default"] || 0;
this.parseSolidFill();
this.parseStroke();
return true;
}
onCtor(){
}
/** 描边 */
parseStroke(){
if(this.source.effects?.stroke){
let stroke = this.source.effects?.stroke[0];
// 外描边
if(stroke?.enabled && stroke?.position === "outside"){
let color = stroke.color;
this.outline = {
width: stroke.size.value,
color: new Color(color.r,color.g,color.b,stroke.opacity * 255)
}
}
}
}
/** 解析 颜色叠加 */
parseSolidFill() {
if(this.source.effects?.solidFill){
let solidFills = this.source.effects?.solidFill;
for (let i = 0; i < solidFills.length; i++) {
const solidFill = solidFills[i];
if(solidFill.enabled){
let color = solidFill.color;
this.color = new Color(color.r,color.g,color.b,solidFill.opacity * 255);
}
}
}
}
}

View File

@ -0,0 +1,114 @@
import fs from 'fs-extra';
import path from 'path';
import crypto from "crypto";
class FileUtils {
// 深度遍历
DFS(root: string, callback?: (options: {isDirectory: boolean,fullPath: string, fileName: string,depth: number}) => void,depth = 0) {
let exists = fs.existsSync(root);
if (!exists) {
console.log(`FileUtils-> ${root} is not exists`);
return;
}
let files = fs.readdirSync(root);
let _cacheDepth = depth;
depth ++;
files.forEach((file) => {
let fullPath = path.join(root, file);
let stat = fs.lstatSync(fullPath);
let isDirectory = stat.isDirectory();
callback?.({isDirectory,fullPath,fileName: file,depth: _cacheDepth});
if (!isDirectory) {
} else {
this.DFS(fullPath,callback,depth);
}
});
}
filterFile(root: string, filter?: (fileName: string) => boolean) {
let exists = fs.existsSync(root);
if (!exists) {
console.log(`FileUtils-> ${root} is not exists`);
return;
}
var res: string[] = [];
let files = fs.readdirSync(root);
files.forEach((file) => {
let pathName = path.join(root, file);
let stat = fs.lstatSync(pathName);
let isDirectory = stat.isDirectory();
// 只对文件进行判断
if(!isDirectory){
let isPass = filter(file);
if(!isPass){
return;
}
}
if (!isDirectory) {
res.push(pathName);
} else {
res = res.concat(this.filterFile(pathName,filter));
}
});
return res
}
getFolderFiles(dir: string,type: "folder" | "file"){
let exists = fs.existsSync(dir);
if (!exists) {
console.log(`FileUtils-> ${dir} is not exists`);
return;
}
let res: {fullPath: string,basename: string}[] = [];
let files = fs.readdirSync(dir);
files.forEach((file) => {
let fullPath = path.join(dir, file);
let stat = fs.lstatSync(fullPath);
let isDirectory = stat.isDirectory();
if (isDirectory) {
if(type === 'folder'){
res.push({fullPath,basename: file});
}
}else{
if(type === 'file'){
res.push({fullPath,basename: file});
}
}
});
return res;
}
async writeFile(fullPath: string, data: any) {
if(typeof data !== 'string'){
try {
data = JSON.stringify(data,null,2);
} catch (error) {
console.log(`FileUtils->writeFile `,error);
return;
}
}
console.log(`写入文件 ${fullPath}`);
let dir = path.dirname(fullPath);
await fs.mkdirp(dir);
await fs.writeFile(fullPath, data);
console.log(`写入完成 ${fullPath} `);
}
/** 获取文件的 md5 */
getMD5(buffer: Buffer | string){
if(typeof buffer === 'string'){
buffer = fs.readFileSync(buffer);
}
let md5 = crypto.createHash("md5").update(buffer).digest("hex");
return md5;
}
}
export let fileUtils = new FileUtils();

View File

@ -0,0 +1,60 @@
import canvas from 'canvas';
export interface Border {
l?: number;
r?: number;
t?: number;
b?: number;
}
export class Texture9Utils {
static safeBorder(_canvas: canvas.Canvas, border: Border) {
border.l = (border.l ?? border.r) || 0;
border.r = (border.r ?? border.l) || 0;
border.t = (border.t ?? border.b) || 0;
border.b = (border.b ?? border.t) || 0;
return border;
}
static split(_canvas: canvas.Canvas, border: Border): canvas.Canvas {
this.safeBorder(_canvas, border);
let cw = _canvas.width;
let ch = _canvas.height;
let space = 4;
let left = border.l || cw;
let right = border.r || cw;
let top = border.t || ch;
let bottom = border.b || ch;
if (border.b == 0 && border.t == 0 && border.l == 0 && border.r == 0) {
return _canvas;
}
if (border.l + border.r > cw + space) {
console.log(`Texture9Utils-> 设置的九宫格 left right 数据不合理,请重新设置`);
return _canvas;
}
if (border.b + border.t > ch + space) {
console.log(`Texture9Utils-> 设置的九宫格 bottom top 数据不合理,请重新设置`);
return _canvas;
}
let newCanvas = canvas.createCanvas(Math.min(cw, border.l + border.r + space) || cw, Math.min(ch, border.b + border.t + space) || ch);
let ctx = newCanvas.getContext("2d");
// 左上
ctx.drawImage(_canvas, 0, 0, left + space, top + space, 0, 0, left + space, top + space);
// 左下
ctx.drawImage(_canvas, 0, ch - bottom, left + space, bottom, 0, top + space, left + space, bottom);
// 右上
ctx.drawImage(_canvas, cw - left, 0, right, top + space, left + space, 0, right, top + space);
// 右下
ctx.drawImage(_canvas, cw - left, ch - bottom, right, bottom, left + space, top + space, right, bottom);
return newCanvas;
}
}

View File

@ -0,0 +1,98 @@
import canvas from 'canvas';
import { PsdLayer } from "../psd/PsdLayer";
import fs from 'fs-extra';
import path from 'path';
import { PsdDocument } from '../psd/PsdDocument';
import { PsdImage } from '../psd/PsdImage';
// ------------decode-uuid
const BASE64_KEYS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
const values = new Array(123); // max char code in base64Keys
for (let i = 0; i < 123; ++i) { values[i] = 64; } // fill with placeholder('=') index
for (let i = 0; i < 64; ++i) { values[BASE64_KEYS.charCodeAt(i)] = i; }
// decoded value indexed by base64 char code
const BASE64_VALUES = values;
const HexChars = '0123456789abcdef'.split('');
const _t = ['', '', '', ''];
const UuidTemplate = _t.concat(_t, '-', _t, '-', _t, '-', _t, '-', _t, _t, _t);
const Indices = UuidTemplate.map((x, i) => x === '-' ? NaN : i).filter(isFinite);
let HexMap = {}
{
for (let i = 0; i < HexChars.length; i++) {
let char = HexChars[i]
HexMap[char] = i
}
}
class Utils {
uuid() {
var d = new Date().getTime();
if (globalThis.performance && typeof globalThis.performance.now === "function") {
d += performance.now(); //use high-precision timer if available
}
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
decodeUuid(base64) {
const strs = base64.split('@');
const uuid = strs[0];
if (uuid.length !== 22) {
return base64;
}
UuidTemplate[0] = base64[0];
UuidTemplate[1] = base64[1];
for (let i = 2, j = 2; i < 22; i += 2) {
const lhs = BASE64_VALUES[base64.charCodeAt(i)];
const rhs = BASE64_VALUES[base64.charCodeAt(i + 1)];
UuidTemplate[Indices[j++]] = HexChars[lhs >> 2];
UuidTemplate[Indices[j++]] = HexChars[((lhs & 3) << 2) | rhs >> 4];
UuidTemplate[Indices[j++]] = HexChars[rhs & 0xF];
}
return base64.replace(uuid, UuidTemplate.join(''));
}
// 压缩uuid
compressUuid(fullUuid) {
const strs = fullUuid.split('@');
const uuid: string = strs[0];
if (uuid.length !== 36) {
return fullUuid;
}
let zipUuid = []
zipUuid[0] = uuid[0];
zipUuid[1] = uuid[1];
let cleanUuid = uuid.replace('-', '').replace('-', '').replace('-', '').replace('-', '')
for (let i = 2, j = 2; i < 32; i += 3) {
const left = HexMap[String.fromCharCode(cleanUuid.charCodeAt(i))];
const mid = HexMap[String.fromCharCode(cleanUuid.charCodeAt(i + 1))];
const right = HexMap[String.fromCharCode(cleanUuid.charCodeAt(i + 2))];
zipUuid[j++] = BASE64_KEYS[(left << 2) + (mid >> 2)]
zipUuid[j++] = BASE64_KEYS[((mid & 3) << 4) + right]
}
return fullUuid.replace(uuid, zipUuid.join(''));
}
isNumber(val){
return (!isNaN(parseFloat(val)) && isFinite(val));
}
}
export const utils = new Utils();

View File

@ -0,0 +1,98 @@
var Uuid = require('node-uuid');
var Base64KeyChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var AsciiTo64 = new Array(128);
for (var i = 0; i < 128; ++i) { AsciiTo64[i] = 0; }
for (i = 0; i < 64; ++i) { AsciiTo64[Base64KeyChars.charCodeAt(i)] = i; }
var Reg_Dash = /-/g;
var Reg_Uuid = /^[0-9a-fA-F-]{36}$/;
var Reg_NormalizedUuid = /^[0-9a-fA-F]{32}$/;
var Reg_CompressedUuid = /^[0-9a-zA-Z+/]{22,23}$/;
export class UuidUtils {
// 加了这个标记后,字符串就不可能会是 uuid 了。
static NonUuidMark: '.'
// 压缩后的 uuid 可以减小保存时的尺寸,但不能做为文件名(因为无法区分大小写并且包含非法字符)。
// 默认将 uuid 的后面 27 位压缩成 18 位,前 5 位保留下来,方便调试。
// fc991dd7-0033-4b80-9d41-c8a86a702e59 -> fc9913XADNLgJ1ByKhqcC5Z
// 如果启用 min 则将 uuid 的后面 30 位压缩成 20 位,前 2 位保留不变。
// fc991dd7-0033-4b80-9d41-c8a86a702e59 -> fcmR3XADNLgJ1ByKhqcC5Z
/*
* @param {Boolean} [min=false]
*/
static compressUuid (uuid, min) {
if (Reg_Uuid.test(uuid)) {
uuid = uuid.replace(Reg_Dash, '');
}
else if (!Reg_NormalizedUuid.test(uuid)) {
return uuid;
}
var reserved = (min === true) ? 2 : 5;
return UuidUtils.compressHex(uuid, reserved);
}
static compressHex (hexString, reservedHeadLength) {
var length = hexString.length;
var i;
if (typeof reservedHeadLength !== 'undefined') {
i = reservedHeadLength;
}
else {
i = length % 3;
}
var head = hexString.slice(0, i);
var base64Chars = [];
while (i < length) {
var hexVal1 = parseInt(hexString[i], 16);
var hexVal2 = parseInt(hexString[i + 1], 16);
var hexVal3 = parseInt(hexString[i + 2], 16);
base64Chars.push(Base64KeyChars[(hexVal1 << 2) | (hexVal2 >> 2)]);
base64Chars.push(Base64KeyChars[((hexVal2 & 3) << 4) | hexVal3]);
i += 3;
}
return head + base64Chars.join('');
}
static decompressUuid (str) {
if (str.length === 23) {
// decode base64
var hexChars = [];
for (var i = 5; i < 23; i += 2) {
var lhs = AsciiTo64[str.charCodeAt(i)];
var rhs = AsciiTo64[str.charCodeAt(i + 1)];
hexChars.push((lhs >> 2).toString(16));
hexChars.push((((lhs & 3) << 2) | rhs >> 4).toString(16));
hexChars.push((rhs & 0xF).toString(16));
}
//
str = str.slice(0, 5) + hexChars.join('');
}
else if (str.length === 22) {
// decode base64
var hexChars = [];
for (var i = 2; i < 22; i += 2) {
var lhs = AsciiTo64[str.charCodeAt(i)];
var rhs = AsciiTo64[str.charCodeAt(i + 1)];
hexChars.push((lhs >> 2).toString(16));
hexChars.push((((lhs & 3) << 2) | rhs >> 4).toString(16));
hexChars.push((rhs & 0xF).toString(16));
}
//
str = str.slice(0, 2) + hexChars.join('');
}
return [str.slice(0, 8), str.slice(8, 12), str.slice(12, 16), str.slice(16, 20), str.slice(20)].join('-');
}
static isUuid (str) {
return Reg_CompressedUuid.test(str) || Reg_NormalizedUuid.test(str) || Reg_Uuid.test(str);
}
static uuid () {
var uuid = Uuid.v4();
return UuidUtils.compressUuid(uuid, true);
}
};

View File

@ -0,0 +1,40 @@
export class Color{
declare r: number;
declare g: number;
declare b: number;
declare a: number;
constructor(r: number,g: number,b: number,a: number){
this.r = Math.ceil(r || 0);
this.g = Math.ceil(g || 0);
this.b = Math.ceil(b || 0);
this.a = Math.ceil(a || 0);
}
set(color: Color){
this.r = Math.ceil(color.r || 0);
this.g = Math.ceil(color.g || 0);
this.b = Math.ceil(color.b || 0);
this.a = Math.ceil(color.a || 0);
}
public toHEX (fmt: '#rgb' | '#rrggbb' | '#rrggbbaa' = '#rrggbb') {
const prefix = '0';
// #rrggbb
const hex = [
(this.r < 16 ? prefix : '') + (this.r).toString(16),
(this.g < 16 ? prefix : '') + (this.g).toString(16),
(this.b < 16 ? prefix : '') + (this.b).toString(16),
];
const i = -1;
if (fmt === '#rgb') {
hex[0] = hex[0][0];
hex[1] = hex[1][0];
hex[2] = hex[2][0];
} else if (fmt === '#rrggbbaa') {
hex.push((this.a < 16 ? prefix : '') + (this.a).toString(16));
}
return hex.join('');
}
}

View File

@ -0,0 +1,23 @@
export class Rect{
declare left: number;
declare right: number;
declare top: number;
declare bottom: number;
constructor(left: number | Rect = 0,right = 0,top = 0,bottom = 0) {
if(typeof left == 'object'){
this.set(left);
return;
}
this.left = left || 0;
this.right = right || 0;
this.top = top || 0;
this.bottom = bottom || 0;
}
set(rect: Rect){
this.left = rect.left;
this.right = rect.right;
this.top = rect.top;
this.bottom = rect.bottom;
}
}

View File

@ -0,0 +1,8 @@
export class Size{
declare width: number;
declare height: number;
constructor(width: number = 0,height: number = 0) {
this.width = width || 0;
this.height = height || 0;
}
}

View File

@ -0,0 +1,8 @@
export class Vec2{
declare x: number;
declare y: number;
constructor(x: number = 0,y: number = 0) {
this.x = x || 0;
this.y = y || 0;
}
}

View File

@ -0,0 +1,10 @@
export class Vec3{
declare x: number;
declare y: number;
declare z: number;
constructor(x: number = 0,y: number = 0,z: number = 0) {
this.x = x || 0;
this.y = y || 0;
this.z = z || 0;
}
}

View File

@ -0,0 +1,15 @@
{
"cc.Label": {
"__type__": "e4f88adp3hERoJ48DZ2PSAl",
"_N$file":{
"__uuid__": "803c185c-9442-4b99-af1a-682f877539ab"
},
"_isSystemFontUsed": false,
"isFixNumber": true
},
"textOffsetY":{
"default": -3,
"36": -3
},
"textLineHeightOffset": 4
}

BIN
psd2ui-tools/test/demo.psd Normal file

Binary file not shown.

BIN
psd2ui-tools/test/png9.psd Normal file

Binary file not shown.

104
psd2ui-tools/tsconfig.json Normal file
View File

@ -0,0 +1,104 @@
{
"compilerOptions": {
"experimentalDecorators": true,
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ES2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
"declaration": false, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": false, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
}