Skip to content
This repository was archived by the owner on Jul 13, 2024. It is now read-only.

Commit 80ad68a

Browse files
authored
Merge pull request #31 from physical-art/24-phy-42-render-dtif-json-object-using-svg-html-css
24 phy 42 render dtif json object using svg html css
2 parents 3c8e20d + 734e460 commit 80ad68a

File tree

98 files changed

+3541
-462
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

98 files changed

+3541
-462
lines changed

apps/figma-plugin/package.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
"@types/react": "^18.0.37",
5959
"@types/react-dom": "^18.0.11",
6060
"@types/ws": "^8.5.4",
61-
"copy-webpack-plugin": "^11.0.0",
6261
"css-loader": "^6.7.3",
6362
"dotenv": "^16.0.3",
6463
"eslint": "^8.38.0",
@@ -69,9 +68,6 @@
6968
"postcss": "^8.4.23",
7069
"postcss-loader": "^7.2.4",
7170
"prop-types": "15.8.1",
72-
"rollup-plugin-copy": "^3.4.0",
73-
"rollup-plugin-import-css": "^3.2.1",
74-
"rollup-plugin-postcss": "^4.0.2",
7571
"storybook": "7.0.18",
7672
"style-loader": "^3.3.2",
7773
"tailwindcss": "^3.3.1",

apps/figma-plugin/rollup.config.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import yargs from 'yargs/yargs';
1111
import { hideBin } from 'yargs/helpers';
1212
import json from '@rollup/plugin-json';
1313
import bundleSize from 'rollup-plugin-bundle-size';
14+
import { visualizer } from 'rollup-plugin-visualizer';
1415

1516
// https://github.com/egoist/rollup-plugin-esbuild/issues/361
1617
import _esbuild from 'rollup-plugin-esbuild';
@@ -76,7 +77,16 @@ const sharedPlugins = {
7677
),
7778
'process.env.PREVIEW_MODE': isPreview,
7879
}),
80+
// Determine bundle size
7981
bundleSize: bundleSize(),
82+
// Visualize bundle
83+
visualize: (appName) =>
84+
visualizer({
85+
title: appName,
86+
filename: path.resolve(process.cwd(), `./.compile/${appName}.html`),
87+
sourcemap: true,
88+
gzipSize: true,
89+
}),
8090
};
8191

8292
// ============================================================================
@@ -105,6 +115,7 @@ export default [
105115
...parseDotenv('./.env.background'),
106116
}),
107117
sharedPlugins.bundleSize,
118+
sharedPlugins.visualize('background'),
108119
],
109120
external: ['react', 'react-dom'],
110121
},
@@ -115,6 +126,7 @@ export default [
115126
file: './dist/ui.js',
116127
format: 'es',
117128
sourcemap: !isProduction,
129+
inlineDynamicImports: true,
118130
},
119131
plugins: [
120132
sharedPlugins.nodeResolve,
@@ -165,6 +177,7 @@ export default [
165177
],
166178
}),
167179
sharedPlugins.bundleSize,
180+
sharedPlugins.visualize('ui'),
168181
],
169182
},
170183
];

apps/figma-plugin/src/background/events/intermediate-format-export/process-node.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export async function processNode(
3030
}
3131
return uploadDataToBucket(key, data, contentType?.mimeType);
3232
};
33+
3334
// Format the node for export
3435
const toExportNode = await formatFrameToScene(node, {
3536
...options,
@@ -59,7 +60,7 @@ export async function processNode(
5960

6061
// Upload the node as JSON string to bucket
6162
const json = JSON.stringify(toExportNode);
62-
const key = sha256(json);
63+
const key = options.nameAsBucketId ? node.name : sha256(json);
6364
await uploadDataToBucket(key, stringToUint8Array(json), 'application/json');
6465

6566
// Post success message and notify the user

apps/figma-plugin/src/shared/types/background-events.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ export interface TIntermediateFormatExportEvent extends TBaseFigmaMessageEvent {
99
FrameNode | ComponentNode | InstanceNode,
1010
'name' | 'id'
1111
>[];
12-
options: Omit<TFormatNodeOptions, 'uploadStaticData'>;
12+
options: Omit<TFormatNodeOptions, 'uploadStaticData'> & {
13+
nameAsBucketId?: boolean;
14+
};
1315
};
1416
}
1517

apps/figma-plugin/src/ui/routes/dtif/ExportPreview.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,29 @@ const ExportPreview: React.FC<TProps> = (props) => {
3636
// Render node as JSXs
3737
React.useEffect(() => {
3838
const renderNodeAsJSX = async () => {
39-
if (node != null) {
40-
const jsxNode = await (isFrameNode(node)
41-
? renderRelativeParent(node, 0.2)
42-
: renderNode(node, { isLocked: false, isVisible: true }));
43-
setNodeAsJSX(jsxNode);
44-
setNodeAsJSXString(toJSXString(jsxNode as React.ReactElement));
39+
if (node != null && activeTab === EPreviewTabs.PREVIEW) {
40+
setTimeout(async () => {
41+
const jsxNode = await (isFrameNode(node)
42+
? renderRelativeParent(node, 0.2)
43+
: renderNode(node, { isLocked: false, isVisible: true }));
44+
setNodeAsJSX(jsxNode);
45+
setNodeAsJSXString(toJSXString(jsxNode as React.ReactElement));
46+
});
4547
}
4648
};
4749
renderNodeAsJSX();
48-
}, [node]);
50+
}, [node, activeTab === EPreviewTabs.PREVIEW]);
4951

5052
// Find an highlight all code snippets in the page
5153
React.useEffect(() => {
5254
// setIsLoadingHighlight(true); // Directly set in callback to avoid waiting for next render cycle
5355
// Wrapped in timeout to avoid UI lag and instead highlight after switch
54-
setTimeout(() => {
55-
Prism.highlightAll();
56-
setIsLoadingHighlight(false);
57-
});
56+
if (activeTab === EPreviewTabs.JSX) {
57+
setTimeout(() => {
58+
Prism.highlightAll();
59+
setIsLoadingHighlight(false);
60+
});
61+
}
5862
}, [nodeAsJSXString, activeTab === EPreviewTabs.JSX]);
5963

6064
// ============================================================================

apps/figma-plugin/src/ui/routes/dtif/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,19 @@ const DTIFExport: React.FC = () => {
7171

7272
// Send export event to sandbox
7373
if (selectedFrame != null) {
74+
// TODO: make config accessible from UI
7475
uiHandler.postMessage('intermediate-format-export', {
7576
selectedElements: [selectedFrame],
7677
options: {
7778
svg: {
7879
inline: true,
7980
exportIdentifierRegex: '_svg$',
81+
frameToSVG: false,
8082
},
8183
gradientFill: {
8284
inline: true,
8385
},
86+
nameAsBucketId: true,
8487
},
8588
});
8689
} else {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Design Decisions
2+
3+
## Local Definition of Defs
4+
5+
In SVG, the `<defs>` element is used to define graphical elements that can be reused later with the `<use>` tag or as a clipping path, gradient definition, etc. Typically, these `<defs>` elements are defined at the top level of the SVG, which provides a centralized repository of reusable elements.
6+
7+
However, in complex, dynamic SVG structures, it can be more practical and efficient to define `<defs>` locally within each grouping (`<g>`) element. This approach keeps each grouping unit contained with its clip path and transformations.
8+
9+
One of the key reasons to choose local definition of `<defs>` is the consideration for performance and maintainability. Specifically, in a dynamic and interactive application where SVG elements are frequently updated, moved, or transformed, local `<defs>` help avoid the overhead of traversing the SVG tree multiple times and synchronize transformations across different parts of the SVG.
10+
11+
While not the most "typical" approach, local definition of `<defs>` is a valid and often beneficial strategy for managing complex and dynamic SVG structures.
12+
13+
# Good To Know
14+
15+
## G Elements and Width/Height
16+
17+
In SVG, the `<g>` element is used to group SVG shapes together. Once grouped, transformations and styles can be applied to the whole group as a single entity. However, unlike some other SVG elements such as `<rect>`, the `<g>` element does not have `width` and `height` attributes.
18+
19+
The dimensions of a `<g>` element are determined by the contents within it. Any attempt to apply `width` or `height` directly to a `<g>` element will have no effect. This is because `<g>` elements are not directly rendered; their purpose is to group other elements.
20+
21+
For example, consider the following SVG structure:
22+
23+
```html
24+
<svg width="200" height="200" viewBox="0 0 200 200">
25+
<g width="50" height="50" fill="green">
26+
<rect width="50" height="50" fill="blue"></rect>
27+
</g>
28+
</svg>
29+
```
30+
31+
In this case, the `<rect>` inside the `<g>` element is set to be 50 units wide and 50 units high. However, the `width` and `height` attributes applied to the `<g>` element are ignored. The size of the `<g>` element is determined by the dimensions and positions of its content, not by any `width` or `height` attributes applied directly to the `<g>` itself.
32+
33+
## Transformations on G Elements and Child Element Positioning
34+
35+
When a transformation (such as translate, rotate, scale, etc.) is applied to a `<g>` element, it affects the coordinate system for that group and all of its child elements. This means the positions of the child elements become relative to the coordinate system of the parent `<g>` element, not the overall SVG.
36+
37+
For example, consider the following SVG structure:
38+
39+
```html
40+
<svg width="200" height="200" viewBox="0 0 200 200">
41+
<g transform="translate(50,50)">
42+
<rect width="50" height="50" fill="blue"></rect>
43+
</g>
44+
</svg>
45+
```
46+
47+
In this case, a `translate(50,50)` transformation is applied to the `<g>` element. This means that the origin of the coordinate system for this group is moved 50 units to the right and 50 units down. As a result, the `<rect>` inside the `<g>` element, which is positioned at (0,0) relative to the `<g>`, will actually appear at the (50,50) position in the overall SVG. This is because the rectangle's position is relative to its parent group, not the overall SVG.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './nodes';
2+
export * from './other';
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {
2+
createEllipsePath,
3+
getIdentifier,
4+
transformToCSS,
5+
} from '@/components/canvas/utils';
6+
import { TEllipseNode } from '@pda/types/dtif';
7+
import React from 'react';
8+
import { Fill } from '../other';
9+
10+
const Ellipse: React.FC<TProps> = (props) => {
11+
const { node, index = 0 } = props;
12+
const fillClipPathId = React.useMemo(
13+
() =>
14+
getIdentifier({
15+
id: node.id,
16+
index,
17+
type: 'ellipse',
18+
category: 'fill-clip',
19+
isDefinition: true,
20+
}),
21+
[node.id]
22+
);
23+
const svgPath = React.useMemo(
24+
() =>
25+
createEllipsePath({
26+
arcData: node.arcData,
27+
width: node.width,
28+
height: node.height,
29+
}),
30+
[node.arcData, node.width, node.height]
31+
);
32+
33+
return (
34+
<g
35+
id={getIdentifier({
36+
id: node.id,
37+
index,
38+
type: 'ellipse',
39+
})}
40+
style={{
41+
display: node.isVisible ? 'block' : 'none',
42+
opacity: node.opacity,
43+
pointerEvents: node.isLocked ? 'none' : 'auto',
44+
...transformToCSS(node.relativeTransform),
45+
}}
46+
>
47+
<defs>
48+
<clipPath id={fillClipPathId}>
49+
<path d={svgPath} fill={'red'} />
50+
</clipPath>
51+
</defs>
52+
<Fill node={node} clipPathId={fillClipPathId} />
53+
</g>
54+
);
55+
};
56+
57+
export default Ellipse;
58+
59+
type TProps = {
60+
node: TEllipseNode;
61+
index?: number;
62+
};
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { getIdentifier, transformToCSS } from '@/components/canvas/utils';
2+
import { TFrameNode } from '@pda/types/dtif';
3+
import React from 'react';
4+
import { Fill } from '../other';
5+
import Node from './Node';
6+
7+
const Frame: React.FC<TProps> = (props) => {
8+
const { node, index = 0 } = props;
9+
const contentClipPathId = React.useMemo(
10+
() =>
11+
getIdentifier({
12+
id: node.id,
13+
index,
14+
type: 'frame',
15+
category: 'content-clip',
16+
isDefinition: true,
17+
}),
18+
[node.id]
19+
);
20+
const fillClipPathId = React.useMemo(
21+
() =>
22+
getIdentifier({
23+
id: node.id,
24+
index,
25+
type: 'frame',
26+
category: 'fill-clip',
27+
isDefinition: true,
28+
}),
29+
[node.id]
30+
);
31+
32+
return (
33+
<g
34+
id={getIdentifier({
35+
id: node.id,
36+
index,
37+
type: 'frame',
38+
})}
39+
style={{ ...transformToCSS(node.relativeTransform) }}
40+
>
41+
{/* Frame Content */}
42+
<g
43+
id={getIdentifier({
44+
id: node.id,
45+
index,
46+
type: 'frame',
47+
category: 'content',
48+
})}
49+
clipPath={node.clipsContent ? `url(#${contentClipPathId})` : undefined}
50+
>
51+
<defs>
52+
<clipPath id={fillClipPathId}>
53+
<rect width={node.width} height={node.height} />
54+
</clipPath>
55+
</defs>
56+
<Fill node={node} clipPathId={fillClipPathId} />
57+
<g
58+
id={getIdentifier({
59+
id: node.id,
60+
index,
61+
type: 'frame',
62+
category: 'children',
63+
})}
64+
>
65+
{node.children.map((child, i) => (
66+
<Node
67+
key={getIdentifier({
68+
id: node.id,
69+
index: i,
70+
type: 'child',
71+
})}
72+
index={i}
73+
node={child}
74+
/>
75+
))}
76+
</g>
77+
</g>
78+
79+
{/* Clips Content */}
80+
{node.clipsContent && (
81+
<defs>
82+
<clipPath id={contentClipPathId}>
83+
<rect width={node.width} height={node.height} />
84+
</clipPath>
85+
</defs>
86+
)}
87+
</g>
88+
);
89+
};
90+
91+
export default Frame;
92+
93+
type TProps = {
94+
node: TFrameNode;
95+
index?: number;
96+
};

0 commit comments

Comments
 (0)