mirror of
https://github.com/penpot/penpot-exporter-figma-plugin.git
synced 2024-12-22 05:33:02 -05:00
Merge pull request #52 from penpot/dev
Plugin relaunch, initial setup, features and refactoring
This commit is contained in:
commit
4cb8db5332
147 changed files with 20200 additions and 8959 deletions
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
5
.changeset/calm-eels-reflect.md
Normal file
5
.changeset/calm-eels-reflect.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"penpot-exporter": minor
|
||||
---
|
||||
|
||||
Initial plugin release
|
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_size = 2
|
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
ui-src/lib/penpot.js
|
25
.eslintrc.cjs
Normal file
25
.eslintrc.cjs
Normal file
|
@ -0,0 +1,25 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
},
|
||||
env: { browser: true, node: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:@figma/figma-plugins/recommended'
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }]
|
||||
}
|
||||
};
|
34
.github/workflows/ci.yaml
vendored
Normal file
34
.github/workflows/ci.yaml
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, dev]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.node }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['20.x']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: npm run build
|
||||
lint:
|
||||
name: Lint ${{ matrix.node }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['20.x']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: npm run lint
|
31
.github/workflows/release.yaml
vendored
Normal file
31
.github/workflows/release.yaml
vendored
Normal file
|
@ -0,0 +1,31 @@
|
|||
name: Publish and release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['20.x']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- uses: changesets/action@v1
|
||||
with:
|
||||
title: Release
|
||||
publish: npx changeset tag
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,2 @@
|
|||
node_modules
|
||||
package-lock.json
|
||||
dist
|
||||
|
|
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
@ -0,0 +1 @@
|
|||
20
|
5
.prettierignore
Normal file
5
.prettierignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
.changeset
|
||||
node_modules
|
||||
dist
|
||||
ui-src/lib/penpot.js
|
||||
LICENSE
|
69
DEV_GUIDE.md
69
DEV_GUIDE.md
|
@ -1,39 +1,40 @@
|
|||
# Developer guide
|
||||
|
||||
The plugin relies in [penpot.js](https://github.com/penpot/penpot-exporter-figma-plugin/blob/main/src/penpot.js)
|
||||
library. It contains a subset of Penpot frontend app, transpiled into
|
||||
javascript to be used from js code (this was easy since the frontend is written
|
||||
in ClojureScript, that has direct translation to javascript).
|
||||
The plugin relies in
|
||||
[penpot.js](https://github.com/penpot/penpot-exporter-figma-plugin/blob/main/src/penpot.js) library.
|
||||
It contains a subset of Penpot frontend app, transpiled into javascript to be used from js code
|
||||
(this was easy since the frontend is written in ClojureScript, that has direct translation to
|
||||
javascript).
|
||||
|
||||
Basically, it exports the `createFile` function and the `File` data type, that
|
||||
represents a Penpot file as it resides in memory inside the frontend app. It
|
||||
has function to create pages and their content, and also an `export` function
|
||||
that generates and downloads a .zip archive with the Penpot file as svg
|
||||
documents in Penpot annotated format, that you can import directly into Penpot.
|
||||
Basically, it exports the `createFile` function and the `File` data type, that represents a Penpot
|
||||
file as it resides in memory inside the frontend app. It has function to create pages and their
|
||||
content, and also an `export` function that generates and downloads a .zip archive with the Penpot
|
||||
file as svg documents in Penpot annotated format, that you can import directly into Penpot.
|
||||
|
||||
You can see the [source of the library at Penpot repo](https://github.com/penpot/penpot/tree/develop/frontend/src/app/libs).
|
||||
You can see the
|
||||
[source of the library at Penpot repo](https://github.com/penpot/penpot/tree/develop/frontend/src/app/libs).
|
||||
|
||||
To see a general description of the data types used in the functions you can
|
||||
see [the data model](https://help.penpot.app/technical-guide/data-model/).
|
||||
Their full specifications are in the [common types module](https://github.com/penpot/penpot/tree/develop/common/src/app/common/types).
|
||||
To see a general description of the data types used in the functions you can see
|
||||
[the data model](https://help.penpot.app/technical-guide/data-model/). Their full specifications are
|
||||
in the
|
||||
[common types module](https://github.com/penpot/penpot/tree/develop/common/src/app/common/types).
|
||||
|
||||
Those types are defined in [Clojure spec](https://clojure.org/guides/spec) format.
|
||||
For those unfamiliar with the syntax, here is a small basic guide:
|
||||
Those types are defined in [Clojure spec](https://clojure.org/guides/spec) format. For those
|
||||
unfamiliar with the syntax, here is a small basic guide:
|
||||
|
||||
```clojure
|
||||
(s/def ::name string?)
|
||||
(s/def ::id uuid?)
|
||||
```
|
||||
|
||||
A parameter or attribute called `name` is of type string, and `id` is an *uuid*
|
||||
string (e.g. "000498f3-27fc-8000-8988-ca7d52f46843").
|
||||
A parameter or attribute called `name` is of type string, and `id` is an _uuid_ string (e.g.
|
||||
"000498f3-27fc-8000-8988-ca7d52f46843").
|
||||
|
||||
```clojure
|
||||
(s/def ::stroke-alignment #{:center :inner :outer})
|
||||
```
|
||||
|
||||
`stroke-alignment` is of an enumerated type, and the valid values are "center",
|
||||
"inner" and "outer".
|
||||
`stroke-alignment` is of an enumerated type, and the valid values are "center", "inner" and "outer".
|
||||
|
||||
```clojure
|
||||
(ns app.common.types.shape
|
||||
|
@ -44,17 +45,17 @@ string (e.g. "000498f3-27fc-8000-8988-ca7d52f46843").
|
|||
(s/def ::line-height ::us/safe-number)
|
||||
```
|
||||
|
||||
`line-height` is of the type `safe-number`, defined in `app.common.spec`
|
||||
namespace, that here is imported with the name `us` (a *safe number* is a
|
||||
integer or floating point number, with a value not too big or small).
|
||||
`line-height` is of the type `safe-number`, defined in `app.common.spec` namespace, that here is
|
||||
imported with the name `us` (a _safe number_ is a integer or floating point number, with a value not
|
||||
too big or small).
|
||||
|
||||
```clojure
|
||||
(s/def ::page
|
||||
(s/keys :req-un [::id ::name ::objects ::options]))
|
||||
```
|
||||
|
||||
`page` is an object with four required arguments: `id`, `name`, `objects` and
|
||||
`options`, whose types have to be defined above.
|
||||
`page` is an object with four required arguments: `id`, `name`, `objects` and `options`, whose types
|
||||
have to be defined above.
|
||||
|
||||
```clojure
|
||||
(s/def ::column
|
||||
|
@ -66,8 +67,8 @@ integer or floating point number, with a value not too big or small).
|
|||
::gutter]))
|
||||
```
|
||||
|
||||
`column` has one required attribute `color` and five optional ones `size`,
|
||||
`type`, `item-length`, `margin` and `gutter`.
|
||||
`column` has one required attribute `color` and five optional ones `size`, `type`, `item-length`,
|
||||
`margin` and `gutter`.
|
||||
|
||||
```clojure
|
||||
(s/def ::children
|
||||
|
@ -76,8 +77,8 @@ integer or floating point number, with a value not too big or small).
|
|||
:min-count 1))
|
||||
```
|
||||
|
||||
`children` is a collection (implemented as a vector) of objects of type
|
||||
`content`, and must have a minimun lenght of 1.
|
||||
`children` is a collection (implemented as a vector) of objects of type `content`, and must have a
|
||||
minimun lenght of 1.
|
||||
|
||||
```clojure
|
||||
(defmulti animation-spec :animation-type)
|
||||
|
@ -102,11 +103,9 @@ integer or floating point number, with a value not too big or small).
|
|||
(s/multi-spec animation-spec ::animation-type))
|
||||
```
|
||||
|
||||
This is probably the most complex construct. `animation` is a multi schema
|
||||
object. It has an attribute called `animation-type` and the rest of the fields
|
||||
depend on its value. For example, if `animation-type` is "dissolve", the object
|
||||
must also have a `duration` and an `easing` attribute.
|
||||
|
||||
Other constructs should be more or less auto explicative with this guide
|
||||
and the clojure.spec manual linked above.
|
||||
This is probably the most complex construct. `animation` is a multi schema object. It has an
|
||||
attribute called `animation-type` and the rest of the fields depend on its value. For example, if
|
||||
`animation-type` is "dissolve", the object must also have a `duration` and an `easing` attribute.
|
||||
|
||||
Other constructs should be more or less auto explicative with this guide and the clojure.spec manual
|
||||
linked above.
|
||||
|
|
123
README.md
123
README.md
|
@ -1,10 +1,9 @@
|
|||
|
||||
[uri_license]: https://www.mozilla.org/en-US/MPL/2.0
|
||||
[uri_license_image]: https://img.shields.io/badge/MPL-2.0-blue.svg
|
||||
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<img style="width:100px" src="src/logo.svg" alt="PENPOT">
|
||||
<img style="width:100px" src="ui-src/logo.svg" alt="PENPOT">
|
||||
<br>
|
||||
PENPOT EXPORTER
|
||||
</h1>
|
||||
|
@ -25,9 +24,10 @@
|
|||
|
||||
![](penpotexporter.gif)
|
||||
|
||||
This is a **very early-stage** Figma plugin to export Figma files to Penpot format. For now is little more than a proof of concept, or a first scaffolding, not a fully functional exporter.
|
||||
This is a **very early-stage** Figma plugin to export Figma files to Penpot format. For now is
|
||||
little more than a proof of concept, or a first scaffolding, not a fully functional exporter.
|
||||
|
||||
## Table of contents ##
|
||||
## Table of contents
|
||||
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [Why a Penpot exporter](#why-a-penpot-exporter)
|
||||
|
@ -42,70 +42,97 @@ This is a **very early-stage** Figma plugin to export Figma files to Penpot form
|
|||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
## Why a Penpot exporter ##
|
||||
## Why a Penpot exporter
|
||||
|
||||
The aim of this plugin is to help people migrate their files from Figma to [Penpot](https://penpot.app/). Migrating work from one design tool to another was never an easy task due to the abundance of closed and non-standard formats, and this is not a different case. Our approach to better solve this situation is to release a code skeleton for the minimum version of a Figma plugin that can convert a Figma file into a Penpot annotated SVG file.
|
||||
The aim of this plugin is to help people migrate their files from Figma to
|
||||
[Penpot](https://penpot.app/). Migrating work from one design tool to another was never an easy task
|
||||
due to the abundance of closed and non-standard formats, and this is not a different case. Our
|
||||
approach to better solve this situation is to release a code skeleton for the minimum version of a
|
||||
Figma plugin that can convert a Figma file into a Penpot annotated SVG file.
|
||||
|
||||
There is a sense of urgency for this capability because there is a feeling that Adobe might force Figma to limit exports and interoperability via plugins very soon.
|
||||
There is a sense of urgency for this capability because there is a feeling that Adobe might force
|
||||
Figma to limit exports and interoperability via plugins very soon.
|
||||
|
||||
## Getting started
|
||||
|
||||
## Getting started ##
|
||||
|
||||
This plugin makes use of npm, webpack and react, and is written on TypeScript. It also includes a Penpot file builder library.
|
||||
This plugin makes use of npm, webpack and react, and is written on TypeScript. It also includes a
|
||||
Penpot file builder library.
|
||||
|
||||
### Pre-requisites
|
||||
|
||||
To use this plugin, you will need to have `node` and `npm` installed on your computer. If you don't already have these, you can download and install them from the official website ([https://nodejs.org/en/](https://nodejs.org/en/)).
|
||||
To use this plugin, you will need to have `node` and `npm` installed on your computer. If you don't
|
||||
already have these, you can download and install them from the official website
|
||||
([https://nodejs.org/en/](https://nodejs.org/en/)).
|
||||
|
||||
Once you have `node` and `npm` installed, you will need to download the source code for this plugin.
|
||||
You can do this by clicking the "Clone or download" button on the GitHub page for this project and
|
||||
then selecting "Download ZIP". Extract the ZIP file to a location on your computer.
|
||||
|
||||
Once you have `node` and `npm` installed, you will need to download the source code for this plugin. You can do this by clicking the "Clone or download" button on the GitHub page for this project and then selecting "Download ZIP". Extract the ZIP file to a location on your computer.
|
||||
### Building
|
||||
|
||||
#### For Windows users:
|
||||
|
||||
1. Open the terminal by searching for "Command Prompt" in the start menu.
|
||||
2. Use the `cd` command to navigate to the folder where the repository has been extracted. For example, if the repository is located in the `Downloads` folder, you can use the following command: `cd Downloads/penpot-exporter-figma-plugin`.
|
||||
3. Once you are in the correct folder, you can run the `npm install` command to install the dependencies, and then the `npm run build` command to build the plugin.
|
||||
2. Use the `cd` command to navigate to the folder where the repository has been extracted. For
|
||||
example, if the repository is located in the `Downloads` folder, you can use the following
|
||||
command: `cd Downloads/penpot-exporter-figma-plugin`.
|
||||
3. Once you are in the correct folder, you can run the `npm install` command to install the
|
||||
dependencies, and then the `npm run build` command to build the plugin.
|
||||
|
||||
#### For Mac users:
|
||||
|
||||
1. Open the terminal by searching for "Terminal" in the Launchpad or by using the `Command + Space` keyboard shortcut and searching for "Terminal".
|
||||
2. Use the `cd` command to navigate to the folder where the repository has been extracted. For example, if the repository is located in the `Downloads` folder, you can use the following command: `cd Downloads/penpot-exporter-figma-plugin`.
|
||||
3. Once you are in the correct folder, you can run the `npm install` command to install the dependencies, and then the `npm run build` command to build the plugin.
|
||||
1. Open the terminal by searching for "Terminal" in the Launchpad or by using the `Command + Space`
|
||||
keyboard shortcut and searching for "Terminal".
|
||||
2. Use the `cd` command to navigate to the folder where the repository has been extracted. For
|
||||
example, if the repository is located in the `Downloads` folder, you can use the following
|
||||
command: `cd Downloads/penpot-exporter-figma-plugin`.
|
||||
3. Once you are in the correct folder, you can run the `npm install` command to install the
|
||||
dependencies, and then the `npm run build` command to build the plugin.
|
||||
|
||||
#### For Linux users:
|
||||
|
||||
1. Open the terminal by using the `Ctrl + Alt + T` keyboard shortcut.
|
||||
2. Use the `cd` command to navigate to the folder where the repository has been extracted. For example, if the repository is located in the `Downloads` folder, you can use the following command: `cd Downloads/penpot-exporter-figma-plugin`.
|
||||
3. Once you are in the correct folder, you can run the `npm install` command to install the dependencies, and then the `npm run build` command to build the plugin.
|
||||
2. Use the `cd` command to navigate to the folder where the repository has been extracted. For
|
||||
example, if the repository is located in the `Downloads` folder, you can use the following
|
||||
command: `cd Downloads/penpot-exporter-figma-plugin`.
|
||||
3. Once you are in the correct folder, you can run the `npm install` command to install the
|
||||
dependencies, and then the `npm run build` command to build the plugin.
|
||||
|
||||
### Add to Figma
|
||||
`Figma menu` > `Plugins` > `Development` > `Import plugin from manifest…`
|
||||
To add the plugin to Figma, open Figma and go to the `Plugins` menu. Select `Development` and then choose `Import plugin from manifest…`.
|
||||
|
||||
`Figma menu` > `Plugins` > `Development` > `Import plugin from manifest…` To add the plugin to
|
||||
Figma, open Figma and go to the `Plugins` menu. Select `Development` and then choose
|
||||
`Import plugin from manifest…`.
|
||||
|
||||
<img src="resources/Import plugin from manifest.png" alt='Screenshot of the Plugins > Development menus open showing the, "Import plugin from manifest" option.'>
|
||||
|
||||
Select the `manifest.json` file that is located in the folder where you extracted the source code for the plugin.
|
||||
|
||||
Select the `manifest.json` file that is located in the folder where you extracted the source code
|
||||
for the plugin.
|
||||
|
||||
### To use the plugin
|
||||
|
||||
1. Select what you want to export
|
||||
2. `Figma menu` > `Plugins` > `Development` > `Penpot Exporter`
|
||||
go to the `Plugins` menu in Figma and select `Development` followed by `Penpot Exporter`.
|
||||
2. `Figma menu` > `Plugins` > `Development` > `Penpot Exporter` go to the `Plugins` menu in Figma
|
||||
and select `Development` followed by `Penpot Exporter`.
|
||||
3. This will generate a .zip file that you can import into Penpot.
|
||||
|
||||
## Call to the community
|
||||
|
||||
## Call to the community ##
|
||||
|
||||
Answering to the interest expressed by community members to build the plugin by themselves, at the Penpot team we decided to help solve the need without having to depend on our current product priorities. That is why we have published this bare minimum version of the plugin, unsatisfactory in itself, but it unlocks the possibility for others to continue the task.
|
||||
Answering to the interest expressed by community members to build the plugin by themselves, at the
|
||||
Penpot team we decided to help solve the need without having to depend on our current product
|
||||
priorities. That is why we have published this bare minimum version of the plugin, unsatisfactory in
|
||||
itself, but it unlocks the possibility for others to continue the task.
|
||||
|
||||
Yes, we are asking for help. 🤗
|
||||
|
||||
We have explained this approach in a [community post](https://community.penpot.app/t/figma-file-importer/1684). Feel free to join the conversation there.
|
||||
We have explained this approach in a
|
||||
[community post](https://community.penpot.app/t/figma-file-importer/1684). Feel free to join the
|
||||
conversation there.
|
||||
|
||||
## What can this plugin currently import? ##
|
||||
## What can this plugin currently import?
|
||||
|
||||
As mentioned above, this plugin gets you to a starting point. Things that are currently included in the import are:
|
||||
As mentioned above, this plugin gets you to a starting point. Things that are currently included in
|
||||
the import are:
|
||||
|
||||
- **Basic shapes** (rectangles, ellipses).
|
||||
- **Frames** (Boards in Penpot).
|
||||
|
@ -114,20 +141,27 @@ As mentioned above, this plugin gets you to a starting point. Things that are cu
|
|||
- **Texts** (basic support. Only fonts available on Google fonts).
|
||||
- **Images** (basic support)
|
||||
|
||||
## Limitations ##
|
||||
The obvious limitations are the features that are in Figma but not in Penpot or work differently in both tools so they can not be easily converted. We leave some comments below about the ones that are commonly considered more important:
|
||||
## Limitations
|
||||
|
||||
- **Autolayout**: Not in Penpot yet but in a very advanced state of development. There will be news soon.
|
||||
- **Components**: Currently very different from their counterparts at Figma. However, Penpot components are under a rework that we expect will make the conversion easier.
|
||||
- **Variants**: Not expected in the short term. Also, we are thinking of different solutions to solve component states, things that eventually could make it difficult to import.
|
||||
The obvious limitations are the features that are in Figma but not in Penpot or work differently in
|
||||
both tools so they can not be easily converted. We leave some comments below about the ones that are
|
||||
commonly considered more important:
|
||||
|
||||
- **Autolayout**: Not in Penpot yet but in a very advanced state of development. There will be news
|
||||
soon.
|
||||
- **Components**: Currently very different from their counterparts at Figma. However, Penpot
|
||||
components are under a rework that we expect will make the conversion easier.
|
||||
- **Variants**: Not expected in the short term. Also, we are thinking of different solutions to
|
||||
solve component states, things that eventually could make it difficult to import.
|
||||
|
||||
## Contributing
|
||||
|
||||
## Contributing ##
|
||||
|
||||
If you want to make many people very happy and help us build this code skeleton for the minimum version of the Figma plugin, a further effort will be needed to have a satisfactory import experience.
|
||||
If you want to make many people very happy and help us build this code skeleton for the minimum
|
||||
version of the Figma plugin, a further effort will be needed to have a satisfactory import
|
||||
experience.
|
||||
|
||||
For instance, it will be interesting to add:
|
||||
|
||||
- Strokes
|
||||
- Fills with radial gradients
|
||||
- Paths
|
||||
|
@ -137,10 +171,11 @@ For instance, it will be interesting to add:
|
|||
- Constraints
|
||||
- ...
|
||||
|
||||
Motivated to contribute? Take a look at our [Contributing Guide](https://help.penpot.app/contributing-guide/) that explains our guidelines (they're for the Penpot Core, but are mostly of application here too).
|
||||
Motivated to contribute? Take a look at our
|
||||
[Contributing Guide](https://help.penpot.app/contributing-guide/) that explains our guidelines
|
||||
(they're for the Penpot Core, but are mostly of application here too).
|
||||
|
||||
|
||||
## License ##
|
||||
## License
|
||||
|
||||
```
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
|
@ -149,4 +184,6 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
|
||||
Copyright (c) KALEIDOS INC
|
||||
```
|
||||
Penpot and the Penpot exporter plugin are Kaleidos’ [open source projects](https://kaleidos.net/products)
|
||||
|
||||
Penpot and the Penpot exporter plugin are Kaleidos’
|
||||
[open source projects](https://kaleidos.net/products)
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
{
|
||||
"name": "Penpot Exporter",
|
||||
"id": "",
|
||||
"id": "1362760353975083275",
|
||||
"api": "1.0.0",
|
||||
"main": "dist/code.js",
|
||||
"ui": "dist/ui.html",
|
||||
"editorType": ["figma", "figjam"]
|
||||
"ui": "dist/index.html",
|
||||
"editorType": ["figma"],
|
||||
"networkAccess": { "allowedDomains": ["none"] },
|
||||
"documentAccess": "dynamic-page"
|
||||
}
|
||||
|
|
9109
package-lock.json
generated
Normal file
9109
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
76
package.json
76
package.json
|
@ -1,37 +1,59 @@
|
|||
{
|
||||
"name": "penpot-exporter",
|
||||
"id": "1161652283781700708",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.0",
|
||||
"description": "Penpot exporter",
|
||||
"main": "code.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"watch": "webpack watch"
|
||||
"build": "npm run build:ui && npm run build:main -- --minify",
|
||||
"build:main": "esbuild plugin-src/code.ts --bundle --outfile=dist/code.js --target=es2016",
|
||||
"build:ui": "vite build --minify esbuild --emptyOutDir=false",
|
||||
"build:watch": "concurrently -n widget,iframe \"npm run build:main -- --watch\" \"npm run build:ui -- --watch\"",
|
||||
"dev": "concurrently -n tsc,build,vite 'npm:tsc:watch' 'npm:build:watch' 'vite'",
|
||||
"lint": "concurrently \"npm:lint:*\"",
|
||||
"lint:eslint": "eslint .",
|
||||
"lint:stylelint": "stylelint ui-src/**.css",
|
||||
"lint:prettier": "prettier --check .",
|
||||
"lint:tsc-ui": "tsc -p ui-src/tsconfig.json --noEmit --pretty false",
|
||||
"lint:tsc-plugin": "tsc -p plugin-src/tsconfig.json --noEmit --pretty false",
|
||||
"fix-lint": "concurrently \"npm:fix-lint:*\"",
|
||||
"fix-lint:eslint": "eslint . --fix",
|
||||
"fix-lint:stylelint": "stylelint ui-src/**.css --fix",
|
||||
"fix-lint:prettier": "prettier --write ."
|
||||
},
|
||||
"author": "Kaleidos",
|
||||
"license": "MPL2.0",
|
||||
"devDependencies": {
|
||||
"@figma/plugin-typings": "*",
|
||||
"@types/node": "^16.11.64",
|
||||
"css-loader": "^6.2.0",
|
||||
"html-webpack-inline-source-plugin": "0.0.10",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"style-loader": "^3.2.1",
|
||||
"ts-loader": "^9.2.5",
|
||||
"typescript": "^4.3.5",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.51.1",
|
||||
"webpack-cli": "^4.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@figma-plugin/helpers": "^0.15.2",
|
||||
"@types/react": "^17.0.19",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"crypto": "^1.0.1",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"slugify": "^1.6.5"
|
||||
"react": "^18.2",
|
||||
"react-dom": "^18.2",
|
||||
"slugify": "^1.6",
|
||||
"svg-path-parser": "^1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/changelog-github": "^0.5",
|
||||
"@changesets/cli": "^2.27",
|
||||
"@figma/eslint-plugin-figma-plugins": "^0.15",
|
||||
"@figma/plugin-typings": "^1.90",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3",
|
||||
"@types/react": "^18.2",
|
||||
"@types/react-dom": "^18.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5",
|
||||
"@typescript-eslint/parser": "^7.5",
|
||||
"@types/svg-path-parser": "^1.1",
|
||||
"@vitejs/plugin-react-swc": "^3.6",
|
||||
"concurrently": "^8.2",
|
||||
"esbuild": "^0.20",
|
||||
"eslint": "^8.57",
|
||||
"eslint-config-prettier": "^9.1",
|
||||
"eslint-plugin-prettier": "^5.1",
|
||||
"eslint-plugin-react": "^7.34",
|
||||
"prettier": "^3.2",
|
||||
"stylelint": "^16.3",
|
||||
"stylelint-config-standard": "^36.0",
|
||||
"tsconfig-paths-webpack-plugin": "^4.1",
|
||||
"typescript": "^5.4",
|
||||
"vite": "^5.2",
|
||||
"vite-plugin-singlefile": "^2.0",
|
||||
"vite-plugin-svgr": "^4.2",
|
||||
"vite-tsconfig-paths": "^4.3"
|
||||
}
|
||||
}
|
||||
|
|
19
plugin-src/code.ts
Normal file
19
plugin-src/code.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {
|
||||
handleCancelMessage,
|
||||
handleExportMessage,
|
||||
handleResizeMessage
|
||||
} from '@plugin/messageHandlers';
|
||||
|
||||
figma.showUI(__html__, { themeColors: true, height: 200, width: 300 });
|
||||
|
||||
figma.ui.onmessage = async msg => {
|
||||
if (msg.type === 'export') {
|
||||
await handleExportMessage();
|
||||
}
|
||||
if (msg.type === 'cancel') {
|
||||
handleCancelMessage();
|
||||
}
|
||||
if (msg.type === 'resize') {
|
||||
handleResizeMessage(msg.width, msg.height);
|
||||
}
|
||||
};
|
3
plugin-src/messageHandlers/handleCancelMessage.ts
Normal file
3
plugin-src/messageHandlers/handleCancelMessage.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function handleCancelMessage() {
|
||||
figma.closePlugin();
|
||||
}
|
8
plugin-src/messageHandlers/handleExportMessage.ts
Normal file
8
plugin-src/messageHandlers/handleExportMessage.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { transformDocumentNode } from '@plugin/transformers';
|
||||
|
||||
export async function handleExportMessage() {
|
||||
await figma.loadAllPagesAsync();
|
||||
|
||||
const penpotNode = await transformDocumentNode(figma.root);
|
||||
figma.ui.postMessage({ type: 'FIGMAFILE', data: penpotNode });
|
||||
}
|
3
plugin-src/messageHandlers/handleResizeMessage.ts
Normal file
3
plugin-src/messageHandlers/handleResizeMessage.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export function handleResizeMessage(width: number, height: number) {
|
||||
figma.ui.resize(width, height);
|
||||
}
|
3
plugin-src/messageHandlers/index.ts
Normal file
3
plugin-src/messageHandlers/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './handleCancelMessage';
|
||||
export * from './handleExportMessage';
|
||||
export * from './handleResizeMessage';
|
10
plugin-src/transformers/index.ts
Normal file
10
plugin-src/transformers/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export * from './transformDocumentNode';
|
||||
export * from './transformEllipseNode';
|
||||
export * from './transformFrameNode';
|
||||
export * from './transformGroupNode';
|
||||
export * from './transformImageNode';
|
||||
export * from './transformPageNode';
|
||||
export * from './transformPathNode';
|
||||
export * from './transformRectangleNode';
|
||||
export * from './transformSceneNode';
|
||||
export * from './transformTextNode';
|
9
plugin-src/transformers/partials/index.ts
Normal file
9
plugin-src/transformers/partials/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export * from './transformBlend';
|
||||
export * from './transformChildren';
|
||||
export * from './transformDimensionAndPosition';
|
||||
export * from './transformFills';
|
||||
export * from './transformProportion';
|
||||
export * from './transformSceneNode';
|
||||
export * from './transformStrokes';
|
||||
export * from './transformTextStyle';
|
||||
export * from './transformVectorPaths';
|
12
plugin-src/transformers/partials/transformBlend.ts
Normal file
12
plugin-src/transformers/partials/transformBlend.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { translateBlendMode } from '@plugin/translators';
|
||||
|
||||
import { ShapeAttributes } from '@ui/lib/types/shape/shapeAttributes';
|
||||
|
||||
export const transformBlend = (
|
||||
node: SceneNodeMixin & MinimalBlendMixin
|
||||
): Partial<ShapeAttributes> => {
|
||||
return {
|
||||
blendMode: translateBlendMode(node.blendMode),
|
||||
opacity: !node.visible ? 0 : node.opacity // @TODO: check this. If we use the property hidden and it's hidden, it won't export
|
||||
};
|
||||
};
|
16
plugin-src/transformers/partials/transformChildren.ts
Normal file
16
plugin-src/transformers/partials/transformChildren.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { transformSceneNode } from '@plugin/transformers';
|
||||
|
||||
import { PenpotNode } from '@ui/lib/types/penpotNode';
|
||||
import { Children } from '@ui/lib/types/utils/children';
|
||||
|
||||
export const transformChildren = async (
|
||||
node: ChildrenMixin,
|
||||
baseX: number = 0,
|
||||
baseY: number = 0
|
||||
): Promise<Children> => {
|
||||
return {
|
||||
children: (
|
||||
await Promise.all(node.children.map(child => transformSceneNode(child, baseX, baseY)))
|
||||
).filter((child): child is PenpotNode => !!child)
|
||||
};
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
import { ShapeGeomAttributes } from '@ui/lib/types/shape/shapeGeomAttributes';
|
||||
|
||||
export const transformDimensionAndPosition = (
|
||||
node: DimensionAndPositionMixin,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
): ShapeGeomAttributes => {
|
||||
return {
|
||||
x: node.x + baseX,
|
||||
y: node.y + baseY,
|
||||
width: node.width,
|
||||
height: node.height
|
||||
};
|
||||
};
|
11
plugin-src/transformers/partials/transformFills.ts
Normal file
11
plugin-src/transformers/partials/transformFills.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { translateFills } from '@plugin/translators';
|
||||
|
||||
import { ShapeAttributes } from '@ui/lib/types/shape/shapeAttributes';
|
||||
|
||||
export const transformFills = (
|
||||
node: MinimalFillsMixin & DimensionAndPositionMixin
|
||||
): Partial<ShapeAttributes> => {
|
||||
return {
|
||||
fills: translateFills(node.fills, node.width, node.height)
|
||||
};
|
||||
};
|
7
plugin-src/transformers/partials/transformProportion.ts
Normal file
7
plugin-src/transformers/partials/transformProportion.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { ShapeAttributes } from '@ui/lib/types/shape/shapeAttributes';
|
||||
|
||||
export const transformProportion = (node: LayoutMixin): Partial<ShapeAttributes> => {
|
||||
return {
|
||||
proportionLock: node.constrainProportions
|
||||
};
|
||||
};
|
8
plugin-src/transformers/partials/transformSceneNode.ts
Normal file
8
plugin-src/transformers/partials/transformSceneNode.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { ShapeAttributes } from '@ui/lib/types/shape/shapeAttributes';
|
||||
|
||||
export const transformSceneNode = (node: SceneNodeMixin): Partial<ShapeAttributes> => {
|
||||
return {
|
||||
blocked: node.locked,
|
||||
hidden: false // @TODO: check this. it won't export if we hide it
|
||||
};
|
||||
};
|
30
plugin-src/transformers/partials/transformStrokes.ts
Normal file
30
plugin-src/transformers/partials/transformStrokes.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { translateStrokes } from '@plugin/translators';
|
||||
|
||||
import { ShapeAttributes } from '@ui/lib/types/shape/shapeAttributes';
|
||||
|
||||
const isVectorLike = (node: GeometryMixin | VectorLikeMixin): node is VectorLikeMixin => {
|
||||
return 'vectorNetwork' in node;
|
||||
};
|
||||
|
||||
const isIndividualStrokes = (
|
||||
node: GeometryMixin | IndividualStrokesMixin
|
||||
): node is IndividualStrokesMixin => {
|
||||
return 'strokeTopWeight' in node;
|
||||
};
|
||||
|
||||
const hasFillGeometry = (node: GeometryMixin): boolean => {
|
||||
return node.fillGeometry.length > 0;
|
||||
};
|
||||
|
||||
export const transformStrokes = (
|
||||
node: GeometryMixin | (GeometryMixin & IndividualStrokesMixin)
|
||||
): Partial<ShapeAttributes> => {
|
||||
return {
|
||||
strokes: translateStrokes(
|
||||
node,
|
||||
hasFillGeometry(node),
|
||||
isVectorLike(node) ? node.vectorNetwork : undefined,
|
||||
isIndividualStrokes(node) ? node : undefined
|
||||
)
|
||||
};
|
||||
};
|
29
plugin-src/transformers/partials/transformTextStyle.ts
Normal file
29
plugin-src/transformers/partials/transformTextStyle.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { translateTextDecoration, translateTextTransform } from '@plugin/translators';
|
||||
|
||||
import { TextStyle } from '@ui/lib/types/text/textContent';
|
||||
|
||||
export const transformTextStyle = (
|
||||
node: Pick<
|
||||
StyledTextSegment,
|
||||
| 'characters'
|
||||
| 'start'
|
||||
| 'end'
|
||||
| 'fontName'
|
||||
| 'fontSize'
|
||||
| 'fontWeight'
|
||||
| 'lineHeight'
|
||||
| 'letterSpacing'
|
||||
| 'textCase'
|
||||
| 'textDecoration'
|
||||
| 'fills'
|
||||
>
|
||||
): Partial<TextStyle> => {
|
||||
return {
|
||||
fontFamily: node.fontName.family,
|
||||
fontSize: node.fontSize.toString(),
|
||||
fontStyle: node.fontName.style,
|
||||
fontWeight: node.fontWeight.toString(),
|
||||
textDecoration: translateTextDecoration(node),
|
||||
textTransform: translateTextTransform(node)
|
||||
};
|
||||
};
|
28
plugin-src/transformers/partials/transformVectorPaths.ts
Normal file
28
plugin-src/transformers/partials/transformVectorPaths.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { translateVectorPaths } from '@plugin/translators';
|
||||
|
||||
import { PathAttributes } from '@ui/lib/types/path/pathAttributes';
|
||||
|
||||
const getVectorPaths = (node: VectorNode | StarNode | LineNode | PolygonNode): VectorPaths => {
|
||||
switch (node.type) {
|
||||
case 'STAR':
|
||||
case 'POLYGON':
|
||||
return node.fillGeometry;
|
||||
case 'VECTOR':
|
||||
return node.vectorPaths;
|
||||
case 'LINE':
|
||||
return node.strokeGeometry;
|
||||
}
|
||||
};
|
||||
|
||||
export const transformVectorPaths = (
|
||||
node: VectorNode | StarNode | LineNode | PolygonNode,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
): PathAttributes => {
|
||||
const vectorPaths = getVectorPaths(node);
|
||||
|
||||
return {
|
||||
type: 'path',
|
||||
content: translateVectorPaths(vectorPaths, baseX + node.x, baseY + node.y)
|
||||
};
|
||||
};
|
10
plugin-src/transformers/transformDocumentNode.ts
Normal file
10
plugin-src/transformers/transformDocumentNode.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { PenpotDocument } from '@ui/lib/types/penpotDocument';
|
||||
|
||||
import { transformPageNode } from '.';
|
||||
|
||||
export const transformDocumentNode = async (node: DocumentNode): Promise<PenpotDocument> => {
|
||||
return {
|
||||
name: node.name,
|
||||
children: await Promise.all(node.children.map(child => transformPageNode(child)))
|
||||
};
|
||||
};
|
27
plugin-src/transformers/transformEllipseNode.ts
Normal file
27
plugin-src/transformers/transformEllipseNode.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import {
|
||||
transformBlend,
|
||||
transformDimensionAndPosition,
|
||||
transformFills,
|
||||
transformProportion,
|
||||
transformSceneNode,
|
||||
transformStrokes
|
||||
} from '@plugin/transformers/partials';
|
||||
|
||||
import { CircleShape } from '@ui/lib/types/circle/circleShape';
|
||||
|
||||
export const transformEllipseNode = (
|
||||
node: EllipseNode,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
): CircleShape => {
|
||||
return {
|
||||
type: 'circle',
|
||||
name: node.name,
|
||||
...transformFills(node),
|
||||
...transformStrokes(node),
|
||||
...transformDimensionAndPosition(node, baseX, baseY),
|
||||
...transformSceneNode(node),
|
||||
...transformBlend(node),
|
||||
...transformProportion(node)
|
||||
};
|
||||
};
|
41
plugin-src/transformers/transformFrameNode.ts
Normal file
41
plugin-src/transformers/transformFrameNode.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import {
|
||||
transformBlend,
|
||||
transformChildren,
|
||||
transformDimensionAndPosition,
|
||||
transformFills,
|
||||
transformProportion,
|
||||
transformSceneNode,
|
||||
transformStrokes
|
||||
} from '@plugin/transformers/partials';
|
||||
|
||||
import { FrameShape } from '@ui/lib/types/frame/frameShape';
|
||||
|
||||
const isSectionNode = (node: FrameNode | SectionNode): node is SectionNode => {
|
||||
return node.type === 'SECTION';
|
||||
};
|
||||
|
||||
export const transformFrameNode = async (
|
||||
node: FrameNode | SectionNode,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
): Promise<FrameShape> => {
|
||||
return {
|
||||
type: 'frame',
|
||||
name: node.name,
|
||||
showContent: isSectionNode(node) ? true : !node.clipsContent,
|
||||
...transformFills(node),
|
||||
// Figma API does not expose strokes for sections,
|
||||
// they plan to add it in the future. Refactor this when available.
|
||||
// @see: https://forum.figma.com/t/why-are-strokes-not-available-on-section-nodes/41658
|
||||
...(isSectionNode(node) ? [] : transformStrokes(node)),
|
||||
...(await transformChildren(node, baseX + node.x, baseY + node.y)),
|
||||
...transformDimensionAndPosition(node, baseX, baseY),
|
||||
// Figma API does not expose blend modes for sections,
|
||||
// they plan to add it in the future. Refactor this when available.
|
||||
// @see: https://forum.figma.com/t/add-a-blendmode-property-for-sectionnode/58560
|
||||
...(isSectionNode(node) ? [] : transformBlend(node)),
|
||||
...transformSceneNode(node),
|
||||
// Figma API does not expose constraints proportions for sections
|
||||
...(isSectionNode(node) ? [] : transformProportion(node))
|
||||
};
|
||||
};
|
23
plugin-src/transformers/transformGroupNode.ts
Normal file
23
plugin-src/transformers/transformGroupNode.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import {
|
||||
transformBlend,
|
||||
transformDimensionAndPosition,
|
||||
transformSceneNode
|
||||
} from '@plugin/transformers/partials';
|
||||
import { transformChildren } from '@plugin/transformers/partials';
|
||||
|
||||
import { GroupShape } from '@ui/lib/types/group/groupShape';
|
||||
|
||||
export const transformGroupNode = async (
|
||||
node: GroupNode,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
): Promise<GroupShape> => {
|
||||
return {
|
||||
type: 'group',
|
||||
name: node.name,
|
||||
...(await transformChildren(node, baseX, baseY)),
|
||||
...transformDimensionAndPosition(node, baseX, baseY),
|
||||
...transformSceneNode(node),
|
||||
...transformBlend(node)
|
||||
};
|
||||
};
|
28
plugin-src/transformers/transformImageNode.ts
Normal file
28
plugin-src/transformers/transformImageNode.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { transformDimensionAndPosition } from '@plugin/transformers/partials';
|
||||
import { detectMimeType } from '@plugin/utils';
|
||||
|
||||
import { ImageShape } from '@ui/lib/types/image/imageShape';
|
||||
|
||||
export const transformImageNode = async (
|
||||
node: SceneNode,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
): Promise<ImageShape> => {
|
||||
let dataUri = '';
|
||||
if ('exportAsync' in node) {
|
||||
const value = await node.exportAsync({ format: 'PNG' });
|
||||
const b64 = figma.base64Encode(value);
|
||||
dataUri = 'data:' + detectMimeType(b64) + ';base64,' + b64;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'image',
|
||||
name: node.name,
|
||||
metadata: {
|
||||
width: node.width,
|
||||
height: node.height
|
||||
},
|
||||
dataUri: dataUri,
|
||||
...transformDimensionAndPosition(node, baseX, baseY)
|
||||
};
|
||||
};
|
10
plugin-src/transformers/transformPageNode.ts
Normal file
10
plugin-src/transformers/transformPageNode.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { transformChildren } from '@plugin/transformers/partials';
|
||||
|
||||
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
||||
|
||||
export const transformPageNode = async (node: PageNode): Promise<PenpotPage> => {
|
||||
return {
|
||||
name: node.name,
|
||||
...(await transformChildren(node))
|
||||
};
|
||||
};
|
32
plugin-src/transformers/transformPathNode.ts
Normal file
32
plugin-src/transformers/transformPathNode.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import {
|
||||
transformBlend,
|
||||
transformDimensionAndPosition,
|
||||
transformFills,
|
||||
transformProportion,
|
||||
transformSceneNode,
|
||||
transformStrokes,
|
||||
transformVectorPaths
|
||||
} from '@plugin/transformers/partials';
|
||||
|
||||
import { PathShape } from '@ui/lib/types/path/pathShape';
|
||||
|
||||
const hasFillGeometry = (node: VectorNode | StarNode | LineNode | PolygonNode): boolean => {
|
||||
return 'fillGeometry' in node && node.fillGeometry.length > 0;
|
||||
};
|
||||
|
||||
export const transformPathNode = (
|
||||
node: VectorNode | StarNode | LineNode | PolygonNode,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
): PathShape => {
|
||||
return {
|
||||
name: node.name,
|
||||
...(hasFillGeometry(node) ? transformFills(node) : []),
|
||||
...transformStrokes(node),
|
||||
...transformVectorPaths(node, baseX, baseY),
|
||||
...transformDimensionAndPosition(node, baseX, baseY),
|
||||
...transformSceneNode(node),
|
||||
...transformBlend(node),
|
||||
...transformProportion(node)
|
||||
};
|
||||
};
|
27
plugin-src/transformers/transformRectangleNode.ts
Normal file
27
plugin-src/transformers/transformRectangleNode.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import {
|
||||
transformBlend,
|
||||
transformDimensionAndPosition,
|
||||
transformFills,
|
||||
transformProportion,
|
||||
transformSceneNode,
|
||||
transformStrokes
|
||||
} from '@plugin/transformers/partials';
|
||||
|
||||
import { RectShape } from '@ui/lib/types/rect/rectShape';
|
||||
|
||||
export const transformRectangleNode = (
|
||||
node: RectangleNode,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
): RectShape => {
|
||||
return {
|
||||
type: 'rect',
|
||||
name: node.name,
|
||||
...transformFills(node),
|
||||
...transformStrokes(node),
|
||||
...transformDimensionAndPosition(node, baseX, baseY),
|
||||
...transformSceneNode(node),
|
||||
...transformBlend(node),
|
||||
...transformProportion(node)
|
||||
};
|
||||
};
|
53
plugin-src/transformers/transformSceneNode.ts
Normal file
53
plugin-src/transformers/transformSceneNode.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { calculateAdjustment } from '@plugin/utils';
|
||||
|
||||
import { PenpotNode } from '@ui/lib/types/penpotNode';
|
||||
|
||||
import {
|
||||
transformEllipseNode,
|
||||
transformFrameNode,
|
||||
transformGroupNode,
|
||||
transformImageNode,
|
||||
transformPathNode,
|
||||
transformRectangleNode,
|
||||
transformTextNode
|
||||
} from '.';
|
||||
|
||||
export const transformSceneNode = async (
|
||||
node: SceneNode,
|
||||
baseX: number = 0,
|
||||
baseY: number = 0
|
||||
): Promise<PenpotNode | undefined> => {
|
||||
// @TODO: when penpot 2.0, manage image as fills for the basic types
|
||||
if (
|
||||
'fills' in node &&
|
||||
node.fills !== figma.mixed &&
|
||||
node.fills.find(fill => fill.type === 'IMAGE')
|
||||
) {
|
||||
// If the nested frames extended the bounds of the rasterized image, we need to
|
||||
// account for this both in position on the canvas and the calculated width and
|
||||
// height of the image.
|
||||
const [adjustedX, adjustedY] = calculateAdjustment(node);
|
||||
return await transformImageNode(node, baseX + adjustedX, baseY + adjustedY);
|
||||
}
|
||||
|
||||
switch (node.type) {
|
||||
case 'RECTANGLE':
|
||||
return transformRectangleNode(node, baseX, baseY);
|
||||
case 'ELLIPSE':
|
||||
return transformEllipseNode(node, baseX, baseY);
|
||||
case 'SECTION':
|
||||
case 'FRAME':
|
||||
return await transformFrameNode(node, baseX, baseY);
|
||||
case 'GROUP':
|
||||
return await transformGroupNode(node, baseX, baseY);
|
||||
case 'TEXT':
|
||||
return transformTextNode(node, baseX, baseY);
|
||||
case 'STAR':
|
||||
case 'POLYGON':
|
||||
case 'VECTOR':
|
||||
case 'LINE':
|
||||
return transformPathNode(node, baseX, baseY);
|
||||
}
|
||||
|
||||
console.error(`Unsupported node type: ${node.type}`);
|
||||
};
|
49
plugin-src/transformers/transformTextNode.ts
Normal file
49
plugin-src/transformers/transformTextNode.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import {
|
||||
transformBlend,
|
||||
transformDimensionAndPosition,
|
||||
transformFills,
|
||||
transformProportion,
|
||||
transformSceneNode,
|
||||
transformTextStyle
|
||||
} from '@plugin/transformers/partials';
|
||||
import { translateStyledTextSegments } from '@plugin/translators';
|
||||
|
||||
import { TextShape } from '@ui/lib/types/text/textShape';
|
||||
|
||||
export const transformTextNode = (node: TextNode, baseX: number, baseY: number): TextShape => {
|
||||
const styledTextSegments = node.getStyledTextSegments([
|
||||
'fontName',
|
||||
'fontSize',
|
||||
'fontWeight',
|
||||
'lineHeight',
|
||||
'letterSpacing',
|
||||
'textCase',
|
||||
'textDecoration',
|
||||
'fills'
|
||||
]);
|
||||
|
||||
return {
|
||||
type: 'text',
|
||||
name: node.name,
|
||||
content: {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph-set',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: translateStyledTextSegments(styledTextSegments, node.width, node.height),
|
||||
...(styledTextSegments.length ? transformTextStyle(styledTextSegments[0]) : {}),
|
||||
...transformFills(node)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
...transformDimensionAndPosition(node, baseX, baseY),
|
||||
...transformSceneNode(node),
|
||||
...transformBlend(node),
|
||||
...transformProportion(node)
|
||||
};
|
||||
};
|
7
plugin-src/translators/index.ts
Normal file
7
plugin-src/translators/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export * from './translateBlendMode';
|
||||
export * from './translateFills';
|
||||
export * from './translateStrokes';
|
||||
export * from './translateStyledTextSegments';
|
||||
export * from './translateTextDecoration';
|
||||
export * from './translateTextTransform';
|
||||
export * from './translateVectorPaths';
|
46
plugin-src/translators/translateBlendMode.ts
Normal file
46
plugin-src/translators/translateBlendMode.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { BlendMode as PenpotBlendMode } from '@ui/lib/types/utils/blendModes';
|
||||
|
||||
export const translateBlendMode = (blendMode: BlendMode): PenpotBlendMode => {
|
||||
switch (blendMode) {
|
||||
//@TODO: is not translatable in penpot, this is the closest one
|
||||
case 'PASS_THROUGH':
|
||||
case 'NORMAL':
|
||||
return 'normal';
|
||||
//@TODO: is not translatable in penpot, this is the closest one
|
||||
case 'LINEAR_BURN':
|
||||
case 'DARKEN':
|
||||
return 'darken';
|
||||
case 'MULTIPLY':
|
||||
return 'multiply';
|
||||
case 'COLOR_BURN':
|
||||
return 'color-burn';
|
||||
case 'LIGHTEN':
|
||||
return 'lighten';
|
||||
case 'SCREEN':
|
||||
return 'screen';
|
||||
//@TODO: is not translatable in penpot, this is the closest one
|
||||
case 'LINEAR_DODGE':
|
||||
case 'COLOR_DODGE':
|
||||
return 'color-dodge';
|
||||
case 'OVERLAY':
|
||||
return 'overlay';
|
||||
case 'SOFT_LIGHT':
|
||||
return 'soft-light';
|
||||
case 'HARD_LIGHT':
|
||||
return 'hard-light';
|
||||
case 'DIFFERENCE':
|
||||
return 'difference';
|
||||
case 'EXCLUSION':
|
||||
return 'exclusion';
|
||||
case 'HUE':
|
||||
return 'hue';
|
||||
case 'SATURATION':
|
||||
return 'saturation';
|
||||
case 'COLOR':
|
||||
return 'color';
|
||||
case 'LUMINOSITY':
|
||||
return 'luminosity';
|
||||
default:
|
||||
return 'normal';
|
||||
}
|
||||
};
|
69
plugin-src/translators/translateFills.ts
Normal file
69
plugin-src/translators/translateFills.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { rgbToHex } from '@plugin/utils';
|
||||
import { calculateLinearGradient } from '@plugin/utils/calculateLinearGradient';
|
||||
|
||||
import { Fill } from '@ui/lib/types/utils/fill';
|
||||
|
||||
export const translateFill = (fill: Paint, width: number, height: number): Fill | undefined => {
|
||||
switch (fill.type) {
|
||||
case 'SOLID':
|
||||
return translateSolidFill(fill);
|
||||
case 'GRADIENT_LINEAR':
|
||||
return translateGradientLinearFill(fill, width, height);
|
||||
}
|
||||
|
||||
console.error(`Unsupported fill type: ${fill.type}`);
|
||||
};
|
||||
|
||||
export const translateFills = (
|
||||
fills: readonly Paint[] | typeof figma.mixed,
|
||||
width: number,
|
||||
height: number
|
||||
): Fill[] => {
|
||||
const figmaFills = fills === figma.mixed ? [] : fills;
|
||||
const penpotFills: Fill[] = [];
|
||||
|
||||
for (const fill of figmaFills) {
|
||||
const penpotFill = translateFill(fill, width, height);
|
||||
if (penpotFill) {
|
||||
// colors are applied in reverse order in Figma, that's why we unshift
|
||||
penpotFills.unshift(penpotFill);
|
||||
}
|
||||
}
|
||||
|
||||
return penpotFills;
|
||||
};
|
||||
|
||||
const translateSolidFill = (fill: SolidPaint): Fill => {
|
||||
return {
|
||||
fillColor: rgbToHex(fill.color),
|
||||
fillOpacity: !fill.visible ? 0 : fill.opacity
|
||||
};
|
||||
};
|
||||
|
||||
const translateGradientLinearFill = (fill: GradientPaint, width: number, height: number): Fill => {
|
||||
const points = calculateLinearGradient(width, height, fill.gradientTransform);
|
||||
|
||||
return {
|
||||
fillColorGradient: {
|
||||
type: 'linear',
|
||||
startX: points.start[0] / width,
|
||||
startY: points.start[1] / height,
|
||||
endX: points.end[0] / width,
|
||||
endY: points.end[1] / height,
|
||||
width: 1,
|
||||
stops: [
|
||||
{
|
||||
color: rgbToHex(fill.gradientStops[0].color),
|
||||
offset: fill.gradientStops[0].position,
|
||||
opacity: fill.gradientStops[0].color.a * (fill.opacity ?? 1)
|
||||
},
|
||||
{
|
||||
color: rgbToHex(fill.gradientStops[1].color),
|
||||
offset: fill.gradientStops[1].position,
|
||||
opacity: fill.gradientStops[1].color.a * (fill.opacity ?? 1)
|
||||
}
|
||||
]
|
||||
},
|
||||
fillOpacity: fill.visible === false ? 0 : undefined
|
||||
};
|
||||
};
|
84
plugin-src/translators/translateStrokes.ts
Normal file
84
plugin-src/translators/translateStrokes.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { translateFill } from '@plugin/translators/translateFills';
|
||||
|
||||
import { Stroke, StrokeAlignment, StrokeCaps } from '@ui/lib/types/utils/stroke';
|
||||
|
||||
export const translateStrokes = (
|
||||
nodeStrokes: MinimalStrokesMixin,
|
||||
hasFillGeometry?: boolean,
|
||||
vectorNetwork?: VectorNetwork,
|
||||
individualStrokes?: IndividualStrokesMixin
|
||||
): Stroke[] => {
|
||||
return nodeStrokes.strokes.map((paint, index) => {
|
||||
const fill = translateFill(paint, 0, 0);
|
||||
const stroke: Stroke = {
|
||||
strokeColor: fill?.fillColor,
|
||||
strokeOpacity: fill?.fillOpacity,
|
||||
strokeWidth: translateStrokeWeight(nodeStrokes.strokeWeight, individualStrokes),
|
||||
strokeAlignment: translateStrokeAlignment(nodeStrokes.strokeAlign),
|
||||
strokeStyle: nodeStrokes.dashPattern.length ? 'dashed' : 'solid'
|
||||
};
|
||||
|
||||
if (!hasFillGeometry && index === 0 && vectorNetwork && vectorNetwork.vertices.length) {
|
||||
stroke.strokeCapStart = translateStrokeCap(vectorNetwork.vertices[0]);
|
||||
stroke.strokeCapEnd = translateStrokeCap(
|
||||
vectorNetwork.vertices[vectorNetwork.vertices.length - 1]
|
||||
);
|
||||
}
|
||||
|
||||
return stroke;
|
||||
});
|
||||
};
|
||||
|
||||
const translateStrokeWeight = (
|
||||
strokeWeight: number | typeof figma.mixed,
|
||||
individualStrokes?: IndividualStrokesMixin
|
||||
): number => {
|
||||
if (strokeWeight !== figma.mixed) {
|
||||
return strokeWeight;
|
||||
}
|
||||
|
||||
if (!individualStrokes) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
individualStrokes.strokeTopWeight,
|
||||
individualStrokes.strokeRightWeight,
|
||||
individualStrokes.strokeBottomWeight,
|
||||
individualStrokes.strokeLeftWeight
|
||||
);
|
||||
};
|
||||
|
||||
const translateStrokeAlignment = (
|
||||
strokeAlign: 'CENTER' | 'INSIDE' | 'OUTSIDE'
|
||||
): StrokeAlignment => {
|
||||
switch (strokeAlign) {
|
||||
case 'CENTER':
|
||||
return 'center';
|
||||
case 'INSIDE':
|
||||
return 'inner';
|
||||
case 'OUTSIDE':
|
||||
return 'outer';
|
||||
}
|
||||
};
|
||||
|
||||
const translateStrokeCap = (vertex: VectorVertex): StrokeCaps | undefined => {
|
||||
switch (vertex.strokeCap as StrokeCap | ConnectorStrokeCap) {
|
||||
case 'NONE':
|
||||
return;
|
||||
case 'ROUND':
|
||||
return 'round';
|
||||
case 'ARROW_EQUILATERAL':
|
||||
case 'TRIANGLE_FILLED':
|
||||
return 'triangle-arrow';
|
||||
case 'SQUARE':
|
||||
return 'square';
|
||||
case 'CIRCLE_FILLED':
|
||||
return 'circle-marker';
|
||||
case 'DIAMOND_FILLED':
|
||||
return 'diamond-marker';
|
||||
case 'ARROW_LINES':
|
||||
default:
|
||||
return 'line-arrow';
|
||||
}
|
||||
};
|
33
plugin-src/translators/translateStyledTextSegments.ts
Normal file
33
plugin-src/translators/translateStyledTextSegments.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { transformTextStyle } from '@plugin/transformers/partials';
|
||||
import { translateFills } from '@plugin/translators/translateFills';
|
||||
|
||||
import { TextNode } from '@ui/lib/types/text/textContent';
|
||||
|
||||
export const translateStyledTextSegments = (
|
||||
segments: Pick<
|
||||
StyledTextSegment,
|
||||
| 'characters'
|
||||
| 'start'
|
||||
| 'end'
|
||||
| 'fontName'
|
||||
| 'fontSize'
|
||||
| 'fontWeight'
|
||||
| 'lineHeight'
|
||||
| 'letterSpacing'
|
||||
| 'textCase'
|
||||
| 'textDecoration'
|
||||
| 'fills'
|
||||
>[],
|
||||
width: number,
|
||||
height: number
|
||||
): TextNode[] => {
|
||||
return segments.map(segment => {
|
||||
figma.ui.postMessage({ type: 'FONT_NAME', data: segment.fontName.family });
|
||||
|
||||
return {
|
||||
fills: translateFills(segment.fills, width, height),
|
||||
text: segment.characters,
|
||||
...transformTextStyle(segment)
|
||||
};
|
||||
});
|
||||
};
|
10
plugin-src/translators/translateTextDecoration.ts
Normal file
10
plugin-src/translators/translateTextDecoration.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export const translateTextDecoration = (segment: Pick<StyledTextSegment, 'textDecoration'>) => {
|
||||
switch (segment.textDecoration) {
|
||||
case 'STRIKETHROUGH':
|
||||
return 'line-through';
|
||||
case 'UNDERLINE':
|
||||
return 'underline';
|
||||
default:
|
||||
return 'none';
|
||||
}
|
||||
};
|
12
plugin-src/translators/translateTextTransform.ts
Normal file
12
plugin-src/translators/translateTextTransform.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
export const translateTextTransform = (segment: Pick<StyledTextSegment, 'textCase'>) => {
|
||||
switch (segment.textCase) {
|
||||
case 'UPPER':
|
||||
return 'uppercase';
|
||||
case 'LOWER':
|
||||
return 'lowercase';
|
||||
case 'TITLE':
|
||||
return 'capitalize';
|
||||
default:
|
||||
return 'none';
|
||||
}
|
||||
};
|
69
plugin-src/translators/translateVectorPaths.ts
Normal file
69
plugin-src/translators/translateVectorPaths.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { CurveToCommand, LineToCommand, MoveToCommand, parseSVG } from 'svg-path-parser';
|
||||
|
||||
import { Segment } from '@ui/lib/types/path/PathContent';
|
||||
|
||||
export const translateVectorPaths = (
|
||||
paths: VectorPaths,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
): Segment[] => {
|
||||
let segments: Segment[] = [];
|
||||
|
||||
for (const path of paths) {
|
||||
segments = [...segments, ...translateVectorPath(path, baseX, baseY)];
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
|
||||
const translateVectorPath = (path: VectorPath, baseX: number, baseY: number): Segment[] => {
|
||||
const normalizedPaths = parseSVG(path.data);
|
||||
|
||||
return normalizedPaths.map(command => {
|
||||
switch (command.command) {
|
||||
case 'moveto':
|
||||
return translateMoveToCommand(command, baseX, baseY);
|
||||
case 'lineto':
|
||||
return translateLineToCommand(command, baseX, baseY);
|
||||
case 'curveto':
|
||||
return translateCurveToCommand(command, baseX, baseY);
|
||||
case 'closepath':
|
||||
default:
|
||||
return {
|
||||
command: 'close-path'
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const translateMoveToCommand = (command: MoveToCommand, baseX: number, baseY: number): Segment => {
|
||||
return {
|
||||
command: 'move-to',
|
||||
params: { x: command.x + baseX, y: command.y + baseY }
|
||||
};
|
||||
};
|
||||
|
||||
const translateLineToCommand = (command: LineToCommand, baseX: number, baseY: number): Segment => {
|
||||
return {
|
||||
command: 'line-to',
|
||||
params: { x: command.x + baseX, y: command.y + baseY }
|
||||
};
|
||||
};
|
||||
|
||||
const translateCurveToCommand = (
|
||||
command: CurveToCommand,
|
||||
baseX: number,
|
||||
baseY: number
|
||||
): Segment => {
|
||||
return {
|
||||
command: 'curve-to',
|
||||
params: {
|
||||
c1x: command.x1 + baseX,
|
||||
c1y: command.y1 + baseY,
|
||||
c2x: command.x2 + baseX,
|
||||
c2y: command.y2 + baseY,
|
||||
x: command.x + baseX,
|
||||
y: command.y + baseY
|
||||
}
|
||||
};
|
||||
};
|
10
plugin-src/tsconfig.json
Normal file
10
plugin-src/tsconfig.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"lib": ["es6"],
|
||||
"strict": true,
|
||||
"typeRoots": ["../node_modules/@figma"],
|
||||
"moduleResolution": "Node"
|
||||
}
|
||||
}
|
6
plugin-src/utils/applyMatrixToPoint.ts
Normal file
6
plugin-src/utils/applyMatrixToPoint.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const applyMatrixToPoint = (matrix: number[][], point: number[]) => {
|
||||
return [
|
||||
point[0] * matrix[0][0] + point[1] * matrix[0][1] + matrix[0][2],
|
||||
point[0] * matrix[1][0] + point[1] * matrix[1][1] + matrix[1][2]
|
||||
];
|
||||
};
|
17
plugin-src/utils/calculateAdjustment.ts
Normal file
17
plugin-src/utils/calculateAdjustment.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
export const calculateAdjustment = (node: SceneNode) => {
|
||||
// For each child, check whether the X or Y position is less than 0 and less than the
|
||||
// current adjustment.
|
||||
let adjustedX = 0;
|
||||
let adjustedY = 0;
|
||||
if ('children' in node) {
|
||||
for (const child of node.children) {
|
||||
if (child.x < adjustedX) {
|
||||
adjustedX = child.x;
|
||||
}
|
||||
if (child.y < adjustedY) {
|
||||
adjustedY = child.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [adjustedX, adjustedY];
|
||||
};
|
23
plugin-src/utils/calculateLinearGradient.ts
Normal file
23
plugin-src/utils/calculateLinearGradient.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { applyMatrixToPoint } from '@plugin/utils/applyMatrixToPoint';
|
||||
import { matrixInvert } from '@plugin/utils/matrixInvert';
|
||||
|
||||
export const calculateLinearGradient = (shapeWidth: number, shapeHeight: number, t: Transform) => {
|
||||
const transform = t.length === 2 ? [...t, [0, 0, 1]] : [...t];
|
||||
const mxInv = matrixInvert(transform);
|
||||
|
||||
if (!mxInv) {
|
||||
return {
|
||||
start: [0, 0],
|
||||
end: [0, 0]
|
||||
};
|
||||
}
|
||||
|
||||
const startEnd = [
|
||||
[0, 0.5],
|
||||
[1, 0.5]
|
||||
].map(p => applyMatrixToPoint(mxInv, p));
|
||||
return {
|
||||
start: [startEnd[0][0] * shapeWidth, startEnd[0][1] * shapeHeight],
|
||||
end: [startEnd[1][0] * shapeWidth, startEnd[1][1] * shapeHeight]
|
||||
};
|
||||
};
|
18
plugin-src/utils/detectMimeType.ts
Normal file
18
plugin-src/utils/detectMimeType.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
export interface Signatures {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const signatures: Signatures = {
|
||||
'R0lGODdh': 'image/gif',
|
||||
'R0lGODlh': 'image/gif',
|
||||
'iVBORw0KGgo': 'image/png',
|
||||
'/9j/': 'image/jpg'
|
||||
};
|
||||
|
||||
export const detectMimeType = (b64: string) => {
|
||||
for (const s in signatures) {
|
||||
if (b64.indexOf(s) === 0) {
|
||||
return signatures[s];
|
||||
}
|
||||
}
|
||||
};
|
3
plugin-src/utils/index.ts
Normal file
3
plugin-src/utils/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './detectMimeType';
|
||||
export * from './rgbToHex';
|
||||
export * from './calculateAdjustment';
|
96
plugin-src/utils/matrixInvert.ts
Normal file
96
plugin-src/utils/matrixInvert.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
export const matrixInvert = (M: number[][]): number[][] | undefined => {
|
||||
// if the matrix isn't square: exit (error)
|
||||
if (M.length !== M[0].length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// create the identity matrix (I), and a copy (C) of the original
|
||||
const dim = M.length;
|
||||
let i = 0,
|
||||
ii = 0,
|
||||
j = 0,
|
||||
e = 0;
|
||||
const I: number[][] = [],
|
||||
C: number[][] = [];
|
||||
for (i = 0; i < dim; i += 1) {
|
||||
// Create the row
|
||||
I[i] = [];
|
||||
C[i] = [];
|
||||
for (j = 0; j < dim; j += 1) {
|
||||
// if we're on the diagonal, put a 1 (for identity)
|
||||
if (i === j) {
|
||||
I[i][j] = 1;
|
||||
} else {
|
||||
I[i][j] = 0;
|
||||
}
|
||||
|
||||
// Also, make the copy of the original
|
||||
C[i][j] = M[i][j];
|
||||
}
|
||||
}
|
||||
|
||||
// Perform elementary row operations
|
||||
for (i = 0; i < dim; i += 1) {
|
||||
// get the element e on the diagonal
|
||||
e = C[i][i];
|
||||
|
||||
// if we have a 0 on the diagonal (we'll need to swap with a lower row)
|
||||
if (e === 0) {
|
||||
// look through every row below the i'th row
|
||||
for (ii = i + 1; ii < dim; ii += 1) {
|
||||
// if the ii'th row has a non-0 in the i'th col
|
||||
if (C[ii][i] !== 0) {
|
||||
// it would make the diagonal have a non-0 so swap it
|
||||
for (j = 0; j < dim; j++) {
|
||||
e = C[i][j]; // temp store i'th row
|
||||
C[i][j] = C[ii][j]; // replace i'th row by ii'th
|
||||
C[ii][j] = e; // replace ii'th by temp
|
||||
e = I[i][j]; // temp store i'th row
|
||||
I[i][j] = I[ii][j]; // replace i'th row by ii'th
|
||||
I[ii][j] = e; // replace ii'th by temp
|
||||
}
|
||||
// don't bother checking other rows since we've swapped
|
||||
break;
|
||||
}
|
||||
}
|
||||
// get the new diagonal
|
||||
e = C[i][i];
|
||||
// if it's still 0, not invertable (error)
|
||||
if (e === 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Scale this row down by e (so we have a 1 on the diagonal)
|
||||
for (j = 0; j < dim; j++) {
|
||||
C[i][j] = C[i][j] / e; // apply to original matrix
|
||||
I[i][j] = I[i][j] / e; // apply to identity
|
||||
}
|
||||
|
||||
// Subtract this row (scaled appropriately for each row) from ALL of
|
||||
// the other rows so that there will be 0's in this column in the
|
||||
// rows above and below this one
|
||||
for (ii = 0; ii < dim; ii++) {
|
||||
// Only apply to other rows (we want a 1 on the diagonal)
|
||||
if (ii === i) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We want to change this element to 0
|
||||
e = C[ii][i];
|
||||
|
||||
// Subtract (the row above(or below) scaled by e) from (the
|
||||
// current row) but start at the i'th column and assume all the
|
||||
// stuff left of diagonal is 0 (which it should be if we made this
|
||||
// algorithm correctly)
|
||||
for (j = 0; j < dim; j++) {
|
||||
C[ii][j] -= e * C[i][j]; // apply to original matrix
|
||||
I[ii][j] -= e * I[i][j]; // apply to identity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we've done all operations, C should be the identity
|
||||
// matrix I should be the inverse:
|
||||
return I;
|
||||
};
|
7
plugin-src/utils/rgbToHex.ts
Normal file
7
plugin-src/utils/rgbToHex.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const rgbToHex = (color: RGB) => {
|
||||
const r = Math.round(255 * color.r);
|
||||
const g = Math.round(255 * color.g);
|
||||
const b = Math.round(255 * color.b);
|
||||
const rgb = (r << 16) | (g << 8) | (b << 0);
|
||||
return '#' + (0x1000000 + rgb).toString(16).slice(1);
|
||||
};
|
19
prettier.config.mjs
Normal file
19
prettier.config.mjs
Normal file
|
@ -0,0 +1,19 @@
|
|||
/** @type {import("prettier").Config} */
|
||||
const config = {
|
||||
arrowParens: 'avoid',
|
||||
bracketSpacing: true,
|
||||
printWidth: 100,
|
||||
proseWrap: 'always',
|
||||
quoteProps: 'consistent',
|
||||
semi: true,
|
||||
singleQuote: true,
|
||||
tabWidth: 2,
|
||||
trailingComma: 'none',
|
||||
useTabs: false,
|
||||
plugins: ['@trivago/prettier-plugin-sort-imports'],
|
||||
importOrder: ['^@plugin/(.*)$', '^@ui/(.*)$', '^[./]'],
|
||||
importOrderSeparation: true,
|
||||
importOrderSortSpecifiers: true
|
||||
};
|
||||
|
||||
export default config;
|
112
src/code.ts
112
src/code.ts
|
@ -1,112 +0,0 @@
|
|||
|
||||
|
||||
let fileData = [];
|
||||
|
||||
type NodeData = {
|
||||
id: string
|
||||
name: string,
|
||||
type: string,
|
||||
children: Node[],
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
fills: any
|
||||
}
|
||||
|
||||
const signatures = {
|
||||
R0lGODdh: "image/gif",
|
||||
R0lGODlh: "image/gif",
|
||||
iVBORw0KGgo: "image/png",
|
||||
"/9j/": "image/jpg"
|
||||
};
|
||||
|
||||
function detectMimeType(b64) {
|
||||
for (var s in signatures) {
|
||||
if (b64.indexOf(s) === 0) {
|
||||
return signatures[s];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function traverse(node): NodeData {
|
||||
let children:any[] = [];
|
||||
|
||||
if ("children" in node) {
|
||||
if (node.type !== "INSTANCE") {
|
||||
for (const child of node.children) {
|
||||
children.push (traverse(child))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
name: node.name,
|
||||
children: children,
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
fills: node.fills === figma.mixed ? [] : node.fills //TODO: Support mixed fills
|
||||
}
|
||||
|
||||
if (node.fills && Array.isArray(node.fills)){
|
||||
|
||||
// Find any fill of type image
|
||||
const imageFill = node.fills.find(fill => fill.type === "IMAGE");
|
||||
if (imageFill) {
|
||||
// An "image" in Figma is a shape with one or more image fills, potentially blended with other fill
|
||||
// types. Given the complexity of mirroring this exactly in Penpot, which treats images as first-class
|
||||
// objects, we're going to simplify this by exporting this shape as a PNG image.
|
||||
node.exportAsync({format: "PNG"}).then((value) => {
|
||||
const b64 = figma.base64Encode(value);
|
||||
figma.ui.postMessage({type: "IMAGE", data: {
|
||||
id: node.id,
|
||||
value: "data:" + detectMimeType(b64) + ";base64," + b64
|
||||
}});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type == "TEXT") {
|
||||
const styledTextSegments = node.getStyledTextSegments(["fontName", "fontSize", "fontWeight", "lineHeight", "letterSpacing", "textCase", "textDecoration", "fills"]);
|
||||
|
||||
if (styledTextSegments[0]) {
|
||||
let font = {
|
||||
fontName: styledTextSegments[0].fontName,
|
||||
fontSize: styledTextSegments[0].fontSize.toString(),
|
||||
fontWeight: styledTextSegments[0].fontWeight.toString(),
|
||||
characters: node.characters,
|
||||
lineHeight: styledTextSegments[0].lineHeight,
|
||||
letterSpacing: styledTextSegments[0].letterSpacing,
|
||||
fills: styledTextSegments[0].fills,
|
||||
textCase: styledTextSegments[0].textCase,
|
||||
textDecoration: styledTextSegments[0].textDecoration,
|
||||
textAlignHorizontal: node.textAlignHorizontal,
|
||||
textAlignVertical: node.textAlignVertical,
|
||||
children: styledTextSegments
|
||||
};
|
||||
result = {...result, ...font};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
figma.showUI(__html__, { themeColors: true, height: 200, width: 300 });
|
||||
|
||||
let root: NodeData = traverse(figma.root) // start the traversal at the root
|
||||
figma.ui.postMessage({type: "FIGMAFILE", data: root});
|
||||
|
||||
figma.ui.onmessage = (msg) => {
|
||||
if (msg.type === "cancel") {
|
||||
figma.closePlugin();
|
||||
}
|
||||
|
||||
if (msg.type === "resize") {
|
||||
figma.ui.resize(msg.width, msg.height);
|
||||
}
|
||||
|
||||
};
|
1434
src/gfonts.json
1434
src/gfonts.json
File diff suppressed because it is too large
Load diff
6736
src/penpot.js
6736
src/penpot.js
File diff suppressed because it is too large
Load diff
|
@ -1 +0,0 @@
|
|||
<div id="penpot-export-page"></div>
|
482
src/ui.tsx
482
src/ui.tsx
|
@ -1,482 +0,0 @@
|
|||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import "./ui.css";
|
||||
import * as penpot from "./penpot.js";
|
||||
|
||||
import { extractLinearGradientParamsFromTransform } from "@figma-plugin/helpers";
|
||||
import slugify from "slugify";
|
||||
|
||||
declare function require(path: string): any;
|
||||
|
||||
// Open resources/gfonts.json and create a set of matched font names
|
||||
const gfonts = new Set();
|
||||
require("./gfonts.json").forEach((font) => gfonts.add(font));
|
||||
|
||||
type PenpotExporterProps = {
|
||||
}
|
||||
|
||||
type FigmaImageData = {
|
||||
value: string,
|
||||
width: number,
|
||||
height: number
|
||||
}
|
||||
|
||||
type PenpotExporterState = {
|
||||
isDebug: boolean,
|
||||
penpotFileData: string
|
||||
missingFonts: Set<string>
|
||||
figmaFileData: string
|
||||
figmaRootNode: NodeData
|
||||
images: { [id: string] : FigmaImageData; };
|
||||
}
|
||||
|
||||
export default class PenpotExporter extends React.Component<PenpotExporterProps, PenpotExporterState> {
|
||||
state: PenpotExporterState = {
|
||||
isDebug: false,
|
||||
penpotFileData: "",
|
||||
figmaFileData: "",
|
||||
missingFonts: new Set(),
|
||||
figmaRootNode: null,
|
||||
images: {}
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
window.addEventListener("message", this.onMessage);
|
||||
}
|
||||
|
||||
componentDidUpdate = () => {
|
||||
this.setDimensions();
|
||||
}
|
||||
|
||||
componentWillUnmount = () =>{
|
||||
window.removeEventListener('message', this.onMessage);
|
||||
}
|
||||
|
||||
rgbToHex = (color) => {
|
||||
const r = Math.round(255 * color.r);
|
||||
const g = Math.round(255 * color.g);
|
||||
const b = Math.round(255 * color.b);
|
||||
const rgb = (r << 16) | (g << 8) | (b << 0);
|
||||
return '#' + (0x1000000 + rgb).toString(16).slice(1);
|
||||
}
|
||||
|
||||
translateSolidFill(fill){
|
||||
return {
|
||||
fillColor: this.rgbToHex(fill.color),
|
||||
fillOpacity: fill.opacity
|
||||
}
|
||||
}
|
||||
|
||||
translateGradientLinearFill(fill, width, height){
|
||||
const points = extractLinearGradientParamsFromTransform(width, height, fill.gradientTransform);
|
||||
return {
|
||||
fillColorGradient: {
|
||||
type: Symbol.for("linear"),
|
||||
width: 1,
|
||||
startX: points.start[0] / width,
|
||||
startY: points.start[1] / height,
|
||||
endX: points.end[0] / width,
|
||||
endY: points.end[1] / height,
|
||||
stops: [{
|
||||
color: this.rgbToHex(fill.gradientStops[0].color),
|
||||
offset: fill.gradientStops[0].position,
|
||||
opacity: fill.gradientStops[0].color.a * fill.opacity
|
||||
},
|
||||
{
|
||||
color: this.rgbToHex(fill.gradientStops[1].color),
|
||||
offset: fill.gradientStops[1].position,
|
||||
opacity: fill.gradientStops[1].color.a * fill.opacity
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
translateFill(fill, width, height){
|
||||
|
||||
if (fill.type === "SOLID"){
|
||||
return this.translateSolidFill(fill);
|
||||
} else if (fill.type === "GRADIENT_LINEAR"){
|
||||
return this.translateGradientLinearFill(fill, width, height);
|
||||
} else {
|
||||
console.error('Color type '+fill.type+' not supported yet');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
translateFills(fills, width, height){
|
||||
let penpotFills = [];
|
||||
let penpotFill = null;
|
||||
for (var fill of fills){
|
||||
penpotFill = this.translateFill(fill, width, height);
|
||||
|
||||
// Penpot does not track fill visibility, so if the Figma fill is invisible we
|
||||
// force opacity to 0
|
||||
if (fill.visible === false){
|
||||
penpotFill.fillOpacity = 0;
|
||||
}
|
||||
|
||||
if (penpotFill !== null){
|
||||
penpotFills.unshift(penpotFill);
|
||||
}
|
||||
}
|
||||
return penpotFills;
|
||||
}
|
||||
|
||||
addFontWarning(font){
|
||||
const newMissingFonts = this.state.missingFonts;
|
||||
newMissingFonts.add(font);
|
||||
|
||||
this.setState(_ => ({missingFonts: newMissingFonts }));
|
||||
}
|
||||
|
||||
createPenpotPage(file, node){
|
||||
file.addPage(node.name);
|
||||
for (var child of node.children){
|
||||
this.createPenpotItem(file, child, 0, 0);
|
||||
}
|
||||
file.closePage();
|
||||
}
|
||||
|
||||
createPenpotBoard(file, node, baseX, baseY){
|
||||
file.addArtboard({ name: node.name, x: node.x + baseX, y: node.y + baseY, width: node.width, height: node.height,
|
||||
fills: this.translateFills(node.fills, node.width, node.height)
|
||||
});
|
||||
for (var child of node.children){
|
||||
this.createPenpotItem(file, child, node.x + baseX, node.y + baseY);
|
||||
}
|
||||
file.closeArtboard();
|
||||
}
|
||||
|
||||
createPenpotGroup(file, node, baseX, baseY){
|
||||
file.addGroup({name: node.name});
|
||||
for (var child of node.children){
|
||||
this.createPenpotItem(file, child, baseX, baseY);
|
||||
}
|
||||
file.closeGroup();
|
||||
}
|
||||
|
||||
createPenpotRectangle(file, node, baseX, baseY){
|
||||
file.createRect({ name: node.name, x: node.x + baseX, y: node.y + baseY, width: node.width, height: node.height,
|
||||
fills: this.translateFills(node.fills, node.width, node.height),
|
||||
});
|
||||
}
|
||||
|
||||
createPenpotCircle(file, node, baseX, baseY){
|
||||
file.createCircle({ name: node.name, x: node.x + baseX, y: node.y + baseY, width: node.width, height: node.height,
|
||||
fills: this.translateFills(node.fills, node.width, node.height),
|
||||
});
|
||||
}
|
||||
|
||||
translateHorizontalAlign(align: string){
|
||||
if (align === "RIGHT"){
|
||||
return Symbol.for("right");
|
||||
}
|
||||
if (align === "CENTER"){
|
||||
return Symbol.for("center");
|
||||
}
|
||||
return Symbol.for("left")
|
||||
}
|
||||
|
||||
translateVerticalAlign(align: string){
|
||||
if (align === "BOTTOM"){
|
||||
return Symbol.for("bottom");
|
||||
}
|
||||
if (align === "CENTER"){
|
||||
return Symbol.for("center");
|
||||
}
|
||||
return Symbol.for("top")
|
||||
}
|
||||
|
||||
translateFontStyle(style:string){
|
||||
return style.toLowerCase().replace(/\s/g, "")
|
||||
}
|
||||
|
||||
getTextDecoration(node){
|
||||
const textDecoration = node.textDecoration;
|
||||
if (textDecoration === "STRIKETHROUGH"){
|
||||
return "line-through";
|
||||
}
|
||||
if (textDecoration === "UNDERLINE"){
|
||||
return "underline";
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
getTextTransform(node){
|
||||
const textCase = node.textCase;
|
||||
if (textCase === "UPPER"){
|
||||
return "uppercase";
|
||||
}
|
||||
if (textCase === "LOWER"){
|
||||
return "lowercase";
|
||||
}
|
||||
if (textCase === "TITLE"){
|
||||
return "capitalize";
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
validateFont(fontName) {
|
||||
const name = slugify(fontName.family.toLowerCase());
|
||||
if (!gfonts.has(name)) {
|
||||
this.addFontWarning(name);
|
||||
}
|
||||
}
|
||||
|
||||
createPenpotText(file, node, baseX, baseY){
|
||||
|
||||
const children = node.children.map((val) => {
|
||||
|
||||
this.validateFont(val.fontName);
|
||||
|
||||
return {
|
||||
lineHeight: val.lineHeight,
|
||||
fontStyle: "normal",
|
||||
textAlign: this.translateHorizontalAlign(node.textAlignHorizontal),
|
||||
fontId: "gfont-" + slugify(val.fontName.family.toLowerCase()),
|
||||
fontSize: val.fontSize.toString(),
|
||||
fontWeight: val.fontWeight.toString(),
|
||||
fontVariantId: this.translateFontStyle(val.fontName.style),
|
||||
textDecoration: this.getTextDecoration(val),
|
||||
textTransform: this.getTextTransform(val),
|
||||
letterSpacing: val.letterSpacing,
|
||||
fills: this.translateFills(val.fills, node.width, node.height),
|
||||
fontFamily: val.fontName.family,
|
||||
text: val.characters }
|
||||
});
|
||||
|
||||
this.validateFont(node.fontName);
|
||||
|
||||
file.createText({
|
||||
name: node.name,
|
||||
x: node.x + baseX,
|
||||
y: node.y + baseY,
|
||||
width: node.width,
|
||||
height: node.height,
|
||||
rotation: 0,
|
||||
type: Symbol.for("text"),
|
||||
content: {
|
||||
type: "root",
|
||||
verticalAlign: this.translateVerticalAlign(node.textAlignVertical),
|
||||
children: [{
|
||||
type: "paragraph-set",
|
||||
children: [{
|
||||
lineHeight: node.lineHeight,
|
||||
fontStyle: "normal",
|
||||
children: children,
|
||||
textTransform: this.getTextTransform(node),
|
||||
textAlign: this.translateHorizontalAlign(node.textAlignHorizontal),
|
||||
fontId: "gfont-" + slugify(node.fontName.family.toLowerCase()),
|
||||
fontSize: node.fontSize.toString(),
|
||||
fontWeight: node.fontWeight.toString(),
|
||||
type: "paragraph",
|
||||
textDecoration: this.getTextDecoration(node),
|
||||
letterSpacing: node.letterSpacing,
|
||||
fills: this.translateFills(node.fills, node.width, node.height),
|
||||
fontFamily: node.fontName.family
|
||||
}]
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createPenpotImage(file, node, baseX, baseY, image){
|
||||
file.createImage({ name: node.name, x: node.x + baseX, y: node.y + baseY, width: image.width, height: image.height,
|
||||
metadata: {
|
||||
width: image.width,
|
||||
height: image.height
|
||||
},
|
||||
dataUri: image.value
|
||||
});
|
||||
}
|
||||
|
||||
calculateAdjustment(node){
|
||||
// For each child, check whether the X or Y position is less than 0 and less than the
|
||||
// current adjustment.
|
||||
let adjustedX = 0;
|
||||
let adjustedY = 0;
|
||||
for (var child of node.children){
|
||||
if (child.x < adjustedX){
|
||||
adjustedX = child.x;
|
||||
}
|
||||
if (child.y < adjustedY){
|
||||
adjustedY = child.y;
|
||||
}
|
||||
}
|
||||
return [adjustedX, adjustedY];
|
||||
}
|
||||
|
||||
createPenpotItem(file, node, baseX, baseY){
|
||||
|
||||
// We special-case images because an image in figma is a shape with one or many
|
||||
// image fills. Given that handling images in Penpot is a bit different, we
|
||||
// rasterize a figma shape with any image fills to a PNG and then add it as a single
|
||||
// Penpot image. Implication is that any node that has an image fill will only be
|
||||
// treated as an image, so we skip node type checks.
|
||||
const hasImageFill = node.fills?.some(fill => fill.type === "IMAGE");
|
||||
if (hasImageFill){
|
||||
|
||||
// If the nested frames extended the bounds of the rasterized image, we need to
|
||||
// account for this both in position on the canvas and the calculated width and
|
||||
// height of the image.
|
||||
const [adjustedX, adjustedY] = this.calculateAdjustment(node);
|
||||
const width = node.width + Math.abs(adjustedX);
|
||||
const height = node.height + Math.abs(adjustedY);
|
||||
|
||||
this.createPenpotImage(file, node, baseX + adjustedX, baseY + adjustedY, this.state.images[node.id]);
|
||||
}
|
||||
else if (node.type == "PAGE"){
|
||||
this.createPenpotPage(file, node);
|
||||
}
|
||||
else if (node.type == "FRAME"){
|
||||
this.createPenpotBoard(file, node, baseX, baseY);
|
||||
}
|
||||
else if (node.type == "GROUP"){
|
||||
this.createPenpotGroup(file, node,baseX, baseY);
|
||||
}
|
||||
else if (node.type == "RECTANGLE"){
|
||||
this.createPenpotRectangle(file, node, baseX, baseY);
|
||||
}
|
||||
else if (node.type == "ELLIPSE"){
|
||||
this.createPenpotCircle(file, node, baseX, baseY);
|
||||
}
|
||||
else if (node.type == "TEXT"){
|
||||
this.createPenpotText(file, node, baseX, baseY);
|
||||
}
|
||||
}
|
||||
|
||||
createPenpotFile(){
|
||||
let node = this.state.figmaRootNode;
|
||||
const file = penpot.createFile(node.name);
|
||||
for (var page of node.children){
|
||||
this.createPenpotItem(file, page, 0, 0);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
onCreatePenpot = () => {
|
||||
const file = this.createPenpotFile();
|
||||
const penpotFileMap = file.asMap();
|
||||
this.setState(state => ({penpotFileData: JSON.stringify(penpotFileMap, (key, value) => (value instanceof Map ? [...value] : value), 4)}));
|
||||
file.export()
|
||||
};
|
||||
|
||||
onCancel = () => {
|
||||
parent.postMessage({ pluginMessage: { type: "cancel" } }, "*");
|
||||
};
|
||||
|
||||
onMessage = (event) => {
|
||||
if (event.data.pluginMessage.type == "FIGMAFILE") {
|
||||
this.setState(state => ({
|
||||
figmaFileData: JSON.stringify(event.data.pluginMessage.data, (key, value) => (value instanceof Map ? [...value] : value), 4),
|
||||
figmaRootNode: event.data.pluginMessage.data}));
|
||||
}
|
||||
else if (event.data.pluginMessage.type == "IMAGE") {
|
||||
|
||||
const data = event.data.pluginMessage.data;
|
||||
const image = document.createElement('img');
|
||||
const thisObj = this;
|
||||
|
||||
image.addEventListener('load', function() {
|
||||
// Get byte array from response
|
||||
thisObj.setState(state =>
|
||||
{
|
||||
state.images[data.id] = {
|
||||
value: data.value,
|
||||
width: image.naturalWidth,
|
||||
height: image.naturalHeight
|
||||
};
|
||||
return state;
|
||||
}
|
||||
);
|
||||
});
|
||||
image.src = data.value;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
setDimensions = () => {
|
||||
|
||||
const isMissingFonts = this.state.missingFonts.size > 0;
|
||||
|
||||
let width = 300;
|
||||
let height = 280;
|
||||
|
||||
if (isMissingFonts) {
|
||||
height += (this.state.missingFonts.size * 20);
|
||||
width = 400;
|
||||
}
|
||||
|
||||
if (this.state.isDebug){
|
||||
height += 600;
|
||||
width = 800;
|
||||
}
|
||||
|
||||
parent.postMessage({ pluginMessage: { type: "resize", width: width, height: height } }, "*");
|
||||
}
|
||||
|
||||
toggleDebug = (event) => {
|
||||
const isDebug = event.currentTarget.checked;
|
||||
this.setState (state => ({isDebug: isDebug}));
|
||||
}
|
||||
|
||||
renderFontWarnings = () => {
|
||||
return (
|
||||
<ul >
|
||||
{Array.from(this.state.missingFonts).map((font) => (
|
||||
<li key={font}>{font}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
// Update the dimensions of the plugin window based on available data and selections
|
||||
return (
|
||||
<main>
|
||||
<header>
|
||||
<img src={require("./logo.svg")} />
|
||||
<h2>Penpot Exporter</h2>
|
||||
</header>
|
||||
<section>
|
||||
<div style={{display:this.state.missingFonts.size > 0 ? "inline" : "none"}}>
|
||||
<div id="missing-fonts">{this.state.missingFonts.size} non-default font{this.state.missingFonts.size > 1 ? 's' : ''}: </div>
|
||||
<small>Ensure fonts are installed in Penpot before importing.</small>
|
||||
<div id="missing-fonts-list">
|
||||
{this.renderFontWarnings()}
|
||||
</div>
|
||||
</div>
|
||||
<div >
|
||||
<input type="checkbox" id="chkDebug" name="chkDebug" onChange={this.toggleDebug}/>
|
||||
<label htmlFor="chkDebug">Show debug data</label>
|
||||
</div>
|
||||
</section>
|
||||
<div style={{display: this.state.isDebug? '': 'none'}}>
|
||||
<section>
|
||||
<textarea style={{width:'790px', height:'270px'}} id="figma-file-data" value={this.state.figmaFileData} readOnly />
|
||||
<label htmlFor="figma-file-data">Figma file data</label>
|
||||
</section>
|
||||
<section>
|
||||
<textarea style={{width:'790px', height:'270px'}} id="penpot-file-data" value={this.state.penpotFileData} readOnly />
|
||||
<label htmlFor="penpot-file-data">Penpot file data</label>
|
||||
</section>
|
||||
</div>
|
||||
<footer>
|
||||
<button className="brand" onClick={this.onCreatePenpot}>
|
||||
Export
|
||||
</button>
|
||||
<button onClick={this.onCancel}>Cancel</button>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<PenpotExporter />
|
||||
</React.StrictMode>,
|
||||
document.getElementById('penpot-export-page')
|
||||
);
|
4
stylelint.config.mjs
Normal file
4
stylelint.config.mjs
Normal file
|
@ -0,0 +1,4 @@
|
|||
/** @type {import('stylelint').Config} */
|
||||
export default {
|
||||
extends: ['stylelint-config-recommended', 'stylelint-config-standard']
|
||||
};
|
9
tsconfig.base.json
Normal file
9
tsconfig.base.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@plugin/*": ["./plugin-src/*"],
|
||||
"@ui/*": ["./ui-src/*"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"jsx": "react",
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@figma"
|
||||
],
|
||||
"moduleResolution":"node"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||
}
|
||||
|
100
ui-src/PenpotExporter.tsx
Normal file
100
ui-src/PenpotExporter.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import slugify from 'slugify';
|
||||
|
||||
import { createPenpotFile } from '@ui/converters';
|
||||
import { PenpotDocument } from '@ui/lib/types/penpotDocument';
|
||||
import { validateFont } from '@ui/validators';
|
||||
|
||||
import Logo from './logo.svg?react';
|
||||
|
||||
export const PenpotExporter = () => {
|
||||
const [missingFonts, setMissingFonts] = useState(new Set<string>());
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
const addFontWarning = (font: string) => {
|
||||
setMissingFonts(missingFonts => missingFonts.add(font));
|
||||
};
|
||||
|
||||
const onMessage = (event: MessageEvent<{ pluginMessage: { type: string; data: unknown } }>) => {
|
||||
if (event.data.pluginMessage?.type == 'FIGMAFILE') {
|
||||
const document = event.data.pluginMessage.data as PenpotDocument;
|
||||
const file = createPenpotFile(document);
|
||||
|
||||
file.export();
|
||||
|
||||
setExporting(false);
|
||||
} else if (event.data.pluginMessage?.type == 'FONT_NAME') {
|
||||
const fontName = event.data.pluginMessage.data as string;
|
||||
|
||||
if (!validateFont(fontName)) {
|
||||
addFontWarning(slugify(fontName.toLowerCase()));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onCreatePenpot = () => {
|
||||
setExporting(true);
|
||||
|
||||
parent.postMessage({ pluginMessage: { type: 'export' } }, '*');
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*');
|
||||
};
|
||||
|
||||
const setDimensions = () => {
|
||||
const isMissingFonts = missingFonts.size > 0;
|
||||
|
||||
let width = 300;
|
||||
let height = 280;
|
||||
|
||||
if (isMissingFonts) {
|
||||
height += missingFonts.size * 20;
|
||||
width = 400;
|
||||
parent.postMessage({ pluginMessage: { type: 'resize', width: width, height: height } }, '*');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', onMessage);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('message', onMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setDimensions();
|
||||
}, [missingFonts]);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<header>
|
||||
<Logo />
|
||||
<h2>Penpot Exporter</h2>
|
||||
</header>
|
||||
<section>
|
||||
<div style={{ display: missingFonts.size > 0 ? 'inline' : 'none' }}>
|
||||
<div id="missing-fonts">
|
||||
{missingFonts.size} non-default font
|
||||
{missingFonts.size > 1 ? 's' : ''}:{' '}
|
||||
</div>
|
||||
<small>Ensure fonts are installed in Penpot before importing.</small>
|
||||
<div id="missing-fonts-list">
|
||||
<ul>
|
||||
{Array.from(missingFonts).map(font => (
|
||||
<li key={font}>{font}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer>
|
||||
<button className="brand" disabled={exporting} onClick={onCreatePenpot}>
|
||||
{exporting ? 'Exporting...' : 'Export to Penpot'}
|
||||
</button>
|
||||
<button onClick={onCancel}>Cancel</button>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
};
|
18
ui-src/converters/createGradientFill.ts
Normal file
18
ui-src/converters/createGradientFill.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Gradient, LINEAR_TYPE, RADIAL_TYPE } from '@ui/lib/types/utils/gradient';
|
||||
|
||||
export const createGradientFill = ({ type, ...rest }: Gradient): Gradient | undefined => {
|
||||
switch (type) {
|
||||
case 'linear':
|
||||
return {
|
||||
type: LINEAR_TYPE,
|
||||
...rest
|
||||
};
|
||||
case 'radial':
|
||||
return {
|
||||
type: RADIAL_TYPE,
|
||||
...rest
|
||||
};
|
||||
}
|
||||
|
||||
console.error(`Unsupported gradient type: ${String(type)}`);
|
||||
};
|
21
ui-src/converters/createPenpotArtboard.ts
Normal file
21
ui-src/converters/createPenpotArtboard.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { PenpotFile } from '@ui/lib/penpot';
|
||||
import { FRAME_TYPE } from '@ui/lib/types/frame/frameAttributes';
|
||||
import { FrameShape } from '@ui/lib/types/frame/frameShape';
|
||||
|
||||
import { createPenpotItem } from '.';
|
||||
|
||||
export const createPenpotArtboard = (
|
||||
file: PenpotFile,
|
||||
{ type, children = [], ...rest }: FrameShape
|
||||
) => {
|
||||
file.addArtboard({
|
||||
type: FRAME_TYPE,
|
||||
...rest
|
||||
});
|
||||
|
||||
for (const child of children) {
|
||||
createPenpotItem(file, child);
|
||||
}
|
||||
|
||||
file.closeArtboard();
|
||||
};
|
17
ui-src/converters/createPenpotCircle.ts
Normal file
17
ui-src/converters/createPenpotCircle.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { PenpotFile } from '@ui/lib/penpot';
|
||||
import { CIRCLE_TYPE } from '@ui/lib/types/circle/circleAttributes';
|
||||
import { CircleShape } from '@ui/lib/types/circle/circleShape';
|
||||
|
||||
import { translateFillGradients, translateUiBlendMode } from '../translators';
|
||||
|
||||
export const createPenpotCircle = (
|
||||
file: PenpotFile,
|
||||
{ type, fills, blendMode, ...rest }: CircleShape
|
||||
) => {
|
||||
file.createCircle({
|
||||
type: CIRCLE_TYPE,
|
||||
fills: translateFillGradients(fills),
|
||||
blendMode: translateUiBlendMode(blendMode),
|
||||
...rest
|
||||
});
|
||||
};
|
14
ui-src/converters/createPenpotFile.ts
Normal file
14
ui-src/converters/createPenpotFile.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { createFile } from '@ui/lib/penpot.js';
|
||||
import { PenpotDocument } from '@ui/lib/types/penpotDocument';
|
||||
|
||||
import { createPenpotPage } from '.';
|
||||
|
||||
export const createPenpotFile = (node: PenpotDocument) => {
|
||||
const file = createFile(node.name);
|
||||
|
||||
for (const page of node.children ?? []) {
|
||||
createPenpotPage(file, page);
|
||||
}
|
||||
|
||||
return file;
|
||||
};
|
23
ui-src/converters/createPenpotGroup.ts
Normal file
23
ui-src/converters/createPenpotGroup.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { PenpotFile } from '@ui/lib/penpot';
|
||||
import { GROUP_TYPE } from '@ui/lib/types/group/groupAttributes';
|
||||
import { GroupShape } from '@ui/lib/types/group/groupShape';
|
||||
import { translateUiBlendMode } from '@ui/translators';
|
||||
|
||||
import { createPenpotItem } from '.';
|
||||
|
||||
export const createPenpotGroup = (
|
||||
file: PenpotFile,
|
||||
{ type, blendMode, children = [], ...rest }: GroupShape
|
||||
) => {
|
||||
file.addGroup({
|
||||
type: GROUP_TYPE,
|
||||
blendMode: translateUiBlendMode(blendMode),
|
||||
...rest
|
||||
});
|
||||
|
||||
for (const child of children) {
|
||||
createPenpotItem(file, child);
|
||||
}
|
||||
|
||||
file.closeGroup();
|
||||
};
|
10
ui-src/converters/createPenpotImage.ts
Normal file
10
ui-src/converters/createPenpotImage.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { PenpotFile } from '@ui/lib/penpot';
|
||||
import { IMAGE_TYPE } from '@ui/lib/types/image/imageAttributes';
|
||||
import { ImageShape } from '@ui/lib/types/image/imageShape';
|
||||
|
||||
export const createPenpotImage = (file: PenpotFile, { type, ...rest }: ImageShape) => {
|
||||
file.createImage({
|
||||
type: IMAGE_TYPE,
|
||||
...rest
|
||||
});
|
||||
};
|
31
ui-src/converters/createPenpotItem.ts
Normal file
31
ui-src/converters/createPenpotItem.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { PenpotFile } from '@ui/lib/penpot';
|
||||
import { PenpotNode } from '@ui/lib/types/penpotNode';
|
||||
|
||||
import {
|
||||
createPenpotArtboard,
|
||||
createPenpotCircle,
|
||||
createPenpotGroup,
|
||||
createPenpotImage,
|
||||
createPenpotPath,
|
||||
createPenpotRectangle,
|
||||
createPenpotText
|
||||
} from '.';
|
||||
|
||||
export const createPenpotItem = (file: PenpotFile, node: PenpotNode) => {
|
||||
switch (node.type) {
|
||||
case 'rect':
|
||||
return createPenpotRectangle(file, node);
|
||||
case 'circle':
|
||||
return createPenpotCircle(file, node);
|
||||
case 'frame':
|
||||
return createPenpotArtboard(file, node);
|
||||
case 'group':
|
||||
return createPenpotGroup(file, node);
|
||||
case 'image':
|
||||
return createPenpotImage(file, node);
|
||||
case 'path':
|
||||
return createPenpotPath(file, node);
|
||||
case 'text':
|
||||
return createPenpotText(file, node);
|
||||
}
|
||||
};
|
14
ui-src/converters/createPenpotPage.ts
Normal file
14
ui-src/converters/createPenpotPage.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { PenpotFile } from '@ui/lib/penpot';
|
||||
import { PenpotPage } from '@ui/lib/types/penpotPage';
|
||||
|
||||
import { createPenpotItem } from '.';
|
||||
|
||||
export const createPenpotPage = (file: PenpotFile, node: PenpotPage) => {
|
||||
file.addPage(node.name);
|
||||
|
||||
for (const child of node.children ?? []) {
|
||||
createPenpotItem(file, child);
|
||||
}
|
||||
|
||||
file.closePage();
|
||||
};
|
21
ui-src/converters/createPenpotPath.ts
Normal file
21
ui-src/converters/createPenpotPath.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { PenpotFile } from '@ui/lib/penpot';
|
||||
import { PATH_TYPE } from '@ui/lib/types/path/pathAttributes';
|
||||
import { PathShape } from '@ui/lib/types/path/pathShape';
|
||||
import {
|
||||
translateFillGradients,
|
||||
translatePathContent,
|
||||
translateUiBlendMode
|
||||
} from '@ui/translators';
|
||||
|
||||
export const createPenpotPath = (
|
||||
file: PenpotFile,
|
||||
{ type, fills, blendMode, content, ...rest }: PathShape
|
||||
) => {
|
||||
file.createPath({
|
||||
type: PATH_TYPE,
|
||||
fills: translateFillGradients(fills),
|
||||
blendMode: translateUiBlendMode(blendMode),
|
||||
content: translatePathContent(content),
|
||||
...rest
|
||||
});
|
||||
};
|
16
ui-src/converters/createPenpotRectangle.ts
Normal file
16
ui-src/converters/createPenpotRectangle.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { PenpotFile } from '@ui/lib/penpot';
|
||||
import { RECT_TYPE } from '@ui/lib/types/rect/rectAttributes';
|
||||
import { RectShape } from '@ui/lib/types/rect/rectShape';
|
||||
import { translateFillGradients, translateUiBlendMode } from '@ui/translators';
|
||||
|
||||
export const createPenpotRectangle = (
|
||||
file: PenpotFile,
|
||||
{ type, fills, blendMode, ...rest }: RectShape
|
||||
) => {
|
||||
file.createRect({
|
||||
type: RECT_TYPE,
|
||||
fills: translateFillGradients(fills),
|
||||
blendMode: translateUiBlendMode(blendMode),
|
||||
...rest
|
||||
});
|
||||
};
|
12
ui-src/converters/createPenpotText.ts
Normal file
12
ui-src/converters/createPenpotText.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { PenpotFile } from '@ui/lib/penpot';
|
||||
import { TEXT_TYPE } from '@ui/lib/types/text/textAttributes';
|
||||
import { TextShape } from '@ui/lib/types/text/textShape';
|
||||
import { translateUiBlendMode } from '@ui/translators';
|
||||
|
||||
export const createPenpotText = (file: PenpotFile, { type, blendMode, ...rest }: TextShape) => {
|
||||
file.createText({
|
||||
type: TEXT_TYPE,
|
||||
blendMode: translateUiBlendMode(blendMode),
|
||||
...rest
|
||||
});
|
||||
};
|
10
ui-src/converters/index.ts
Normal file
10
ui-src/converters/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export * from './createPenpotArtboard';
|
||||
export * from './createPenpotCircle';
|
||||
export * from './createPenpotFile';
|
||||
export * from './createPenpotGroup';
|
||||
export * from './createPenpotImage';
|
||||
export * from './createPenpotItem';
|
||||
export * from './createPenpotPage';
|
||||
export * from './createPenpotPath';
|
||||
export * from './createPenpotRectangle';
|
||||
export * from './createPenpotText';
|
1434
ui-src/gfonts.json
Normal file
1434
ui-src/gfonts.json
Normal file
File diff suppressed because it is too large
Load diff
12
ui-src/index.html
Normal file
12
ui-src/index.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Penpot Exporter</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
44
ui-src/lib/penpot.d.ts
vendored
Normal file
44
ui-src/lib/penpot.d.ts
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { BoolShape } from '@ui/lib/types/bool/boolShape';
|
||||
import { CircleShape } from '@ui/lib/types/circle/circleShape';
|
||||
import { FrameShape } from '@ui/lib/types/frame/frameShape';
|
||||
import { GroupShape } from '@ui/lib/types/group/groupShape';
|
||||
import { ImageShape } from '@ui/lib/types/image/imageShape';
|
||||
import { PathShape } from '@ui/lib/types/path/pathShape';
|
||||
import { RectShape } from '@ui/lib/types/rect/rectShape';
|
||||
import { TextShape } from '@ui/lib/types/text/textShape';
|
||||
|
||||
export interface PenpotFile {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
addPage(name: string, options?: any): void;
|
||||
closePage(): void;
|
||||
addArtboard(artboard: FrameShape): void;
|
||||
closeArtboard(): void;
|
||||
addGroup(group: GroupShape): void;
|
||||
closeGroup(): void;
|
||||
addBool(bool: BoolShape): void;
|
||||
closeBool(): void;
|
||||
createRect(rect: RectShape): void;
|
||||
createCircle(circle: CircleShape): void;
|
||||
createPath(path: PathShape): void;
|
||||
createText(options: TextShape): void;
|
||||
createImage(image: ImageShape): void;
|
||||
// createSVG(svg: any): void;
|
||||
// closeSVG(): void;
|
||||
// addLibraryColor(color: any): void;
|
||||
// updateLibraryColor(color: any): void;
|
||||
// deleteLibraryColor(color: any): void;
|
||||
// addLibraryMedia(media: any): void;
|
||||
// deleteLibraryMedia(media: any): void;
|
||||
// addLibraryTypography(typography: any): void;
|
||||
// deleteLibraryTypography(typography: any): void;
|
||||
// startComponent(component: any): void;
|
||||
// finishComponent(): void;
|
||||
// createComponentInstance(instance: any): void;
|
||||
// lookupShape(shapeId: string): void;
|
||||
// updateObject(id: string, object: any): void;
|
||||
// deleteObject(id: string): void;
|
||||
asMap(): unknown;
|
||||
export(): void;
|
||||
}
|
||||
|
||||
export function createFile(name: string): PenpotFile;
|
6868
ui-src/lib/penpot.js
Normal file
6868
ui-src/lib/penpot.js
Normal file
File diff suppressed because one or more lines are too long
12
ui-src/lib/types/bool/boolAttributes.ts
Normal file
12
ui-src/lib/types/bool/boolAttributes.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Uuid } from '@ui/lib/types/utils/uuid';
|
||||
|
||||
import { BoolContent } from './boolContent';
|
||||
|
||||
export const BOOL_TYPE: unique symbol = Symbol.for('bool');
|
||||
|
||||
export type BoolAttributes = {
|
||||
type: 'bool' | typeof BOOL_TYPE;
|
||||
shapes?: Uuid[];
|
||||
boolType: string; // @TODO: in Penpot this is of type :keyword. check if it makes sense
|
||||
boolContent: BoolContent[];
|
||||
};
|
8
ui-src/lib/types/bool/boolContent.d.ts
vendored
Normal file
8
ui-src/lib/types/bool/boolContent.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { Point } from '@ui/lib/types/utils/point';
|
||||
|
||||
export type BoolContent = {
|
||||
command: string; // @TODO: in Penpot this is of type :keyword. check if it makes sense
|
||||
relative?: boolean;
|
||||
prevPos?: Point;
|
||||
params?: { [keyword: string]: number }; // @TODO: in Penpot this is of type :keyword. check if it makes sense
|
||||
};
|
10
ui-src/lib/types/bool/boolShape.d.ts
vendored
Normal file
10
ui-src/lib/types/bool/boolShape.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { LayoutChildAttributes } from '@ui/lib/types/layout/layoutChildAttributes';
|
||||
import { ShapeAttributes } from '@ui/lib/types/shape/shapeAttributes';
|
||||
import { ShapeBaseAttributes } from '@ui/lib/types/shape/shapeBaseAttributes';
|
||||
|
||||
import { BoolAttributes } from './boolAttributes';
|
||||
|
||||
export type BoolShape = ShapeBaseAttributes &
|
||||
ShapeAttributes &
|
||||
BoolAttributes &
|
||||
LayoutChildAttributes;
|
5
ui-src/lib/types/circle/circleAttributes.ts
Normal file
5
ui-src/lib/types/circle/circleAttributes.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const CIRCLE_TYPE: unique symbol = Symbol.for('circle');
|
||||
|
||||
export type CircleAttributes = {
|
||||
type: 'circle' | typeof CIRCLE_TYPE;
|
||||
};
|
12
ui-src/lib/types/circle/circleShape.d.ts
vendored
Normal file
12
ui-src/lib/types/circle/circleShape.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { LayoutChildAttributes } from '@ui/lib/types/layout/layoutChildAttributes';
|
||||
import { ShapeAttributes } from '@ui/lib/types/shape/shapeAttributes';
|
||||
import { ShapeBaseAttributes } from '@ui/lib/types/shape/shapeBaseAttributes';
|
||||
import { ShapeGeomAttributes } from '@ui/lib/types/shape/shapeGeomAttributes';
|
||||
|
||||
import { CircleAttributes } from './circleAttributes';
|
||||
|
||||
export type CircleShape = ShapeBaseAttributes &
|
||||
ShapeGeomAttributes &
|
||||
ShapeAttributes &
|
||||
CircleAttributes &
|
||||
LayoutChildAttributes;
|
16
ui-src/lib/types/frame/frameAttributes.ts
Normal file
16
ui-src/lib/types/frame/frameAttributes.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Stroke } from '@ui/lib/types/utils/stroke';
|
||||
import { Uuid } from '@ui/lib/types/utils/uuid';
|
||||
|
||||
import { Fill } from '../utils/fill';
|
||||
|
||||
export const FRAME_TYPE: unique symbol = Symbol.for('frame');
|
||||
|
||||
export type FrameAttributes = {
|
||||
type: 'frame' | typeof FRAME_TYPE;
|
||||
shapes?: Uuid[];
|
||||
hideFillOnExport?: boolean;
|
||||
showContent?: boolean;
|
||||
hideInViewer?: boolean;
|
||||
fills?: Fill[];
|
||||
strokes?: Stroke[];
|
||||
};
|
14
ui-src/lib/types/frame/frameShape.d.ts
vendored
Normal file
14
ui-src/lib/types/frame/frameShape.d.ts
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { LayoutAttributes } from '@ui/lib/types/layout/layoutAttributes';
|
||||
import { LayoutChildAttributes } from '@ui/lib/types/layout/layoutChildAttributes';
|
||||
import { ShapeBaseAttributes } from '@ui/lib/types/shape/shapeBaseAttributes';
|
||||
import { ShapeGeomAttributes } from '@ui/lib/types/shape/shapeGeomAttributes';
|
||||
import { Children } from '@ui/lib/types/utils/children';
|
||||
|
||||
import { FrameAttributes } from './frameAttributes';
|
||||
|
||||
export type FrameShape = ShapeBaseAttributes &
|
||||
ShapeGeomAttributes &
|
||||
FrameAttributes &
|
||||
LayoutAttributes &
|
||||
LayoutChildAttributes &
|
||||
Children;
|
8
ui-src/lib/types/group/groupAttributes.ts
Normal file
8
ui-src/lib/types/group/groupAttributes.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { Uuid } from '@ui/lib/types/utils/uuid';
|
||||
|
||||
export const GROUP_TYPE: unique symbol = Symbol.for('group');
|
||||
|
||||
export type GroupAttributes = {
|
||||
type: 'group' | typeof GROUP_TYPE;
|
||||
shapes?: Uuid[];
|
||||
};
|
12
ui-src/lib/types/group/groupShape.d.ts
vendored
Normal file
12
ui-src/lib/types/group/groupShape.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { ShapeAttributes } from '@ui/lib/types/shape/shapeAttributes';
|
||||
import { ShapeBaseAttributes } from '@ui/lib/types/shape/shapeBaseAttributes';
|
||||
import { ShapeGeomAttributes } from '@ui/lib/types/shape/shapeGeomAttributes';
|
||||
import { Children } from '@ui/lib/types/utils/children';
|
||||
|
||||
import { GroupAttributes } from './groupAttributes';
|
||||
|
||||
export type GroupShape = ShapeBaseAttributes &
|
||||
ShapeGeomAttributes &
|
||||
ShapeAttributes &
|
||||
GroupAttributes &
|
||||
Children;
|
14
ui-src/lib/types/image/imageAttributes.ts
Normal file
14
ui-src/lib/types/image/imageAttributes.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Uuid } from '@ui/lib/types/utils/uuid';
|
||||
|
||||
export const IMAGE_TYPE: unique symbol = Symbol.for('image');
|
||||
|
||||
export type ImageAttributes = {
|
||||
type: 'image' | typeof IMAGE_TYPE;
|
||||
dataUri?: string; //@TODO: check how this works because it's not defined in penpot, it's just used in the export
|
||||
metadata: {
|
||||
width: number;
|
||||
height: number;
|
||||
mtype?: string;
|
||||
id?: Uuid;
|
||||
};
|
||||
};
|
12
ui-src/lib/types/image/imageShape.d.ts
vendored
Normal file
12
ui-src/lib/types/image/imageShape.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { LayoutChildAttributes } from '@ui/lib/types/layout/layoutChildAttributes';
|
||||
import { ShapeAttributes } from '@ui/lib/types/shape/shapeAttributes';
|
||||
import { ShapeBaseAttributes } from '@ui/lib/types/shape/shapeBaseAttributes';
|
||||
import { ShapeGeomAttributes } from '@ui/lib/types/shape/shapeGeomAttributes';
|
||||
|
||||
import { ImageAttributes } from './imageAttributes';
|
||||
|
||||
export type ImageShape = ShapeBaseAttributes &
|
||||
ShapeGeomAttributes &
|
||||
ShapeAttributes &
|
||||
ImageAttributes &
|
||||
LayoutChildAttributes;
|
14
ui-src/lib/types/layout/gridCell.d.ts
vendored
Normal file
14
ui-src/lib/types/layout/gridCell.d.ts
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Uuid } from '@ui/lib/types/utils/uuid';
|
||||
|
||||
export type GridCell = {
|
||||
id?: Uuid;
|
||||
areaName?: string;
|
||||
row: number;
|
||||
rowSpan: number;
|
||||
column: number;
|
||||
columnSpan: number;
|
||||
position?: 'auto' | 'manual' | 'area';
|
||||
alignSelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch';
|
||||
justifySelf?: 'auto' | 'start' | 'end' | 'center' | 'stretch';
|
||||
shapes?: Uuid[];
|
||||
};
|
4
ui-src/lib/types/layout/gridTrack.d.ts
vendored
Normal file
4
ui-src/lib/types/layout/gridTrack.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
export type GridTrack = {
|
||||
type: 'percent' | 'flex' | 'auto' | 'fixed';
|
||||
value?: number;
|
||||
};
|
46
ui-src/lib/types/layout/layoutAttributes.d.ts
vendored
Normal file
46
ui-src/lib/types/layout/layoutAttributes.d.ts
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { GridCell } from '@ui/lib/types/layout/gridCell';
|
||||
import { GridTrack } from '@ui/lib/types/layout/gridTrack';
|
||||
import { Uuid } from '@ui/lib/types/utils/uuid';
|
||||
|
||||
type JustifyAlignContent =
|
||||
| 'start'
|
||||
| 'center'
|
||||
| 'end'
|
||||
| 'space-between'
|
||||
| 'space-around'
|
||||
| 'space-evenly'
|
||||
| 'stretch';
|
||||
|
||||
type JustifyAlignItems = 'start' | 'end' | 'center' | 'stretch';
|
||||
|
||||
export type LayoutAttributes = {
|
||||
layout?: 'flex' | 'grid';
|
||||
layoutFlexDir?:
|
||||
| 'row'
|
||||
| 'reverse-row'
|
||||
| 'row-reverse'
|
||||
| 'column'
|
||||
| 'reverse-column'
|
||||
| 'column-reverse';
|
||||
layoutGap?: {
|
||||
rowGap?: number;
|
||||
columnGap?: number;
|
||||
};
|
||||
layoutGapType?: 'simple' | 'multiple';
|
||||
layoutWrapType?: 'wrap' | 'nowrap' | 'no-wrap';
|
||||
layoutPaddingType?: 'simple' | 'multiple';
|
||||
layoutPadding?: {
|
||||
p1?: number;
|
||||
p2?: number;
|
||||
p3?: number;
|
||||
p4?: number;
|
||||
};
|
||||
layoutJustifyContent?: JustifyAlignContent;
|
||||
layoutJustifyItems?: JustifyAlignItems;
|
||||
layoutAlignContent?: JustifyAlignContent;
|
||||
layoutAlignItems?: JustifyAlignItems;
|
||||
layoutGridDir?: 'row' | 'column';
|
||||
layoutGridRows?: GridTrack[];
|
||||
layoutGridColumns?: GridTrack[];
|
||||
layoutGridCells?: { [uuid: Uuid]: GridCell };
|
||||
};
|
55
ui-src/lib/types/layout/layoutChildAttributes.ts
Normal file
55
ui-src/lib/types/layout/layoutChildAttributes.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
export const ITEM_MARGIN_SIMPLE_TYPE: unique symbol = Symbol.for('simple');
|
||||
export const ITEM_MARGIN_MULTIPLE_TYPE: unique symbol = Symbol.for('multiple');
|
||||
export const ITEM_HSIZING_FILL: unique symbol = Symbol.for('fill');
|
||||
export const ITEM_HSIZING_FIX: unique symbol = Symbol.for('fix');
|
||||
export const ITEM_HSIZING_AUTO: unique symbol = Symbol.for('auto');
|
||||
export const ITEM_VSIZING_FILL: unique symbol = Symbol.for('fill');
|
||||
export const ITEM_VSIZING_FIX: unique symbol = Symbol.for('fix');
|
||||
export const ITEM_VSIZING_AUTO: unique symbol = Symbol.for('auto');
|
||||
export const ITEM_ALIGN_SELF_START: unique symbol = Symbol.for('start');
|
||||
export const ITEM_ALIGN_SELF_END: unique symbol = Symbol.for('end');
|
||||
export const ITEM_ALIGN_SELF_CENTER: unique symbol = Symbol.for('center');
|
||||
export const ITEM_ALIGN_SELF_STRETCH: unique symbol = Symbol.for('stretch');
|
||||
|
||||
export type LayoutChildAttributes = {
|
||||
layoutItemMarginType?:
|
||||
| 'simple'
|
||||
| 'multiple'
|
||||
| typeof ITEM_MARGIN_SIMPLE_TYPE
|
||||
| typeof ITEM_MARGIN_MULTIPLE_TYPE;
|
||||
layoutItemMargin?: {
|
||||
m1?: number;
|
||||
m2?: number;
|
||||
m3?: number;
|
||||
m4?: number;
|
||||
};
|
||||
layoutItemMaxH?: number;
|
||||
layoutItemMinH?: number;
|
||||
layoutItemMaxW?: number;
|
||||
layoutItemMinW?: number;
|
||||
layoutItemHSizing?:
|
||||
| 'fill'
|
||||
| 'fix'
|
||||
| 'auto'
|
||||
| typeof ITEM_HSIZING_FILL
|
||||
| typeof ITEM_HSIZING_FIX
|
||||
| typeof ITEM_HSIZING_AUTO;
|
||||
layoutItemVSizing?:
|
||||
| 'fill'
|
||||
| 'fix'
|
||||
| 'auto'
|
||||
| typeof ITEM_VSIZING_FILL
|
||||
| typeof ITEM_VSIZING_FIX
|
||||
| typeof ITEM_VSIZING_AUTO;
|
||||
layoutItemAlignSelf?:
|
||||
| 'start'
|
||||
| 'end'
|
||||
| 'center'
|
||||
| 'stretch'
|
||||
| typeof ITEM_ALIGN_SELF_START
|
||||
| typeof ITEM_ALIGN_SELF_END
|
||||
| typeof ITEM_ALIGN_SELF_CENTER
|
||||
| typeof ITEM_ALIGN_SELF_STRETCH;
|
||||
layoutItemAbsolute?: boolean;
|
||||
layoutItemZIndex?: number;
|
||||
};
|
48
ui-src/lib/types/path/PathContent.ts
Normal file
48
ui-src/lib/types/path/PathContent.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
export const VECTOR_LINE_TO: unique symbol = Symbol.for('line-to');
|
||||
export const VECTOR_CLOSE_PATH: unique symbol = Symbol.for('close-path');
|
||||
export const VECTOR_MOVE_TO: unique symbol = Symbol.for('move-to');
|
||||
export const VECTOR_CURVE_TO: unique symbol = Symbol.for('curve-to');
|
||||
|
||||
export type PathContent = Segment[];
|
||||
export type Segment = LineTo | ClosePath | MoveTo | CurveTo;
|
||||
export type Command =
|
||||
| 'line-to'
|
||||
| 'close-path'
|
||||
| 'move-to'
|
||||
| 'curve-to'
|
||||
| typeof VECTOR_LINE_TO
|
||||
| typeof VECTOR_CLOSE_PATH
|
||||
| typeof VECTOR_MOVE_TO
|
||||
| typeof VECTOR_CURVE_TO;
|
||||
|
||||
type LineTo = {
|
||||
command: 'line-to' | typeof VECTOR_LINE_TO;
|
||||
params: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
|
||||
type ClosePath = {
|
||||
command: 'close-path' | typeof VECTOR_CLOSE_PATH;
|
||||
};
|
||||
|
||||
type MoveTo = {
|
||||
command: 'move-to' | typeof VECTOR_MOVE_TO;
|
||||
params: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
|
||||
type CurveTo = {
|
||||
command: 'curve-to' | typeof VECTOR_CURVE_TO;
|
||||
params: {
|
||||
x: number;
|
||||
y: number;
|
||||
c1x: number;
|
||||
c1y: number;
|
||||
c2x: number;
|
||||
c2y: number;
|
||||
};
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue