mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-19 22:19:36 +02:00
Merge pull request #89 from EternityForest/generic-calc
WIP: Automatic generation of simple calculators from equations
This commit is contained in:
21
package-lock.json
generated
21
package-lock.json
generated
@@ -19,6 +19,7 @@
|
||||
"@mui/material": "^5.15.20",
|
||||
"@playwright/test": "^1.45.0",
|
||||
"@types/ffmpeg": "^1.0.7",
|
||||
"@types/js-quantities": "^1.6.6",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/morsee": "^1.0.2",
|
||||
"@types/omggif": "^1.0.5",
|
||||
@@ -27,10 +28,12 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"formik": "^2.4.6",
|
||||
"jimp": "^0.22.12",
|
||||
"js-quantities": "^1.8.0",
|
||||
"lint-staged": "^15.4.3",
|
||||
"lodash": "^4.17.21",
|
||||
"mime": "^4.0.6",
|
||||
"morsee": "^1.0.9",
|
||||
"nerdamer-prime": "^1.2.4",
|
||||
"notistack": "^3.0.1",
|
||||
"omggif": "^1.0.10",
|
||||
"pdf-lib": "^1.17.1",
|
||||
@@ -3113,6 +3116,11 @@
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/js-quantities": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-quantities/-/js-quantities-1.6.6.tgz",
|
||||
"integrity": "sha512-k2Q8/Avj4Oz50flfTnfVGnUCkt7OYQ3U+lfQVELE/x5mdbwChZ7fM0wpUpVWzbuSL8kIYt9ZsFQ0RFNBv8T3Qw=="
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
@@ -6989,6 +6997,11 @@
|
||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/js-quantities": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/js-quantities/-/js-quantities-1.8.0.tgz",
|
||||
"integrity": "sha512-swDw9RJpXACAWR16vAKoSojAsP6NI7cZjjnjKqhOyZSdybRUdmPr071foD3fejUKSU2JMHz99hflWkRWvfLTpQ=="
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -7840,6 +7853,14 @@
|
||||
"is-buffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/nerdamer-prime": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/nerdamer-prime/-/nerdamer-prime-1.2.4.tgz",
|
||||
"integrity": "sha512-oaGnI7GUTj3T2k2PkeGf/694uLY9pYwFSOkn/5aTuz1RXdJeD6hN0+2csKagB65H6R8J1Zb7UlPq7zEzQ2dumw==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
|
@@ -36,6 +36,7 @@
|
||||
"@mui/material": "^5.15.20",
|
||||
"@playwright/test": "^1.45.0",
|
||||
"@types/ffmpeg": "^1.0.7",
|
||||
"@types/js-quantities": "^1.6.6",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/morsee": "^1.0.2",
|
||||
"@types/omggif": "^1.0.5",
|
||||
@@ -44,10 +45,12 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"formik": "^2.4.6",
|
||||
"jimp": "^0.22.12",
|
||||
"js-quantities": "^1.8.0",
|
||||
"lint-staged": "^15.4.3",
|
||||
"lodash": "^4.17.21",
|
||||
"mime": "^4.0.6",
|
||||
"morsee": "^1.0.9",
|
||||
"nerdamer-prime": "^1.2.4",
|
||||
"notistack": "^3.0.1",
|
||||
"omggif": "^1.0.10",
|
||||
"pdf-lib": "^1.17.1",
|
||||
|
@@ -40,7 +40,7 @@ const FormikListenerComponent = <T,>({
|
||||
|
||||
interface ToolContentProps<T, I> extends ToolComponentProps {
|
||||
inputComponent?: ReactNode;
|
||||
resultComponent: ReactNode;
|
||||
resultComponent?: ReactNode;
|
||||
renderCustomInput?: (
|
||||
values: T,
|
||||
setFieldValue: (fieldName: string, value: any) => void
|
||||
@@ -57,6 +57,7 @@ interface ToolContentProps<T, I> extends ToolComponentProps {
|
||||
setInput?: React.Dispatch<React.SetStateAction<I>>;
|
||||
validationSchema?: any;
|
||||
onValuesChange?: (values: T) => void;
|
||||
verticalGroups?: boolean;
|
||||
}
|
||||
|
||||
export default function ToolContent<T extends FormikValues, I>({
|
||||
@@ -72,7 +73,8 @@ export default function ToolContent<T extends FormikValues, I>({
|
||||
setInput,
|
||||
validationSchema,
|
||||
renderCustomInput,
|
||||
onValuesChange
|
||||
onValuesChange,
|
||||
verticalGroups
|
||||
}: ToolContentProps<T, I>) {
|
||||
return (
|
||||
<Box>
|
||||
@@ -97,7 +99,7 @@ export default function ToolContent<T extends FormikValues, I>({
|
||||
input={input}
|
||||
onValuesChange={onValuesChange}
|
||||
/>
|
||||
<ToolOptions getGroups={getGroups} />
|
||||
<ToolOptions getGroups={getGroups} vertical={verticalGroups} />
|
||||
|
||||
{toolInfo && toolInfo.title && toolInfo.description && (
|
||||
<ToolInfo
|
||||
|
@@ -6,18 +6,20 @@ export default function ToolInputAndResult({
|
||||
result
|
||||
}: {
|
||||
input?: ReactNode;
|
||||
result: ReactNode;
|
||||
result?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Grid id="tool" container spacing={2}>
|
||||
{input && (
|
||||
<Grid item xs={12} md={6}>
|
||||
{input}
|
||||
if (input || result) {
|
||||
return (
|
||||
<Grid id="tool" container spacing={2}>
|
||||
{input && (
|
||||
<Grid item xs={12} md={6}>
|
||||
{input}
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12} md={input ? 6 : 12}>
|
||||
{result}
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12} md={input ? 6 : 12}>
|
||||
{result}
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
178
src/components/input/NumericInputWithUnit.tsx
Normal file
178
src/components/input/NumericInputWithUnit.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Grid, Select, MenuItem } from '@mui/material';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import Qty from 'js-quantities';
|
||||
//
|
||||
|
||||
const siPrefixes: { [key: string]: number } = {
|
||||
'Default prefix': 1,
|
||||
k: 1000,
|
||||
M: 1000000,
|
||||
G: 1000000000,
|
||||
T: 1000000000000,
|
||||
m: 0.001,
|
||||
u: 0.000001,
|
||||
n: 0.000000001,
|
||||
p: 0.000000000001
|
||||
};
|
||||
|
||||
export default function NumericInputWithUnit(props: {
|
||||
value: { value: number; unit: string };
|
||||
disabled?: boolean;
|
||||
disableChangingUnit?: boolean;
|
||||
onOwnChange?: (value: { value: number; unit: string }) => void;
|
||||
defaultPrefix?: string;
|
||||
}) {
|
||||
const [inputValue, setInputValue] = useState(props.value.value);
|
||||
const [prefix, setPrefix] = useState(props.defaultPrefix || 'Default prefix');
|
||||
|
||||
// internal display unit
|
||||
const [unit, setUnit] = useState('');
|
||||
|
||||
// Whether user has overridden the unit
|
||||
const [userSelectedUnit, setUserSelectedUnit] = useState(false);
|
||||
const [unitKind, setUnitKind] = useState('');
|
||||
const [unitOptions, setUnitOptions] = useState<string[]>([]);
|
||||
|
||||
const [disabled, setDisabled] = useState(props.disabled);
|
||||
const [disableChangingUnit, setDisableChangingUnit] = useState(
|
||||
props.disableChangingUnit
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setDisabled(props.disabled);
|
||||
setDisableChangingUnit(props.disableChangingUnit);
|
||||
}, [props.disabled, props.disableChangingUnit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (unitKind != Qty(props.value.unit).kind()) {
|
||||
// Update the options for what units similar to this one are available
|
||||
const kind = Qty(props.value.unit).kind();
|
||||
let units: string[] = [];
|
||||
if (kind) {
|
||||
units = Qty.getUnits(kind);
|
||||
}
|
||||
|
||||
if (!units.includes(props.value.unit)) {
|
||||
units.push(props.value.unit);
|
||||
}
|
||||
|
||||
// Workaround because the lib doesn't list them
|
||||
if (kind == 'area') {
|
||||
units.push('km^2');
|
||||
units.push('mile^2');
|
||||
units.push('inch^2');
|
||||
units.push('m^2');
|
||||
units.push('cm^2');
|
||||
}
|
||||
setUnitOptions(units);
|
||||
setInputValue(props.value.value);
|
||||
setUnit(props.value.unit);
|
||||
setUnitKind(kind);
|
||||
setUserSelectedUnit(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (userSelectedUnit) {
|
||||
if (!isNaN(props.value.value)) {
|
||||
const converted = Qty(props.value.value, props.value.unit).to(
|
||||
unit
|
||||
).scalar;
|
||||
setInputValue(converted);
|
||||
} else {
|
||||
setInputValue(props.value.value);
|
||||
}
|
||||
} else {
|
||||
setInputValue(props.value.value);
|
||||
setUnit(props.value.unit);
|
||||
}
|
||||
}, [props.value.value, props.value.unit, unit]);
|
||||
|
||||
const handleUserValueChange = (newValue: number) => {
|
||||
setInputValue(newValue);
|
||||
|
||||
if (props.onOwnChange) {
|
||||
try {
|
||||
const converted = Qty(newValue * siPrefixes[prefix], unit).to(
|
||||
props.value.unit
|
||||
).scalar;
|
||||
|
||||
props.onOwnChange({ unit: props.value.unit, value: converted });
|
||||
} catch (error) {
|
||||
console.error('Conversion error', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrefixChange = (newPrefix: string) => {
|
||||
setPrefix(newPrefix);
|
||||
};
|
||||
|
||||
const handleUserUnitChange = (newUnit: string) => {
|
||||
if (!newUnit) return;
|
||||
const oldInputValue = inputValue;
|
||||
const oldUnit = unit;
|
||||
setUnit(newUnit);
|
||||
setPrefix('Default prefix');
|
||||
|
||||
const convertedValue = Qty(oldInputValue * siPrefixes[prefix], oldUnit).to(
|
||||
newUnit
|
||||
).scalar;
|
||||
setInputValue(convertedValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item xs={12} md={4}>
|
||||
<TextFieldWithDesc
|
||||
disabled={disabled}
|
||||
type="number"
|
||||
fullWidth
|
||||
sx={{ width: { xs: '75%', sm: '80%', md: '90%' } }}
|
||||
value={(inputValue / siPrefixes[prefix])
|
||||
.toFixed(9)
|
||||
.replace(/(\d*\.\d+?)0+$/, '$1')}
|
||||
onOwnChange={(value) => handleUserValueChange(parseFloat(value))}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={3}>
|
||||
<Select
|
||||
fullWidth
|
||||
disabled={disableChangingUnit}
|
||||
value={prefix}
|
||||
sx={{ width: { xs: '75%', sm: '80%', md: '90%' } }}
|
||||
onChange={(evt) => {
|
||||
handlePrefixChange(evt.target.value || '');
|
||||
}}
|
||||
>
|
||||
{Object.keys(siPrefixes).map((key) => (
|
||||
<MenuItem key={key} value={key}>
|
||||
{key}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={5}>
|
||||
<Select
|
||||
fullWidth
|
||||
disabled={disableChangingUnit}
|
||||
placeholder={'Unit'}
|
||||
sx={{ width: { xs: '75%', sm: '80%', md: '90%' } }}
|
||||
value={unit}
|
||||
onChange={(event) => {
|
||||
setUserSelectedUnit(true);
|
||||
handleUserUnitChange(event.target.value || '');
|
||||
}}
|
||||
>
|
||||
{unitOptions.map((key) => (
|
||||
<MenuItem key={key} value={key}>
|
||||
{key}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
@@ -13,10 +13,12 @@ export type GetGroupsType<T> = (
|
||||
|
||||
export default function ToolOptions<T extends FormikValues>({
|
||||
children,
|
||||
getGroups
|
||||
getGroups,
|
||||
vertical
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
getGroups: GetGroupsType<T> | null;
|
||||
vertical?: boolean;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const formikContext = useFormikContext<T>();
|
||||
@@ -49,6 +51,7 @@ export default function ToolOptions<T extends FormikValues>({
|
||||
<Stack direction={'row'} spacing={2}>
|
||||
<ToolOptionGroups
|
||||
groups={getGroups({ ...formikContext, updateField }) ?? []}
|
||||
vertical={vertical}
|
||||
/>
|
||||
{children}
|
||||
</Stack>
|
||||
|
18
src/datatables/data/material_electrical_properties.ts
Normal file
18
src/datatables/data/material_electrical_properties.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export default {
|
||||
title: 'Material Electrical Properties',
|
||||
columns: {
|
||||
resistivity_20c: {
|
||||
title: 'Resistivity at 20°C',
|
||||
type: 'number',
|
||||
unit: 'Ω/m'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
Copper: {
|
||||
resistivity_20c: 1.68e-8
|
||||
},
|
||||
Aluminum: {
|
||||
resistivity_20c: 2.82e-8
|
||||
}
|
||||
}
|
||||
};
|
73
src/datatables/data/wire_gauge.ts
Normal file
73
src/datatables/data/wire_gauge.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { DataTable } from '../types';
|
||||
|
||||
const data: DataTable = {
|
||||
title: 'American Wire Gauge',
|
||||
columns: {
|
||||
diameter: {
|
||||
title: 'Diameter',
|
||||
type: 'number',
|
||||
unit: 'mm'
|
||||
},
|
||||
area: {
|
||||
title: 'Area',
|
||||
type: 'number',
|
||||
unit: 'mm2'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
'0000 AWG': { diameter: 11.684 },
|
||||
'000 AWG': { diameter: 10.405 },
|
||||
'00 AWG': { diameter: 9.266 },
|
||||
'0 AWG': { diameter: 8.251 },
|
||||
'(4/0) AWG': { diameter: 11.684 },
|
||||
'(3/0) AWG': { diameter: 10.405 },
|
||||
'(2/0) AWG': { diameter: 9.266 },
|
||||
'(1/0) AWG': { diameter: 8.251 },
|
||||
'1 AWG': { diameter: 7.348 },
|
||||
'2 AWG': { diameter: 6.544 },
|
||||
'3 AWG': { diameter: 5.827 },
|
||||
'4 AWG': { diameter: 5.189 },
|
||||
'5 AWG': { diameter: 4.621 },
|
||||
'6 AWG': { diameter: 4.115 },
|
||||
'7 AWG': { diameter: 3.665 },
|
||||
'8 AWG': { diameter: 3.264 },
|
||||
'9 AWG': { diameter: 2.906 },
|
||||
'10 AWG': { diameter: 2.588 },
|
||||
'11 AWG': { diameter: 2.305 },
|
||||
'12 AWG': { diameter: 2.053 },
|
||||
'13 AWG': { diameter: 1.828 },
|
||||
'14 AWG': { diameter: 1.628 },
|
||||
'15 AWG': { diameter: 1.45 },
|
||||
'16 AWG': { diameter: 1.291 },
|
||||
'17 AWG': { diameter: 1.15 },
|
||||
'18 AWG': { diameter: 1.024 },
|
||||
'19 AWG': { diameter: 0.912 },
|
||||
'20 AWG': { diameter: 0.812 },
|
||||
'21 AWG': { diameter: 0.723 },
|
||||
'22 AWG': { diameter: 0.644 },
|
||||
'23 AWG': { diameter: 0.573 },
|
||||
'24 AWG': { diameter: 0.511 },
|
||||
'25 AWG': { diameter: 0.455 },
|
||||
'26 AWG': { diameter: 0.405 },
|
||||
'27 AWG': { diameter: 0.361 },
|
||||
'28 AWG': { diameter: 0.321 },
|
||||
'29 AWG': { diameter: 0.286 },
|
||||
'30 AWG': { diameter: 0.255 },
|
||||
'31 AWG': { diameter: 0.227 },
|
||||
'32 AWG': { diameter: 0.202 },
|
||||
'33 AWG': { diameter: 0.18 },
|
||||
'34 AWG': { diameter: 0.16 },
|
||||
'35 AWG': { diameter: 0.143 },
|
||||
'36 AWG': { diameter: 0.127 },
|
||||
'37 AWG': { diameter: 0.113 },
|
||||
'38 AWG': { diameter: 0.101 },
|
||||
'39 AWG': { diameter: 0.0897 },
|
||||
'40 AWG': { diameter: 0.0799 }
|
||||
}
|
||||
};
|
||||
|
||||
for (const key in data.data) {
|
||||
data.data[key].area = Math.PI * (data.data[key].diameter / 2) ** 2;
|
||||
}
|
||||
|
||||
export default data;
|
8
src/datatables/index.ts
Normal file
8
src/datatables/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { DataTable } from './types.ts';
|
||||
|
||||
/* Used in case later we want any kind of computed extra data */
|
||||
export function dataTableLookup(table: DataTable, key: string): any {
|
||||
return table.data[key];
|
||||
}
|
||||
|
||||
export { DataTable };
|
17
src/datatables/types.ts
Normal file
17
src/datatables/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
Represents a set of rows indexed by a key.
|
||||
Used for calculator presets
|
||||
|
||||
*/
|
||||
export interface DataTable {
|
||||
title: string;
|
||||
/* A JSON schema properties */
|
||||
columns: {
|
||||
[key: string]: { title: string; type: string; unit: string };
|
||||
};
|
||||
data: {
|
||||
[key: string]: {
|
||||
[key: string]: any;
|
||||
};
|
||||
};
|
||||
}
|
6
src/pages/tools/number/generic-calc/data/index.ts
Normal file
6
src/pages/tools/number/generic-calc/data/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import ohmslaw from './ohmsLaw';
|
||||
import voltageDropInWire from './voltageDropInWire';
|
||||
import sphereArea from './sphereArea';
|
||||
import sphereVolume from './sphereVolume';
|
||||
|
||||
export default [ohmslaw, voltageDropInWire, sphereArea, sphereVolume];
|
46
src/pages/tools/number/generic-calc/data/ohmsLaw.ts
Normal file
46
src/pages/tools/number/generic-calc/data/ohmsLaw.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { GenericCalcType } from './types';
|
||||
|
||||
const ohmsLawCalc: GenericCalcType = {
|
||||
icon: 'mdi:ohm',
|
||||
keywords: [
|
||||
'ohm',
|
||||
'voltage',
|
||||
'current',
|
||||
'resistance',
|
||||
'electrical',
|
||||
'circuit',
|
||||
'electronics',
|
||||
'power',
|
||||
'V=IR'
|
||||
],
|
||||
shortDescription:
|
||||
"Calculate voltage, current, or resistance in electrical circuits using Ohm's Law",
|
||||
name: "Ohm's Law",
|
||||
path: 'ohms-law',
|
||||
description: 'Calculates voltage, current and resistance',
|
||||
longDescription:
|
||||
"This calculator applies Ohm's Law (V = I × R) to determine any of the three electrical parameters when the other two are known. Ohm's Law is a fundamental principle in electrical engineering that describes the relationship between voltage (V), current (I), and resistance (R). This tool is essential for electronics hobbyists, electrical engineers, and students working with circuits to quickly solve for unknown values in their electrical designs.",
|
||||
formula: 'V = I * R',
|
||||
presets: [],
|
||||
variables: [
|
||||
{
|
||||
name: 'V',
|
||||
title: 'Voltage',
|
||||
unit: 'volt',
|
||||
default: 5
|
||||
},
|
||||
{
|
||||
name: 'I',
|
||||
title: 'Current',
|
||||
unit: 'ampere',
|
||||
default: 1
|
||||
},
|
||||
{
|
||||
name: 'R',
|
||||
title: 'Resistance',
|
||||
unit: 'ohm'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default ohmsLawCalc;
|
41
src/pages/tools/number/generic-calc/data/sphereArea.ts
Normal file
41
src/pages/tools/number/generic-calc/data/sphereArea.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { GenericCalcType } from './types';
|
||||
|
||||
const areaSphere: GenericCalcType = {
|
||||
icon: 'ph:sphere-duotone',
|
||||
keywords: [
|
||||
'sphere',
|
||||
'area',
|
||||
'surface area',
|
||||
'geometry',
|
||||
'mathematics',
|
||||
'radius',
|
||||
'calculation',
|
||||
'3D',
|
||||
'shape'
|
||||
],
|
||||
shortDescription:
|
||||
'Calculate the surface area of a sphere based on its radius',
|
||||
name: 'Area of a Sphere',
|
||||
path: 'area-sphere',
|
||||
description: 'Area of a Sphere',
|
||||
longDescription:
|
||||
'This calculator determines the surface area of a sphere using the formula A = 4πr². You can either input the radius to find the surface area or enter the surface area to calculate the required radius. This tool is useful for students studying geometry, engineers working with spherical objects, and anyone needing to perform calculations involving spherical surfaces.',
|
||||
formula: 'A = 4 * pi * r**2',
|
||||
presets: [],
|
||||
variables: [
|
||||
{
|
||||
name: 'A',
|
||||
title: 'Area',
|
||||
unit: 'mm2'
|
||||
},
|
||||
{
|
||||
name: 'r',
|
||||
title: 'Radius',
|
||||
formula: 'r = sqrt(A/pi) / 2',
|
||||
unit: 'mm',
|
||||
default: 1
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default areaSphere;
|
47
src/pages/tools/number/generic-calc/data/sphereVolume.ts
Normal file
47
src/pages/tools/number/generic-calc/data/sphereVolume.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { GenericCalcType } from './types';
|
||||
|
||||
const volumeSphere: GenericCalcType = {
|
||||
icon: 'gravity-ui:sphere',
|
||||
keywords: [
|
||||
'sphere',
|
||||
'volume',
|
||||
'geometry',
|
||||
'mathematics',
|
||||
'radius',
|
||||
'diameter',
|
||||
'calculation',
|
||||
'3D',
|
||||
'shape',
|
||||
'capacity'
|
||||
],
|
||||
shortDescription: 'Calculate the volume of a sphere using radius or diameter',
|
||||
name: 'Volume of a Sphere',
|
||||
path: 'volume-sphere',
|
||||
description: 'Volume of a Sphere',
|
||||
longDescription:
|
||||
'This calculator computes the volume of a sphere using the formula V = (4/3)πr³. You can input either the radius or diameter to find the volume, or enter the volume to determine the required radius. The tool is valuable for students, engineers, and professionals working with spherical objects in fields such as physics, engineering, and manufacturing.',
|
||||
formula: 'v = (4/3) * pi * r**3',
|
||||
presets: [],
|
||||
variables: [
|
||||
{
|
||||
name: 'v',
|
||||
title: 'Volume',
|
||||
unit: 'mm3'
|
||||
},
|
||||
{
|
||||
name: 'r',
|
||||
title: 'Radius',
|
||||
unit: 'mm',
|
||||
default: 1,
|
||||
alternates: [
|
||||
{
|
||||
title: 'Diameter',
|
||||
formula: 'x = 2 * v',
|
||||
unit: 'mm'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default volumeSphere;
|
49
src/pages/tools/number/generic-calc/data/types.ts
Normal file
49
src/pages/tools/number/generic-calc/data/types.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { DataTable } from '../../../../../datatables';
|
||||
import { ToolMeta } from '@tools/defineTool';
|
||||
|
||||
export interface AlternativeVarInfo {
|
||||
title: string;
|
||||
unit: string;
|
||||
defaultPrefix?: string;
|
||||
formula: string;
|
||||
}
|
||||
|
||||
export interface GenericCalcType extends Omit<ToolMeta, 'component'> {
|
||||
formula: string;
|
||||
extraOutputs?: {
|
||||
title: string;
|
||||
formula: string;
|
||||
unit: string;
|
||||
// Si prefix default
|
||||
defaultPrefix?: string;
|
||||
}[];
|
||||
presets?: {
|
||||
title: string;
|
||||
source: DataTable;
|
||||
default: string;
|
||||
bind: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}[];
|
||||
variables: {
|
||||
name: string;
|
||||
title: string;
|
||||
unit: string;
|
||||
defaultPrefix?: string;
|
||||
// If absence, assume it's the default target var
|
||||
default?: number;
|
||||
|
||||
// If present and false, don't allow user to select this as output
|
||||
solvable?: boolean;
|
||||
|
||||
// Alternate rearrangement of the formula, to be used when calculating this.
|
||||
// If missing, the main formula is used with auto derivation.
|
||||
formula?: string;
|
||||
|
||||
// Alternates are alternate ways of entering the exact same thing,
|
||||
// like the diameter or radius. The formula for an alternate
|
||||
// can use only one variable, always called v, which is the main
|
||||
// variable it's an alternate of
|
||||
alternates?: AlternativeVarInfo[];
|
||||
}[];
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
import type { GenericCalcType } from './types';
|
||||
import material_electrical_properties from '../../../../../datatables/data/material_electrical_properties';
|
||||
import wire_gauge from '../../../../../datatables/data/wire_gauge';
|
||||
|
||||
const voltageDropInWire: GenericCalcType = {
|
||||
icon: 'simple-icons:wire',
|
||||
keywords: [
|
||||
'voltage drop',
|
||||
'cable',
|
||||
'wire',
|
||||
'electrical',
|
||||
'resistance',
|
||||
'power loss',
|
||||
'conductor',
|
||||
'resistivity',
|
||||
'AWG',
|
||||
'gauge'
|
||||
],
|
||||
shortDescription:
|
||||
'Calculate voltage drop and power loss in electrical cables based on length, material, and current',
|
||||
name: 'Round trip voltage drop in cable',
|
||||
path: 'cable-voltage-drop',
|
||||
formula: 'x = (((p * L) / (A/10**6) ) *2) * I',
|
||||
description:
|
||||
'Calculates round trip voltage and power loss in a 2 conductor cable',
|
||||
longDescription:
|
||||
'This calculator helps determine the voltage drop and power loss in a two-conductor electrical cable. It takes into account the cable length, wire gauge (cross-sectional area), material resistivity, and current flow. The tool calculates the round-trip voltage drop, total resistance of the cable, and the power dissipated as heat. This is particularly useful for electrical engineers, electricians, and hobbyists when designing electrical systems to ensure voltage levels remain within acceptable limits at the load.',
|
||||
presets: [
|
||||
{
|
||||
title: 'Material',
|
||||
source: material_electrical_properties,
|
||||
default: 'Copper',
|
||||
bind: {
|
||||
p: 'resistivity_20c'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Wire Gauge',
|
||||
source: wire_gauge,
|
||||
default: '24 AWG',
|
||||
bind: {
|
||||
A: 'area'
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
extraOutputs: [
|
||||
{
|
||||
title: 'Total Resistance',
|
||||
formula: '((p * L) / (A/10**6))*2',
|
||||
unit: 'Ω'
|
||||
},
|
||||
{
|
||||
title: 'Total Power Dissipated',
|
||||
formula: 'I**2 * (((p * L) / (A/10**6))*2)',
|
||||
unit: 'W'
|
||||
}
|
||||
],
|
||||
variables: [
|
||||
{
|
||||
name: 'L',
|
||||
title: 'Length',
|
||||
unit: 'meter',
|
||||
default: 1
|
||||
},
|
||||
{
|
||||
name: 'A',
|
||||
title: 'Wire Area',
|
||||
unit: 'mm2',
|
||||
default: 1
|
||||
},
|
||||
|
||||
{
|
||||
name: 'I',
|
||||
title: 'Current',
|
||||
unit: 'A',
|
||||
default: 1
|
||||
},
|
||||
{
|
||||
name: 'p',
|
||||
title: 'Resistivity',
|
||||
unit: 'Ω/m3',
|
||||
default: 1,
|
||||
defaultPrefix: 'n'
|
||||
},
|
||||
{
|
||||
name: 'x',
|
||||
title: 'Voltage Drop',
|
||||
unit: 'V'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default voltageDropInWire;
|
@@ -0,0 +1,6 @@
|
||||
import { expect, describe, it } from 'vitest';
|
||||
// import { main } from './service';
|
||||
//
|
||||
// describe('generic-calc', () => {
|
||||
//
|
||||
// })
|
586
src/pages/tools/number/generic-calc/index.tsx
Normal file
586
src/pages/tools/number/generic-calc/index.tsx
Normal file
@@ -0,0 +1,586 @@
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
MenuItem,
|
||||
Radio,
|
||||
Select,
|
||||
Stack,
|
||||
TextField,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import NumericInputWithUnit from '@components/input/NumericInputWithUnit';
|
||||
import { UpdateField } from '@components/options/ToolOptions';
|
||||
import { InitialValuesType } from './types';
|
||||
import type { AlternativeVarInfo, GenericCalcType } from './data/types';
|
||||
import { dataTableLookup } from 'datatables';
|
||||
|
||||
import nerdamer from 'nerdamer-prime';
|
||||
import 'nerdamer-prime/Algebra';
|
||||
import 'nerdamer-prime/Solve';
|
||||
import 'nerdamer-prime/Calculus';
|
||||
import Qty from 'js-quantities';
|
||||
import { CustomSnackBarContext } from 'contexts/CustomSnackBarContext';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import useMediaQuery from '@mui/material/useMediaQuery';
|
||||
|
||||
function numericSolveEquationFor(
|
||||
equation: string,
|
||||
varName: string,
|
||||
variables: { [key: string]: number }
|
||||
) {
|
||||
let expr = nerdamer(equation);
|
||||
for (const key in variables) {
|
||||
expr = expr.sub(key, variables[key].toString());
|
||||
}
|
||||
|
||||
let result: nerdamer.Expression | nerdamer.Expression[] =
|
||||
expr.solveFor(varName);
|
||||
|
||||
// Sometimes the result is an array, check for it while keeping linter happy
|
||||
if ((result as unknown as nerdamer.Expression).toDecimal === undefined) {
|
||||
result = (result as unknown as nerdamer.Expression[])[0];
|
||||
}
|
||||
|
||||
return parseFloat(
|
||||
(result as unknown as nerdamer.Expression).evaluate().toDecimal()
|
||||
);
|
||||
}
|
||||
|
||||
export default async function makeTool(
|
||||
calcData: GenericCalcType
|
||||
): Promise<React.JSXElementConstructor<ToolComponentProps>> {
|
||||
const initialValues: InitialValuesType = {
|
||||
outputVariable: '',
|
||||
vars: {},
|
||||
presets: {}
|
||||
};
|
||||
|
||||
return function GenericCalc({ title }: ToolComponentProps) {
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
const theme = useTheme();
|
||||
const lessThanSmall = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
// For UX purposes we need to track what vars are
|
||||
const [valsBoundToPreset, setValsBoundToPreset] = useState<{
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
|
||||
const [extraOutputs, setExtraOutputs] = useState<{
|
||||
[key: string]: number;
|
||||
}>({});
|
||||
|
||||
const updateVarField = (
|
||||
name: string,
|
||||
value: number,
|
||||
unit: string,
|
||||
values: InitialValuesType,
|
||||
updateFieldFunc: UpdateField<InitialValuesType>
|
||||
) => {
|
||||
// Make copy
|
||||
const newVars = { ...values.vars };
|
||||
newVars[name] = {
|
||||
value,
|
||||
unit: unit
|
||||
};
|
||||
updateFieldFunc('vars', newVars);
|
||||
};
|
||||
|
||||
const handleSelectedTargetChange = (
|
||||
varName: string,
|
||||
updateFieldFunc: UpdateField<InitialValuesType>
|
||||
) => {
|
||||
updateFieldFunc('outputVariable', varName);
|
||||
};
|
||||
|
||||
const handleSelectedPresetChange = (
|
||||
selection: string,
|
||||
preset: string,
|
||||
currentValues: InitialValuesType,
|
||||
updateFieldFunc: UpdateField<InitialValuesType>
|
||||
) => {
|
||||
const newPresets = { ...currentValues.presets };
|
||||
newPresets[selection] = preset;
|
||||
updateFieldFunc('presets', newPresets);
|
||||
|
||||
// Clear old selection using setState callback pattern
|
||||
setValsBoundToPreset((prevState) => {
|
||||
const newState = { ...prevState };
|
||||
// Remove all keys bound to this selection
|
||||
Object.keys(newState).forEach((key) => {
|
||||
if (newState[key] === selection) {
|
||||
delete newState[key];
|
||||
}
|
||||
});
|
||||
return newState;
|
||||
});
|
||||
|
||||
const selectionData = calcData.presets?.find(
|
||||
(sel) => sel.title === selection
|
||||
);
|
||||
|
||||
if (preset && preset != '<custom>') {
|
||||
if (selectionData) {
|
||||
// Create an object with the new bindings
|
||||
const newBindings: { [key: string]: string } = {};
|
||||
|
||||
for (const key in selectionData.bind) {
|
||||
// Add to newBindings for later state update
|
||||
newBindings[key] = selection;
|
||||
|
||||
if (currentValues.outputVariable === key) {
|
||||
handleSelectedTargetChange('', updateFieldFunc);
|
||||
}
|
||||
|
||||
updateVarField(
|
||||
key,
|
||||
|
||||
dataTableLookup(selectionData.source, preset)[
|
||||
selectionData.bind[key]
|
||||
],
|
||||
|
||||
selectionData.source.columns[selectionData.bind[key]]?.unit || '',
|
||||
currentValues,
|
||||
updateFieldFunc
|
||||
);
|
||||
}
|
||||
|
||||
// Update state with new bindings
|
||||
setValsBoundToPreset((prevState) => ({
|
||||
...prevState,
|
||||
...newBindings
|
||||
}));
|
||||
} else {
|
||||
throw new Error(
|
||||
`Preset "${preset}" is not valid for selection "${selection}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
calcData.variables.forEach((variable) => {
|
||||
if (variable.solvable === undefined) {
|
||||
variable.solvable = true;
|
||||
}
|
||||
if (variable.default === undefined) {
|
||||
initialValues.vars[variable.name] = {
|
||||
value: NaN,
|
||||
unit: variable.unit
|
||||
};
|
||||
initialValues.outputVariable = variable.name;
|
||||
} else {
|
||||
initialValues.vars[variable.name] = {
|
||||
value: variable.default || 0,
|
||||
unit: variable.unit
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
calcData.presets?.forEach((selection) => {
|
||||
initialValues.presets[selection.title] = selection.default;
|
||||
if (selection.default == '<custom>') return;
|
||||
for (const key in selection.bind) {
|
||||
initialValues.vars[key] = {
|
||||
value: dataTableLookup(selection.source, selection.default)[
|
||||
selection.bind[key]
|
||||
],
|
||||
|
||||
unit: selection.source.columns[selection.bind[key]]?.unit || ''
|
||||
};
|
||||
// We'll set this in useEffect instead of directly modifying state
|
||||
}
|
||||
});
|
||||
|
||||
function getAlternate(
|
||||
alternateInfo: AlternativeVarInfo,
|
||||
mainInfo: GenericCalcType['variables'][number],
|
||||
mainValue: {
|
||||
value: number;
|
||||
unit: string;
|
||||
}
|
||||
) {
|
||||
if (isNaN(mainValue.value)) return NaN;
|
||||
const canonicalValue = Qty(mainValue.value, mainValue.unit).to(
|
||||
mainInfo.unit
|
||||
).scalar;
|
||||
|
||||
return numericSolveEquationFor(alternateInfo.formula, 'x', {
|
||||
v: canonicalValue
|
||||
});
|
||||
}
|
||||
|
||||
function getMainFromAlternate(
|
||||
alternateInfo: AlternativeVarInfo,
|
||||
mainInfo: GenericCalcType['variables'][number],
|
||||
alternateValue: {
|
||||
value: number;
|
||||
unit: string;
|
||||
}
|
||||
) {
|
||||
if (isNaN(alternateValue.value)) return NaN;
|
||||
const canonicalValue = Qty(alternateValue.value, alternateValue.unit).to(
|
||||
alternateInfo.unit
|
||||
).scalar;
|
||||
|
||||
return numericSolveEquationFor(alternateInfo.formula, 'v', {
|
||||
x: canonicalValue
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
inputComponent={null}
|
||||
initialValues={initialValues}
|
||||
toolInfo={{
|
||||
title: calcData.name,
|
||||
description:
|
||||
(calcData.description || '') +
|
||||
' Generated from formula: ' +
|
||||
calcData.formula
|
||||
}}
|
||||
verticalGroups
|
||||
getGroups={({ values, updateField }) => [
|
||||
...(calcData.presets?.length
|
||||
? [
|
||||
{
|
||||
title: 'Presets',
|
||||
component: (
|
||||
<Grid container spacing={2} maxWidth={500}>
|
||||
{calcData.presets?.map((preset) => (
|
||||
<Grid item xs={12} key={preset.title}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', md: 'row' }}
|
||||
spacing={2}
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
>
|
||||
<Typography>{preset.title}</Typography>
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
id="combo-box-demo"
|
||||
value={values.presets[preset.title]}
|
||||
options={[
|
||||
'<custom>',
|
||||
...Object.keys(preset.source.data).sort()
|
||||
]}
|
||||
sx={{ width: '80%' }}
|
||||
onChange={(event, newValue) => {
|
||||
handleSelectedPresetChange(
|
||||
preset.title,
|
||||
newValue || '',
|
||||
values,
|
||||
updateField
|
||||
);
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Preset" />
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: 'Variables',
|
||||
component: (
|
||||
<Box>
|
||||
{lessThanSmall ? (
|
||||
<Stack
|
||||
direction={'column'}
|
||||
spacing={2}
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
>
|
||||
<Typography>Solve for</Typography>
|
||||
<Select
|
||||
sx={{ width: '80%' }}
|
||||
fullWidth
|
||||
value={values.outputVariable}
|
||||
onChange={(event) =>
|
||||
handleSelectedTargetChange(
|
||||
event.target.value,
|
||||
updateField
|
||||
)
|
||||
}
|
||||
>
|
||||
{calcData.variables.map((variable) => (
|
||||
<MenuItem
|
||||
disabled={
|
||||
valsBoundToPreset[variable.name] !== undefined ||
|
||||
variable.solvable === false
|
||||
}
|
||||
key={variable.name}
|
||||
value={variable.name}
|
||||
>
|
||||
{variable.title}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Stack>
|
||||
) : (
|
||||
<Grid container spacing={2} sx={{ mb: 2 }}>
|
||||
<Grid item xs={10}></Grid>
|
||||
<Grid item xs={2}>
|
||||
<Typography fontWeight="bold" align="center">
|
||||
Solve For
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
{calcData.variables.map((variable) => (
|
||||
<Box
|
||||
key={variable.name}
|
||||
sx={{
|
||||
my: 3,
|
||||
p: 1,
|
||||
borderRadius: 1
|
||||
}}
|
||||
>
|
||||
<Grid container spacing={2} alignItems="center">
|
||||
<Grid item xs={lessThanSmall ? 12 : 10}>
|
||||
<Box>
|
||||
<Stack spacing={2}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', md: 'row' }}
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography sx={{ minWidth: '8%' }}>
|
||||
{variable.title}
|
||||
</Typography>
|
||||
<NumericInputWithUnit
|
||||
defaultPrefix={variable.defaultPrefix}
|
||||
value={values.vars[variable.name]}
|
||||
disabled={
|
||||
values.outputVariable === variable.name ||
|
||||
valsBoundToPreset[variable.name] !== undefined
|
||||
}
|
||||
disableChangingUnit={
|
||||
valsBoundToPreset[variable.name] !== undefined
|
||||
}
|
||||
onOwnChange={(val) =>
|
||||
updateVarField(
|
||||
variable.name,
|
||||
val.value,
|
||||
val.unit,
|
||||
values,
|
||||
updateField
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{variable.alternates?.map((alt) => (
|
||||
<Box key={alt.title}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography sx={{ minWidth: '8%' }}>
|
||||
{alt.title}
|
||||
</Typography>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<NumericInputWithUnit
|
||||
key={alt.title}
|
||||
defaultPrefix={alt.defaultPrefix || ''}
|
||||
value={{
|
||||
value:
|
||||
getAlternate(
|
||||
alt,
|
||||
variable,
|
||||
values.vars[variable.name]
|
||||
) || NaN,
|
||||
unit: alt.unit || ''
|
||||
}}
|
||||
disabled={
|
||||
values.outputVariable ===
|
||||
variable.name ||
|
||||
valsBoundToPreset[variable.name] !==
|
||||
undefined
|
||||
}
|
||||
disableChangingUnit={
|
||||
valsBoundToPreset[variable.name] !==
|
||||
undefined
|
||||
}
|
||||
onOwnChange={(val) =>
|
||||
updateVarField(
|
||||
variable.name,
|
||||
getMainFromAlternate(
|
||||
alt,
|
||||
variable,
|
||||
val
|
||||
),
|
||||
variable.unit,
|
||||
values,
|
||||
updateField
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{!lessThanSmall && (
|
||||
<Grid
|
||||
item
|
||||
xs={2}
|
||||
sx={{ display: 'flex', justifyContent: 'center' }}
|
||||
>
|
||||
<Radio
|
||||
value={variable.name}
|
||||
checked={values.outputVariable === variable.name}
|
||||
disabled={
|
||||
valsBoundToPreset[variable.name] !== undefined ||
|
||||
variable.solvable === false
|
||||
}
|
||||
onClick={() =>
|
||||
handleSelectedTargetChange(
|
||||
variable.name,
|
||||
updateField
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
...(calcData.extraOutputs
|
||||
? [
|
||||
{
|
||||
title: 'Extra outputs',
|
||||
component: (
|
||||
<Box>
|
||||
<Grid container spacing={2}>
|
||||
{calcData.extraOutputs?.map((extraOutput) => (
|
||||
<Grid item xs={12} key={extraOutput.title}>
|
||||
<Stack spacing={1}>
|
||||
<Typography>{extraOutput.title}</Typography>
|
||||
<NumericInputWithUnit
|
||||
disabled={true}
|
||||
defaultPrefix={extraOutput.defaultPrefix}
|
||||
value={{
|
||||
value: extraOutputs[extraOutput.title],
|
||||
unit: extraOutput.unit
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
compute={(values) => {
|
||||
if (values.outputVariable === '') {
|
||||
showSnackBar('Please select a solve for variable', 'error');
|
||||
return;
|
||||
}
|
||||
let expr: nerdamer.Expression | null = null;
|
||||
|
||||
for (const variable of calcData.variables) {
|
||||
if (variable.name === values.outputVariable) {
|
||||
if (variable.formula !== undefined) {
|
||||
expr = nerdamer(variable.formula);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expr == null) {
|
||||
expr = nerdamer(calcData.formula);
|
||||
}
|
||||
if (expr == null) {
|
||||
throw new Error('No formula found');
|
||||
}
|
||||
|
||||
Object.keys(values.vars).forEach((key) => {
|
||||
if (key === values.outputVariable) return;
|
||||
if (expr === null) {
|
||||
throw new Error('Math fail');
|
||||
}
|
||||
expr = expr.sub(key, values.vars[key].value.toString());
|
||||
});
|
||||
|
||||
let result: nerdamer.Expression | nerdamer.Expression[] =
|
||||
expr.solveFor(values.outputVariable);
|
||||
|
||||
// Sometimes the result is an array
|
||||
if (
|
||||
(result as unknown as nerdamer.Expression).toDecimal === undefined
|
||||
) {
|
||||
if ((result as unknown as nerdamer.Expression[])?.length < 1) {
|
||||
values.vars[values.outputVariable].value = NaN;
|
||||
if (calcData.extraOutputs !== undefined) {
|
||||
// Update extraOutputs using setState
|
||||
setExtraOutputs((prevState) => {
|
||||
const newState = { ...prevState };
|
||||
for (let i = 0; i < calcData.extraOutputs!.length; i++) {
|
||||
const extraOutput = calcData.extraOutputs![i];
|
||||
newState[extraOutput.title] = NaN;
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
throw new Error('No solution found for this input');
|
||||
}
|
||||
result = (result as unknown as nerdamer.Expression[])[0];
|
||||
}
|
||||
|
||||
if (result) {
|
||||
if (values.vars[values.outputVariable] != undefined) {
|
||||
values.vars[values.outputVariable].value = parseFloat(
|
||||
(result as unknown as nerdamer.Expression)
|
||||
.evaluate()
|
||||
.toDecimal()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
values.vars[values.outputVariable].value = NaN;
|
||||
}
|
||||
|
||||
if (calcData.extraOutputs !== undefined) {
|
||||
for (let i = 0; i < calcData.extraOutputs.length; i++) {
|
||||
const extraOutput = calcData.extraOutputs[i];
|
||||
|
||||
let expr = nerdamer(extraOutput.formula);
|
||||
|
||||
Object.keys(values.vars).forEach((key) => {
|
||||
expr = expr.sub(key, values.vars[key].value.toString());
|
||||
});
|
||||
|
||||
// todo could this have multiple solutions too?
|
||||
const result: nerdamer.Expression = expr.evaluate();
|
||||
|
||||
if (result) {
|
||||
// Update extraOutputs state properly
|
||||
setExtraOutputs((prevState) => ({
|
||||
...prevState,
|
||||
[extraOutput.title]: parseFloat(result.toDecimal())
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
32
src/pages/tools/number/generic-calc/meta.ts
Normal file
32
src/pages/tools/number/generic-calc/meta.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { DefinedTool, defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
import type { GenericCalcType } from './data/types';
|
||||
import allGenericCalcs from './data/index';
|
||||
|
||||
async function importComponent(data: GenericCalcType) {
|
||||
const x = await import('./index');
|
||||
return { default: await x.default(data) };
|
||||
}
|
||||
|
||||
const tools: DefinedTool[] = [];
|
||||
|
||||
allGenericCalcs.forEach((x) => {
|
||||
async function importComponent2() {
|
||||
return await importComponent(x);
|
||||
}
|
||||
|
||||
tools.push(
|
||||
defineTool('number', {
|
||||
name: x.name,
|
||||
path: 'generic-calc/' + x.path,
|
||||
icon: x.icon || '',
|
||||
description: x.description || '',
|
||||
shortDescription: x.description || '',
|
||||
keywords: ['calculator', 'math', ...x.keywords],
|
||||
longDescription: x.longDescription || '',
|
||||
component: lazy(importComponent2)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export { tools };
|
14
src/pages/tools/number/generic-calc/types.ts
Normal file
14
src/pages/tools/number/generic-calc/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type InitialValuesType = {
|
||||
vars: {
|
||||
[key: string]: {
|
||||
value: number;
|
||||
unit: string;
|
||||
};
|
||||
};
|
||||
|
||||
// Track preset selections
|
||||
presets: {
|
||||
[key: string]: string;
|
||||
};
|
||||
outputVariable: string;
|
||||
};
|
@@ -1,5 +1,10 @@
|
||||
import { tool as numberSum } from './sum/meta';
|
||||
import { tool as numberGenerate } from './generate/meta';
|
||||
import { tool as numberArithmeticSequence } from './arithmetic-sequence/meta';
|
||||
|
||||
export const numberTools = [numberSum, numberGenerate, numberArithmeticSequence];
|
||||
import { tools as genericCalcTools } from './generic-calc/meta';
|
||||
export const numberTools = [
|
||||
numberSum,
|
||||
numberGenerate,
|
||||
numberArithmeticSequence,
|
||||
...genericCalcTools
|
||||
];
|
||||
|
@@ -2,7 +2,7 @@ import ToolLayout from '../components/ToolLayout';
|
||||
import React, { JSXElementConstructor, LazyExoticComponent } from 'react';
|
||||
import { IconifyIcon } from '@iconify/react';
|
||||
|
||||
interface ToolOptions {
|
||||
export interface ToolMeta {
|
||||
path: string;
|
||||
component: LazyExoticComponent<JSXElementConstructor<ToolComponentProps>>;
|
||||
keywords: string[];
|
||||
@@ -44,7 +44,7 @@ export interface ToolComponentProps {
|
||||
|
||||
export const defineTool = (
|
||||
basePath: ToolCategory,
|
||||
options: ToolOptions
|
||||
options: ToolMeta
|
||||
): DefinedTool => {
|
||||
const {
|
||||
icon,
|
||||
|
Reference in New Issue
Block a user