Skip to content

Commit 1d49889

Browse files
authored
Merge pull request #89 from EternityForest/generic-calc
WIP: Automatic generation of simple calculators from equations
2 parents 7308cf0 + f0ac25c commit 1d49889

22 files changed

+1271
-19
lines changed

package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@mui/material": "^5.15.20",
3737
"@playwright/test": "^1.45.0",
3838
"@types/ffmpeg": "^1.0.7",
39+
"@types/js-quantities": "^1.6.6",
3940
"@types/lodash": "^4.17.5",
4041
"@types/morsee": "^1.0.2",
4142
"@types/omggif": "^1.0.5",
@@ -44,10 +45,12 @@
4445
"dayjs": "^1.11.13",
4546
"formik": "^2.4.6",
4647
"jimp": "^0.22.12",
48+
"js-quantities": "^1.8.0",
4749
"lint-staged": "^15.4.3",
4850
"lodash": "^4.17.21",
4951
"mime": "^4.0.6",
5052
"morsee": "^1.0.9",
53+
"nerdamer-prime": "^1.2.4",
5154
"notistack": "^3.0.1",
5255
"omggif": "^1.0.10",
5356
"pdf-lib": "^1.17.1",

src/components/ToolContent.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const FormikListenerComponent = <T,>({
4040

4141
interface ToolContentProps<T, I> extends ToolComponentProps {
4242
inputComponent?: ReactNode;
43-
resultComponent: ReactNode;
43+
resultComponent?: ReactNode;
4444
renderCustomInput?: (
4545
values: T,
4646
setFieldValue: (fieldName: string, value: any) => void
@@ -57,6 +57,7 @@ interface ToolContentProps<T, I> extends ToolComponentProps {
5757
setInput?: React.Dispatch<React.SetStateAction<I>>;
5858
validationSchema?: any;
5959
onValuesChange?: (values: T) => void;
60+
verticalGroups?: boolean;
6061
}
6162

6263
export default function ToolContent<T extends FormikValues, I>({
@@ -72,7 +73,8 @@ export default function ToolContent<T extends FormikValues, I>({
7273
setInput,
7374
validationSchema,
7475
renderCustomInput,
75-
onValuesChange
76+
onValuesChange,
77+
verticalGroups
7678
}: ToolContentProps<T, I>) {
7779
return (
7880
<Box>
@@ -97,7 +99,7 @@ export default function ToolContent<T extends FormikValues, I>({
9799
input={input}
98100
onValuesChange={onValuesChange}
99101
/>
100-
<ToolOptions getGroups={getGroups} />
102+
<ToolOptions getGroups={getGroups} vertical={verticalGroups} />
101103

102104
{toolInfo && toolInfo.title && toolInfo.description && (
103105
<ToolInfo

src/components/ToolInputAndResult.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@ export default function ToolInputAndResult({
66
result
77
}: {
88
input?: ReactNode;
9-
result: ReactNode;
9+
result?: ReactNode;
1010
}) {
11-
return (
12-
<Grid id="tool" container spacing={2}>
13-
{input && (
14-
<Grid item xs={12} md={6}>
15-
{input}
11+
if (input || result) {
12+
return (
13+
<Grid id="tool" container spacing={2}>
14+
{input && (
15+
<Grid item xs={12} md={6}>
16+
{input}
17+
</Grid>
18+
)}
19+
<Grid item xs={12} md={input ? 6 : 12}>
20+
{result}
1621
</Grid>
17-
)}
18-
<Grid item xs={12} md={input ? 6 : 12}>
19-
{result}
2022
</Grid>
21-
</Grid>
22-
);
23+
);
24+
}
2325
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { Grid, Select, MenuItem } from '@mui/material';
3+
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
4+
import Qty from 'js-quantities';
5+
//
6+
7+
const siPrefixes: { [key: string]: number } = {
8+
'Default prefix': 1,
9+
k: 1000,
10+
M: 1000000,
11+
G: 1000000000,
12+
T: 1000000000000,
13+
m: 0.001,
14+
u: 0.000001,
15+
n: 0.000000001,
16+
p: 0.000000000001
17+
};
18+
19+
export default function NumericInputWithUnit(props: {
20+
value: { value: number; unit: string };
21+
disabled?: boolean;
22+
disableChangingUnit?: boolean;
23+
onOwnChange?: (value: { value: number; unit: string }) => void;
24+
defaultPrefix?: string;
25+
}) {
26+
const [inputValue, setInputValue] = useState(props.value.value);
27+
const [prefix, setPrefix] = useState(props.defaultPrefix || 'Default prefix');
28+
29+
// internal display unit
30+
const [unit, setUnit] = useState('');
31+
32+
// Whether user has overridden the unit
33+
const [userSelectedUnit, setUserSelectedUnit] = useState(false);
34+
const [unitKind, setUnitKind] = useState('');
35+
const [unitOptions, setUnitOptions] = useState<string[]>([]);
36+
37+
const [disabled, setDisabled] = useState(props.disabled);
38+
const [disableChangingUnit, setDisableChangingUnit] = useState(
39+
props.disableChangingUnit
40+
);
41+
42+
useEffect(() => {
43+
setDisabled(props.disabled);
44+
setDisableChangingUnit(props.disableChangingUnit);
45+
}, [props.disabled, props.disableChangingUnit]);
46+
47+
useEffect(() => {
48+
if (unitKind != Qty(props.value.unit).kind()) {
49+
// Update the options for what units similar to this one are available
50+
const kind = Qty(props.value.unit).kind();
51+
let units: string[] = [];
52+
if (kind) {
53+
units = Qty.getUnits(kind);
54+
}
55+
56+
if (!units.includes(props.value.unit)) {
57+
units.push(props.value.unit);
58+
}
59+
60+
// Workaround because the lib doesn't list them
61+
if (kind == 'area') {
62+
units.push('km^2');
63+
units.push('mile^2');
64+
units.push('inch^2');
65+
units.push('m^2');
66+
units.push('cm^2');
67+
}
68+
setUnitOptions(units);
69+
setInputValue(props.value.value);
70+
setUnit(props.value.unit);
71+
setUnitKind(kind);
72+
setUserSelectedUnit(false);
73+
return;
74+
}
75+
76+
if (userSelectedUnit) {
77+
if (!isNaN(props.value.value)) {
78+
const converted = Qty(props.value.value, props.value.unit).to(
79+
unit
80+
).scalar;
81+
setInputValue(converted);
82+
} else {
83+
setInputValue(props.value.value);
84+
}
85+
} else {
86+
setInputValue(props.value.value);
87+
setUnit(props.value.unit);
88+
}
89+
}, [props.value.value, props.value.unit, unit]);
90+
91+
const handleUserValueChange = (newValue: number) => {
92+
setInputValue(newValue);
93+
94+
if (props.onOwnChange) {
95+
try {
96+
const converted = Qty(newValue * siPrefixes[prefix], unit).to(
97+
props.value.unit
98+
).scalar;
99+
100+
props.onOwnChange({ unit: props.value.unit, value: converted });
101+
} catch (error) {
102+
console.error('Conversion error', error);
103+
}
104+
}
105+
};
106+
107+
const handlePrefixChange = (newPrefix: string) => {
108+
setPrefix(newPrefix);
109+
};
110+
111+
const handleUserUnitChange = (newUnit: string) => {
112+
if (!newUnit) return;
113+
const oldInputValue = inputValue;
114+
const oldUnit = unit;
115+
setUnit(newUnit);
116+
setPrefix('Default prefix');
117+
118+
const convertedValue = Qty(oldInputValue * siPrefixes[prefix], oldUnit).to(
119+
newUnit
120+
).scalar;
121+
setInputValue(convertedValue);
122+
};
123+
124+
return (
125+
<Grid container spacing={2} alignItems="center">
126+
<Grid item xs={12} md={4}>
127+
<TextFieldWithDesc
128+
disabled={disabled}
129+
type="number"
130+
fullWidth
131+
sx={{ width: { xs: '75%', sm: '80%', md: '90%' } }}
132+
value={(inputValue / siPrefixes[prefix])
133+
.toFixed(9)
134+
.replace(/(\d*\.\d+?)0+$/, '$1')}
135+
onOwnChange={(value) => handleUserValueChange(parseFloat(value))}
136+
/>
137+
</Grid>
138+
139+
<Grid item xs={12} md={3}>
140+
<Select
141+
fullWidth
142+
disabled={disableChangingUnit}
143+
value={prefix}
144+
sx={{ width: { xs: '75%', sm: '80%', md: '90%' } }}
145+
onChange={(evt) => {
146+
handlePrefixChange(evt.target.value || '');
147+
}}
148+
>
149+
{Object.keys(siPrefixes).map((key) => (
150+
<MenuItem key={key} value={key}>
151+
{key}
152+
</MenuItem>
153+
))}
154+
</Select>
155+
</Grid>
156+
157+
<Grid item xs={12} md={5}>
158+
<Select
159+
fullWidth
160+
disabled={disableChangingUnit}
161+
placeholder={'Unit'}
162+
sx={{ width: { xs: '75%', sm: '80%', md: '90%' } }}
163+
value={unit}
164+
onChange={(event) => {
165+
setUserSelectedUnit(true);
166+
handleUserUnitChange(event.target.value || '');
167+
}}
168+
>
169+
{unitOptions.map((key) => (
170+
<MenuItem key={key} value={key}>
171+
{key}
172+
</MenuItem>
173+
))}
174+
</Select>
175+
</Grid>
176+
</Grid>
177+
);
178+
}

src/components/options/ToolOptions.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ export type GetGroupsType<T> = (
1313

1414
export default function ToolOptions<T extends FormikValues>({
1515
children,
16-
getGroups
16+
getGroups,
17+
vertical
1718
}: {
1819
children?: ReactNode;
1920
getGroups: GetGroupsType<T> | null;
21+
vertical?: boolean;
2022
}) {
2123
const theme = useTheme();
2224
const formikContext = useFormikContext<T>();
@@ -49,6 +51,7 @@ export default function ToolOptions<T extends FormikValues>({
4951
<Stack direction={'row'} spacing={2}>
5052
<ToolOptionGroups
5153
groups={getGroups({ ...formikContext, updateField }) ?? []}
54+
vertical={vertical}
5255
/>
5356
{children}
5457
</Stack>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export default {
2+
title: 'Material Electrical Properties',
3+
columns: {
4+
resistivity_20c: {
5+
title: 'Resistivity at 20°C',
6+
type: 'number',
7+
unit: 'Ω/m'
8+
}
9+
},
10+
data: {
11+
Copper: {
12+
resistivity_20c: 1.68e-8
13+
},
14+
Aluminum: {
15+
resistivity_20c: 2.82e-8
16+
}
17+
}
18+
};

0 commit comments

Comments
 (0)