Skip to content

Commit 384e7ed

Browse files
authored
Issue 155 rotation (#164)
* made rotations better but still not aligning items right * Added a warning for using nested rotations
1 parent 3f64849 commit 384e7ed

25 files changed

+185
-124
lines changed

packages/backend/src/common/numToAutoFixed.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { indentStringFlutter } from "./indentString";
22

33
// this is necessary to avoid a height of 4.999999523162842.
4-
export const sliceNum = (num: number): string => {
4+
export const numberToFixedString = (num: number): string => {
55
return num.toFixed(2).replace(/\.00$/, "");
66
};
77

8+
export const roundToNearestDecimal = (decimal: number) => (n: number) =>
9+
Math.round(n * 10 ** decimal) / 10 ** decimal;
10+
export const roundToNearestHundreth = roundToNearestDecimal(2);
11+
812
export const printPropertyIfNotDefault = (
913
propertyName: string,
1014
propertyValue: any,
@@ -54,14 +58,14 @@ export const generateWidgetCode = (
5458
return `${key}: [\n${indentStringFlutter(value.join(",\n"))},\n],`;
5559
} else {
5660
return `${key}: ${
57-
typeof value === "number" ? sliceNum(value) : value
61+
typeof value === "number" ? numberToFixedString(value) : value
5862
},`;
5963
}
6064
});
6165

6266
const positionedValuesString = (positionedValues || [])
6367
.map((value) => {
64-
return typeof value === "number" ? sliceNum(value) : value;
68+
return typeof value === "number" ? numberToFixedString(value) : value;
6569
})
6670
.join(", ");
6771

packages/backend/src/common/parseJSX.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { sliceNum } from "./numToAutoFixed";
1+
import { numberToFixedString } from "./numToAutoFixed";
22

33
export const formatWithJSX = (
44
property: string,
@@ -13,9 +13,9 @@ export const formatWithJSX = (
1313

1414
if (typeof value === "number") {
1515
if (isJsx) {
16-
return `${jsx_property}: ${sliceNum(value)}`;
16+
return `${jsx_property}: ${numberToFixedString(value)}`;
1717
} else {
18-
return `${property}: ${sliceNum(value)}px`;
18+
return `${property}: ${numberToFixedString(value)}px`;
1919
}
2020
} else if (isJsx) {
2121
return `${jsx_property}: '${value}'`;

packages/backend/src/flutter/builderImpl/flutterBlend.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { generateWidgetCode, sliceNum } from "../../common/numToAutoFixed";
1+
import {
2+
generateWidgetCode,
3+
numberToFixedString,
4+
} from "../../common/numToAutoFixed";
25

36
/**
47
* https://api.flutter.dev/flutter/widgets/Opacity-class.html
@@ -9,7 +12,7 @@ export const flutterOpacity = (
912
): string => {
1013
if (node.opacity !== undefined && node.opacity !== 1 && child !== "") {
1114
return generateWidgetCode("Opacity", {
12-
opacity: sliceNum(node.opacity),
15+
opacity: numberToFixedString(node.opacity),
1316
child: child,
1417
});
1518
}
@@ -43,7 +46,7 @@ export const flutterRotation = (node: LayoutMixin, child: string): string => {
4346
Math.round(node.rotation) !== 0
4447
) {
4548
return generateWidgetCode("Transform", {
46-
transform: `Matrix4.identity()..translate(0.0, 0.0)..rotateZ(${sliceNum(
49+
transform: `Matrix4.identity()..translate(0.0, 0.0)..rotateZ(${numberToFixedString(
4750
node.rotation * (-3.14159 / 180),
4851
)})`,
4952
child: child,

packages/backend/src/flutter/builderImpl/flutterColor.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { rgbTo8hex, gradientAngle } from "../../common/color";
22
import { addWarning } from "../../common/commonConversionWarnings";
3-
import { generateWidgetCode, sliceNum } from "../../common/numToAutoFixed";
3+
import {
4+
generateWidgetCode,
5+
numberToFixedString,
6+
} from "../../common/numToAutoFixed";
47
import { retrieveTopFill } from "../../common/retrieveFill";
58
import { nearestValue } from "../../tailwind/conversionTables";
69

@@ -101,11 +104,11 @@ const flutterRadialGradient = (fill: GradientPaint): string => {
101104
.map((d) => flutterColor(d.color, d.color.a))
102105
.join(", ");
103106

104-
const x = sliceNum(fill.gradientTransform[0][2]);
105-
const y = sliceNum(fill.gradientTransform[1][2]);
107+
const x = numberToFixedString(fill.gradientTransform[0][2]);
108+
const y = numberToFixedString(fill.gradientTransform[1][2]);
106109
const scaleX = fill.gradientTransform[0][0];
107110
const scaleY = fill.gradientTransform[1][1];
108-
const r = sliceNum(Math.sqrt(scaleX * scaleX + scaleY * scaleY));
111+
const r = numberToFixedString(Math.sqrt(scaleX * scaleX + scaleY * scaleY));
109112

110113
return generateWidgetCode("RadialGradient", {
111114
center: `Alignment(${x}, ${y})`,
@@ -119,10 +122,10 @@ const flutterAngularGradient = (fill: GradientPaint): string => {
119122
.map((d) => flutterColor(d.color, d.color.a))
120123
.join(", ");
121124

122-
const x = sliceNum(fill.gradientTransform[0][2]);
123-
const y = sliceNum(fill.gradientTransform[1][2]);
124-
const startAngle = sliceNum(-fill.gradientTransform[0][0]);
125-
const endAngle = sliceNum(-fill.gradientTransform[0][1]);
125+
const x = numberToFixedString(fill.gradientTransform[0][2]);
126+
const y = numberToFixedString(fill.gradientTransform[1][2]);
127+
const startAngle = numberToFixedString(-fill.gradientTransform[0][0]);
128+
const endAngle = numberToFixedString(-fill.gradientTransform[0][1]);
126129

127130
return generateWidgetCode("SweepGradient", {
128131
center: `Alignment(${x}, ${y})`,

packages/backend/src/flutter/builderImpl/flutterPadding.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
generateWidgetCode,
33
skipDefaultProperty,
4-
sliceNum,
4+
numberToFixedString,
55
} from "../../common/numToAutoFixed";
66
import { commonPadding } from "../../common/commonPadding";
77

@@ -18,22 +18,25 @@ export const flutterPadding = (node: InferredAutoLayoutResult): string => {
1818

1919
if ("all" in padding) {
2020
return skipDefaultProperty(
21-
`const EdgeInsets.all(${sliceNum(padding.all)})`,
21+
`const EdgeInsets.all(${numberToFixedString(padding.all)})`,
2222
"const EdgeInsets.all(0)",
2323
);
2424
}
2525

2626
if ("horizontal" in padding) {
2727
return generateWidgetCode("const EdgeInsets.symmetric", {
28-
horizontal: skipDefaultProperty(sliceNum(padding.horizontal), "0"),
29-
vertical: skipDefaultProperty(sliceNum(padding.vertical), "0"),
28+
horizontal: skipDefaultProperty(
29+
numberToFixedString(padding.horizontal),
30+
"0",
31+
),
32+
vertical: skipDefaultProperty(numberToFixedString(padding.vertical), "0"),
3033
});
3134
}
3235

3336
return generateWidgetCode("const EdgeInsets.only", {
34-
top: skipDefaultProperty(sliceNum(padding.top), "0"),
35-
left: skipDefaultProperty(sliceNum(padding.left), "0"),
36-
right: skipDefaultProperty(sliceNum(padding.right), "0"),
37-
bottom: skipDefaultProperty(sliceNum(padding.bottom), "0"),
37+
top: skipDefaultProperty(numberToFixedString(padding.top), "0"),
38+
left: skipDefaultProperty(numberToFixedString(padding.left), "0"),
39+
right: skipDefaultProperty(numberToFixedString(padding.right), "0"),
40+
bottom: skipDefaultProperty(numberToFixedString(padding.bottom), "0"),
3841
});
3942
};

packages/backend/src/flutter/builderImpl/flutterShadow.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { rgbTo8hex } from "../../common/color";
2-
import { generateWidgetCode, sliceNum } from "../../common/numToAutoFixed";
2+
import {
3+
generateWidgetCode,
4+
numberToFixedString,
5+
} from "../../common/numToAutoFixed";
36
import { indentStringFlutter } from "../../common/indentString";
47

58
// TODO Document it can't do flutter shadows.
@@ -19,11 +22,13 @@ export const flutterShadow = (node: SceneNode): string => {
1922
effect.color,
2023
effect.color.a,
2124
).toUpperCase()})`,
22-
blurRadius: sliceNum(effect.radius),
23-
offset: `Offset(${sliceNum(effect.offset.x)}, ${sliceNum(
25+
blurRadius: numberToFixedString(effect.radius),
26+
offset: `Offset(${numberToFixedString(effect.offset.x)}, ${numberToFixedString(
2427
effect.offset.y,
2528
)})`,
26-
spreadRadius: effect.spread ? sliceNum(effect.spread) : "0",
29+
spreadRadius: effect.spread
30+
? numberToFixedString(effect.spread)
31+
: "0",
2732
});
2833
}
2934
});

packages/backend/src/flutter/builderImpl/flutterSize.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { nodeSize } from "../../common/nodeWidthHeight";
2-
import { sliceNum } from "../../common/numToAutoFixed";
2+
import { numberToFixedString } from "../../common/numToAutoFixed";
33

44
// Used in tests.
55
export const flutterSizeWH = (node: SceneNode): string => {
@@ -23,7 +23,7 @@ export const flutterSize = (
2323
// this cast will always be true, since nodeWidthHeight was called with false to relative.
2424
let propWidth = "";
2525
if (typeof size.width === "number") {
26-
propWidth = sliceNum(size.width);
26+
propWidth = numberToFixedString(size.width);
2727
} else if (size.width === "fill") {
2828
// When parent is a Row, child must be Expanded.
2929
if (
@@ -39,7 +39,7 @@ export const flutterSize = (
3939

4040
let propHeight = "";
4141
if (typeof size.height === "number") {
42-
propHeight = sliceNum(size.height);
42+
propHeight = numberToFixedString(size.height);
4343
} else if (size.height === "fill") {
4444
// When parent is a Column, child must be Expanded.
4545
if (

packages/backend/src/flutter/flutterContainer.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
generateWidgetCode,
1111
skipDefaultProperty,
1212
} from "../common/numToAutoFixed";
13-
import { sliceNum } from "../common/numToAutoFixed";
13+
import { numberToFixedString } from "../common/numToAutoFixed";
1414
import { getCommonRadius } from "../common/commonRadius";
1515
import { commonStroke } from "../common/commonStroke";
1616

@@ -180,12 +180,12 @@ const generateStarBorder = (node: StarNode): string => {
180180

181181
return generateWidgetCode("StarBorder", {
182182
side: generateBorderSideCode(node),
183-
points: sliceNum(points),
184-
innerRadiusRatio: sliceNum(innerRadiusRatio),
185-
pointRounding: sliceNum(pointRounding),
186-
valleyRounding: sliceNum(valleyRounding),
187-
rotation: sliceNum(rotation),
188-
squash: sliceNum(squash),
183+
points: numberToFixedString(points),
184+
innerRadiusRatio: numberToFixedString(innerRadiusRatio),
185+
pointRounding: numberToFixedString(pointRounding),
186+
valleyRounding: numberToFixedString(valleyRounding),
187+
rotation: numberToFixedString(rotation),
188+
squash: numberToFixedString(squash),
189189
});
190190
};
191191

@@ -219,7 +219,7 @@ const generatePolygonBorder = (node: PolygonNode): string => {
219219

220220
return generateWidgetCode("StarBorder.polygon", {
221221
side: generateBorderSideCode(node),
222-
sides: sliceNum(points),
222+
sides: numberToFixedString(points),
223223
borderRadius: generateBorderRadius(node),
224224
});
225225
};
@@ -230,24 +230,24 @@ const generateBorderRadius = (node: SceneNode): string => {
230230
if (radius.all === 0) {
231231
return "";
232232
}
233-
return `BorderRadius.circular(${sliceNum(radius.all)})`;
233+
return `BorderRadius.circular(${numberToFixedString(radius.all)})`;
234234
}
235235

236236
return generateWidgetCode("BorderRadius.only", {
237237
topLeft: skipDefaultProperty(
238-
`Radius.circular(${sliceNum(radius.topLeft)})`,
238+
`Radius.circular(${numberToFixedString(radius.topLeft)})`,
239239
"Radius.circular(0)",
240240
),
241241
topRight: skipDefaultProperty(
242-
`Radius.circular(${sliceNum(radius.topRight)})`,
242+
`Radius.circular(${numberToFixedString(radius.topRight)})`,
243243
"Radius.circular(0)",
244244
),
245245
bottomLeft: skipDefaultProperty(
246-
`Radius.circular(${sliceNum(radius.bottomLeft)})`,
246+
`Radius.circular(${numberToFixedString(radius.bottomLeft)})`,
247247
"Radius.circular(0)",
248248
),
249249
bottomRight: skipDefaultProperty(
250-
`Radius.circular(${sliceNum(radius.bottomRight)})`,
250+
`Radius.circular(${numberToFixedString(radius.bottomRight)})`,
251251
"Radius.circular(0)",
252252
),
253253
});

packages/backend/src/flutter/flutterTextBuilder.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
generateWidgetCode,
33
skipDefaultProperty,
4-
sliceNum,
4+
numberToFixedString,
55
} from "./../common/numToAutoFixed";
66
import { FlutterDefaultBuilder } from "./flutterDefaultBuilder";
77
import { flutterColorFromFills } from "./builderImpl/flutterColor";
@@ -71,7 +71,7 @@ export class FlutterTextBuilder extends FlutterDefaultBuilder {
7171
return segments.map((segment) => {
7272
const color = flutterColorFromFills(segment.fills);
7373

74-
const fontSize = `${sliceNum(segment.fontSize)}`;
74+
const fontSize = `${numberToFixedString(segment.fontSize)}`;
7575
const fontStyle = this.fontStyle(segment.fontName);
7676
const fontFamily = `'${segment.fontName.family}'`;
7777
const fontWeight = `FontWeight.w${segment.fontWeight}`;
@@ -139,16 +139,16 @@ export class FlutterTextBuilder extends FlutterDefaultBuilder {
139139
case "AUTO":
140140
return "";
141141
case "PIXELS":
142-
return sliceNum(lineHeight.value / fontSize);
142+
return numberToFixedString(lineHeight.value / fontSize);
143143
case "PERCENT":
144-
return sliceNum(lineHeight.value / 100);
144+
return numberToFixedString(lineHeight.value / 100);
145145
}
146146
}
147147

148148
letterSpacing(letterSpacing: LetterSpacing, fontSize: number): string {
149149
const value = commonLetterSpacing(letterSpacing, fontSize);
150150
if (value) {
151-
return sliceNum(value);
151+
return numberToFixedString(value);
152152
}
153153
return "";
154154
}

packages/backend/src/html/builderImpl/htmlBlend.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { sliceNum } from "../../common/numToAutoFixed";
1+
import { roundToNearestHundreth } from "./../../common/numToAutoFixed";
2+
import { addWarning } from "../../common/commonConversionWarnings";
3+
import { numberToFixedString } from "../../common/numToAutoFixed";
24
import { formatWithJSX } from "../../common/parseJSX";
35

46
/**
@@ -15,9 +17,9 @@ export const htmlOpacity = (
1517
if (node.opacity !== undefined && node.opacity !== 1) {
1618
// formatWithJSX is not called here because opacity unit doesn't end in px.
1719
if (isJsx) {
18-
return `opacity: ${sliceNum(node.opacity)}`;
20+
return `opacity: ${numberToFixedString(node.opacity)}`;
1921
} else {
20-
return `opacity: ${sliceNum(node.opacity)}`;
22+
return `opacity: ${numberToFixedString(node.opacity)}`;
2123
}
2224
}
2325
return "";
@@ -109,16 +111,38 @@ export const htmlVisibility = (
109111
* if rotation was changed, let it be perceived. Therefore, 1 => 45
110112
*/
111113
export const htmlRotation = (node: LayoutMixin, isJsx: boolean): string[] => {
112-
// that's how you convert angles to clockwise radians: angle * -pi/180
113-
// using 3.14159 as Pi for enough precision and to avoid importing math lib.
114-
if (node.rotation !== undefined && Math.round(node.rotation) !== 0) {
114+
// For some reason, a group with rotation also has rotated nodes.
115+
// - group 1 - rotation 45°
116+
// - child 1 - rotation 45°
117+
//
118+
// if the child is also rotated 45° the effect is doubled
119+
// - group 1 - rotation 45°
120+
// - child 1 - rotation 90°
121+
//
122+
// because of this, we subtract the rotation of the parent from the children.
123+
const parent =
124+
"parent" in node && node.parent ? (node.parent as LayoutMixin) : null;
125+
const parentRotation: number =
126+
parent && "rotation" in parent ? parent.rotation : 0;
127+
const rotation: number = Math.round(parentRotation - node.rotation) ?? 0;
128+
129+
if (
130+
roundToNearestHundreth(parentRotation) !== 0 &&
131+
roundToNearestHundreth(rotation) !== 0
132+
) {
133+
addWarning(
134+
"Rotated elements within rotated containers are not currently supported.",
135+
);
136+
}
137+
138+
if (rotation !== 0) {
115139
return [
116140
formatWithJSX(
117141
"transform",
118142
isJsx,
119-
`rotate(${sliceNum(-node.rotation)}deg)`,
143+
`rotate(${numberToFixedString(rotation)}deg)`,
120144
),
121-
formatWithJSX("transform-origin", isJsx, "0 0"),
145+
formatWithJSX("transform-origin", isJsx, "top left"),
122146
];
123147
}
124148
return [];

0 commit comments

Comments
 (0)