Updated spool UIs (create/edit)

This commit is contained in:
Matt Gerega
2024-03-27 14:47:05 -04:00
parent 3aed735cea
commit 3ea4937f24
7 changed files with 349 additions and 80 deletions

View File

@@ -144,6 +144,8 @@
"measured_weight": "Measured Weight",
"used_length": "Used Length",
"remaining_length": "Remaining Length",
"initial_weight": "Initial Weight",
"empty_weight": "Empty Weight",
"location": "Location",
"lot_nr": "Lot Nr",
"first_used": "First Used",
@@ -158,6 +160,8 @@
"used_weight": "How much filament has been used from the spool. A new spool should have 0g used.",
"remaining_weight": "How much filament is left on the spool. For a new spool this should match the spool weight.",
"measured_weight": "How much the filament and spool weigh.",
"initial_weight": "The initial Total Weight of the filament and spool.",
"empty_weight": "The weight of the spool when it is empty.",
"location": "Where the spool is located if you have multiple locations where you store your spools.",
"lot_nr": "Manufacturer's lot number. Can be used to ensure a print has consistent color if multiple spools are used."
},

View File

@@ -107,9 +107,6 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
});
filamentOptions?.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
const [defaultEmptySpoolWeight, setDefaultEmptySpoolWeight] = useState(0);
const [defaultInitialTotalWeight, setDefaultInitialTotalWeight] = useState(0);
const [weightToEnter, setWeightToEnter] = useState(1);
const [usedWeight, setUsedWeight] = useState(0);
@@ -117,28 +114,26 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
const selectedFilament = filamentOptions?.find((obj) => {
return obj.value === selectedFilamentID;
});
const filamentWeight = selectedFilament?.weight || 0;
const spoolWeight = selectedFilament?.spool_weight || 0;
const filamentChange = (newID: number) => {
const newSelectedFilament = filamentOptions?.find((obj) => {
return obj.value === newID;
});
const initial_weight = form.getFieldValue("initial_weight") as number ?? 0;
const empty_weight = form.getFieldValue("empty_weight") as number ?? 0;
const newFilamentWeight = newSelectedFilament?.weight || 0;
const newSpoolWeight = newSelectedFilament?.spool_weight || 0;
setDefaultEmptySpoolWeight(newSpoolWeight);
setDefaultInitialTotalWeight(newFilamentWeight + newSpoolWeight);
if (weightToEnter >= 3) {
if (!(newFilamentWeight && newSpoolWeight)) {
setWeightToEnter(2);
}
const currentCalculatedFilamentWeight = getTotalWeightFromFilament();
if ((initial_weight === 0 || initial_weight === currentCalculatedFilamentWeight) && newFilamentWeight > 0) {
form.setFieldValue("initial_weight", newFilamentWeight + newSpoolWeight);
}
if (weightToEnter >= 2) {
if (!newFilamentWeight) {
setWeightToEnter(1);
}
if ((empty_weight === 0 || empty_weight === (selectedFilament?.spool_weight ?? 0)) && newSpoolWeight > 0) {
form.setFieldValue("empty_weight", newSpoolWeight);
}
};
@@ -166,6 +161,98 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
setQuantity(quantity - 1);
};
const getSpoolTotalWeight = (): number => {
const initial_weight = form.getFieldValue("initial_weight") as number;
const empty_weight = form.getFieldValue("empty_weight") as number;
const spool_weight = empty_weight ?? selectedFilament?.spool_weight;
return initial_weight ?? (selectedFilament?.weight ?? 0) + spool_weight;
};
const getTotalWeightFromFilament = (): number => {
return (selectedFilament?.weight ?? 0) + (selectedFilament?.spool_weight ?? 0);
}
const getFilamentWeight = (): number => {
const initial_weight = form.getFieldValue("initial_weight") as number;
const empty_weight = form.getFieldValue("empty_weight") as number;
const spool_weight = empty_weight ?? selectedFilament?.spool_weight;
if (initial_weight) {
return initial_weight - (spool_weight ?? 0);
}
return selectedFilament?.weight ?? 0;
}
const getMeasuredWeight = (): number => {
const initial_weight = form.getFieldValue("initial_weight") as number;
if (initial_weight) {
return initial_weight - usedWeight;
}
const empty_weight = form.getFieldValue("empty_weight") as number;
const spool_weight = empty_weight ?? selectedFilament?.spool_weight;
if (selectedFilament?.weight && spool_weight) {
return selectedFilament?.weight - usedWeight + spool_weight;
}
return 0;
}
const getRemainingWeight = (): number => {
const initial_weight = form.getFieldValue("initial_weight") as number ?? 0;
const empty_weight = form.getFieldValue("empty_weight") as number;
const spool_weight = empty_weight ?? selectedFilament?.spool_weight;
let remaining_weight = 0;
if (initial_weight === 0) {
remaining_weight = (selectedFilament?.weight ?? 0) - usedWeight;
}
else {
remaining_weight = initial_weight - spool_weight - usedWeight;
}
return (remaining_weight >= 0) ? remaining_weight : 0;
}
const isMeasuredWeightEnabled = (): boolean => {
if (!isRemainingWeightEnabled()) {
return false;
}
const empty_weight = form.getFieldValue("empty_weight") as number;
return (empty_weight || selectedFilament?.spool_weight) ? true : false;
}
const isRemainingWeightEnabled = (): boolean => {
const initial_weight = form.getFieldValue("initial_weight") as number;
if (initial_weight) {
return true;
}
return selectedFilament?.weight ? true : false;
}
React.useEffect(() => {
if (weightToEnter >= 3)
{
if (!isMeasuredWeightEnabled()) {
setWeightToEnter(2);
return;
}
}
if (weightToEnter >= 2)
{
if (!isRemainingWeightEnabled()) {
setWeightToEnter(1);
return;
}
}
}, [selectedFilament, weightChange])
return (
<Create
title={props.mode === "create" ? t("spool.titles.create") : t("spool.titles.clone")}
@@ -242,8 +329,8 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
/>
</Form.Item>
<Form.Item
label={t("filament.fields.price")}
help={t("filament.fields_help.price")}
label={t("spool.fields.price")}
help={t("spool.fields_help.price")}
name={["price"]}
rules={[
{
@@ -272,10 +359,7 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
},
]}
>
<InputNumber
addonAfter="g"
precision={1}
defaultValue={defaultInitialTotalWeight} />
<InputNumber addonAfter="g" precision={1}/>
</Form.Item>
<Form.Item
@@ -290,10 +374,7 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
},
]}
>
<InputNumber
addonAfter="g"
precision={1}
defaultValue={defaultEmptySpoolWeight} />
<InputNumber addonAfter="g" precision={1} />
</Form.Item>
<Form.Item hidden={true} name={["used_weight"]} initialValue={0}>
@@ -309,10 +390,10 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
value={weightToEnter}
>
<Radio.Button value={1}>{t("spool.fields.used_weight")}</Radio.Button>
<Radio.Button value={2} disabled={!filamentWeight}>
<Radio.Button value={2} disabled={!isRemainingWeightEnabled()}>
{t("spool.fields.remaining_weight")}
</Radio.Button>
<Radio.Button value={3} disabled={!(filamentWeight && spoolWeight)}>
<Radio.Button value={3} disabled={!isMeasuredWeightEnabled()}>
{t("spool.fields.measured_weight")}
</Radio.Button>
</Radio.Group>
@@ -344,9 +425,9 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
formatter={numberFormatter}
parser={numberParser}
disabled={weightToEnter != 2}
value={filamentWeight ? filamentWeight - usedWeight : 0}
value={getRemainingWeight()}
onChange={(value) => {
weightChange(filamentWeight - (value ?? 0));
weightChange(getFilamentWeight() - (value ?? 0));
}}
/>
</Form.Item>
@@ -362,9 +443,10 @@ export const SpoolCreate: React.FC<IResourceComponentsProps & CreateOrCloneProps
formatter={numberFormatter}
parser={numberParser}
disabled={weightToEnter != 3}
value={filamentWeight && spoolWeight ? filamentWeight - usedWeight + spoolWeight : 0}
value={getMeasuredWeight()}
onChange={(value) => {
weightChange(filamentWeight - ((value ?? 0) - spoolWeight));
const totalWeight = getSpoolTotalWeight();
weightChange(totalWeight - (value ?? 0));
}}
/>
</Form.Item>

View File

@@ -5,7 +5,7 @@ import { Form, Input, DatePicker, Select, InputNumber, Radio, Divider, Alert, Ty
import dayjs from "dayjs";
import TextArea from "antd/es/input/TextArea";
import { IFilament } from "../filaments/model";
import { ISpool, ISpoolParsedExtras } from "./model";
import { ISpool, ISpoolParsedExtras, WeightToEnter } from "./model";
import { numberFormatter, numberParser } from "../../utils/parsing";
import { useSpoolmanLocations } from "../../components/otherModels";
import { message } from "antd/lib";
@@ -21,12 +21,6 @@ We also need to stringify them again before sending them back to the API, which
the form's onFinish method. Form.Item's normalize should do this, but it doesn't seem to work.
*/
enum WeightToEnter {
used_weight = 1,
remaining_weight = 2,
measured_weight = 3,
}
export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
const t = useTranslate();
const [messageApi, contextHolder] = message.useMessage();
@@ -56,9 +50,6 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
formProps.initialValues = ParsedExtras(formProps.initialValues);
}
const [defaultEmptySpoolWeight, setDefaultEmptySpoolWeight] = useState(0);
const [defaultInitialTotalWeight, setDefaultInitialTotalWeight] = useState(0);
// Override the form's onFinish method to stringify the extra fields
const originalOnFinish = formProps.onFinish;
formProps.onFinish = (allValues: ISpoolParsedExtras) => {
@@ -103,38 +94,34 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
const selectedFilament = filamentOptions?.find((obj) => {
return obj.value === selectedFilamentID;
});
const filamentWeight = selectedFilament?.weight || 0;
const spoolWeight = selectedFilament?.spool_weight || 0;
const filamentChange = (newID: number) => {
const newSelectedFilament = filamentOptions?.find((obj) => {
return obj.value === newID;
});
const initial_weight = form.getFieldValue("initial_weight") as number ?? 0;
const empty_weight = form.getFieldValue("empty_weight") as number ?? 0;
const newFilamentWeight = newSelectedFilament?.weight || 0;
const newSpoolWeight = newSelectedFilament?.spool_weight || 0;
setDefaultEmptySpoolWeight(newSpoolWeight);
setDefaultInitialTotalWeight(newFilamentWeight + newSpoolWeight);
const filamentHasWeight = newSelectedFilament?.weight || 0;
const filamentHasSpoolWeight = newSelectedFilament?.spool_weight || 0;
if (weightToEnter == WeightToEnter.measured_weight) {
if (!(filamentHasWeight && filamentHasSpoolWeight)) {
setWeightToEnter(WeightToEnter.remaining_weight);
}
const currentCalculatedFilamentWeight = getTotalWeightFromFilament();
if ((initial_weight === 0 || initial_weight === currentCalculatedFilamentWeight) && newFilamentWeight > 0) {
form.setFieldValue("initial_weight", newFilamentWeight + newSpoolWeight);
}
if (weightToEnter == WeightToEnter.remaining_weight || weightToEnter == WeightToEnter.measured_weight) {
if (!filamentHasWeight) {
setWeightToEnter(WeightToEnter.used_weight);
}
if ((empty_weight === 0 || empty_weight === (selectedFilament?.spool_weight ?? 0)) && newSpoolWeight > 0) {
form.setFieldValue("empty_weight", newSpoolWeight);
}
};
const weightChange = (weight: number) => {
setUsedWeight(weight);
form.setFieldValue("used_weight", weight);
form.setFieldsValue({
used_weight: weight,
});
};
const locations = useSpoolmanLocations(true);
@@ -145,6 +132,97 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
allLocations.push(newLocation.trim());
}
const getSpoolTotalWeight = (): number => {
const initial_weight = form.getFieldValue("initial_weight") as number;
const empty_weight = form.getFieldValue("empty_weight") as number;
const spool_weight = empty_weight ?? selectedFilament?.spool_weight;
return initial_weight ?? (selectedFilament?.weight ?? 0) + spool_weight;
};
const getTotalWeightFromFilament = (): number => {
return (selectedFilament?.weight ?? 0) + (selectedFilament?.spool_weight ?? 0);
}
const getFilamentWeight = (): number => {
const initial_weight = form.getFieldValue("initial_weight") as number;
const empty_weight = form.getFieldValue("empty_weight") as number;
const spool_weight = empty_weight ?? selectedFilament?.spool_weight;
if (initial_weight) {
return initial_weight - (spool_weight ?? 0);
}
return selectedFilament?.weight ?? 0;
}
const getMeasuredWeight = (): number => {
const initial_weight = form.getFieldValue("initial_weight") as number;
if (initial_weight) {
return initial_weight - usedWeight;
}
const empty_weight = form.getFieldValue("empty_weight") as number;
const spool_weight = empty_weight ?? selectedFilament?.spool_weight;
if (selectedFilament?.weight && spool_weight) {
return selectedFilament?.weight - usedWeight + spool_weight;
}
return 0;
}
const getRemainingWeight = (): number => {
const initial_weight = form.getFieldValue("initial_weight") as number ?? 0;
const empty_weight = form.getFieldValue("empty_weight") as number;
const spool_weight = empty_weight ?? selectedFilament?.spool_weight;
let remaining_weight = 0;
if (initial_weight === 0) {
remaining_weight = (selectedFilament?.weight ?? 0) - usedWeight;
}
else {
remaining_weight = initial_weight - spool_weight - usedWeight;
}
return (remaining_weight >= 0) ? remaining_weight : 0;
}
const isMeasuredWeightEnabled = (): boolean => {
if (!isRemainingWeightEnabled()) {
return false;
}
const empty_weight = form.getFieldValue("empty_weight") as number;
return (empty_weight || selectedFilament?.spool_weight) ? true : false;
}
const isRemainingWeightEnabled = (): boolean => {
const initial_weight = form.getFieldValue("initial_weight") as number;
if (initial_weight) {
return true;
}
return selectedFilament?.weight ? true : false;
}
React.useEffect(() => {
if (weightToEnter >= WeightToEnter.measured_weight)
{
if (!isMeasuredWeightEnabled()) {
setWeightToEnter(WeightToEnter.remaining_weight);
return;
}
}
if (weightToEnter >= WeightToEnter.remaining_weight)
{
if (!isRemainingWeightEnabled()) {
setWeightToEnter(WeightToEnter.used_weight);
return;
}
}
}, [selectedFilament, weightChange])
const initialUsedWeight = formProps.initialValues?.used_weight || 0;
useEffect(() => {
if (initialUsedWeight) {
@@ -260,10 +338,7 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
},
]}
>
<InputNumber
addonAfter="g"
precision={1}
defaultValue={defaultInitialTotalWeight} />
<InputNumber addonAfter="g" precision={1}/>
</Form.Item>
<Form.Item
@@ -278,15 +353,13 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
},
]}
>
<InputNumber
addonAfter="g"
precision={1}
defaultValue={defaultEmptySpoolWeight} />
<InputNumber addonAfter="g" precision={1} />
</Form.Item>
<Form.Item hidden={true} name={["used_weight"]} initialValue={0}>
<InputNumber value={usedWeight} />
</Form.Item>
<Form.Item label={t("spool.fields.weight_to_use")} help={t("spool.fields_help.weight_to_use")}>
<Radio.Group
onChange={(value) => {
@@ -296,10 +369,10 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
value={weightToEnter}
>
<Radio.Button value={WeightToEnter.used_weight}>{t("spool.fields.used_weight")}</Radio.Button>
<Radio.Button value={WeightToEnter.remaining_weight} disabled={!filamentWeight}>
<Radio.Button value={WeightToEnter.remaining_weight} disabled={!isRemainingWeightEnabled()}>
{t("spool.fields.remaining_weight")}
</Radio.Button>
<Radio.Button value={WeightToEnter.measured_weight} disabled={!(filamentWeight && spoolWeight)}>
<Radio.Button value={WeightToEnter.measured_weight} disabled={!isMeasuredWeightEnabled()}>
{t("spool.fields.measured_weight")}
</Radio.Button>
</Radio.Group>
@@ -318,7 +391,10 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
}}
/>
</Form.Item>
<Form.Item label={t("spool.fields.remaining_weight")} help={t("spool.fields_help.remaining_weight")}>
<Form.Item
label={t("spool.fields.remaining_weight")}
help={t("spool.fields_help.remaining_weight")}
>
<InputNumber
min={0}
addonAfter="g"
@@ -326,13 +402,16 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
formatter={numberFormatter}
parser={numberParser}
disabled={weightToEnter != WeightToEnter.remaining_weight}
value={filamentWeight ? filamentWeight - usedWeight : 0}
value={getRemainingWeight()}
onChange={(value) => {
weightChange(filamentWeight - (value ?? 0));
weightChange(getFilamentWeight() - (value ?? 0));
}}
/>
</Form.Item>
<Form.Item label={t("spool.fields.measured_weight")} help={t("spool.fields_help.measured_weight")}>
<Form.Item
label={t("spool.fields.measured_weight")}
help={t("spool.fields_help.measured_weight")}
>
<InputNumber
min={0}
addonAfter="g"
@@ -340,9 +419,10 @@ export const SpoolEdit: React.FC<IResourceComponentsProps> = () => {
formatter={numberFormatter}
parser={numberParser}
disabled={weightToEnter != WeightToEnter.measured_weight}
value={filamentWeight && spoolWeight ? filamentWeight - usedWeight + spoolWeight : 0}
value={getMeasuredWeight()}
onChange={(value) => {
weightChange(filamentWeight - ((value ?? 0) - spoolWeight));
const totalWeight = getSpoolTotalWeight();
weightChange(totalWeight - (value ?? 0));
}}
/>
</Form.Item>

View File

@@ -1,5 +1,11 @@
import { IFilament } from "../filaments/model";
export enum WeightToEnter {
used_weight = 1,
remaining_weight = 2,
measured_weight = 3,
}
export interface ISpool {
id: number;
registered: string;
@@ -7,6 +13,8 @@ export interface ISpool {
last_used?: string;
filament: IFilament;
price?: number;
initial_weight?: number;
empty_weight?: number;
remaining_weight?: number;
used_weight: number;
remaining_length?: number;

View File

@@ -187,6 +187,18 @@ class Spool(BaseModel):
),
example=500.6,
)
initial_weight: Optional[float] = Field(
default=None,
ge=0,
description=("Initial weight of the filament and spool (gross weight)"),
example=1246,
)
empty_weight: Optional[float] = Field(
default=None,
ge=0,
description=("Weight of an empty spool (tare weight)."),
example=246,
)
used_weight: float = Field(ge=0, description="Consumed weight of filament from the spool in grams.", example=500.3)
remaining_length: Optional[float] = Field(
default=None,
@@ -228,7 +240,17 @@ class Spool(BaseModel):
remaining_weight: Optional[float] = None
remaining_length: Optional[float] = None
if filament.weight is not None:
if item.initial_weight is not None:
filament_spool_weight = item.filament.spool_weight if item.filament.spool_weight is not None else 0
spool_weight = item.empty_weight if item.empty_weight is not None else filament_spool_weight
remaining_weight = max(item.initial_weight - spool_weight - item.used_weight, 0)
remaining_length = length_from_weight(
weight=remaining_weight,
density=filament.density,
diameter=filament.diameter,
)
elif filament.weight is not None:
remaining_weight = max(filament.weight - item.used_weight, 0)
remaining_length = length_from_weight(
weight=remaining_weight,
@@ -249,6 +271,8 @@ class Spool(BaseModel):
last_used=item.last_used,
filament=filament,
price=item.price,
initial_weight=item.initial_weight,
empty_weight=item.empty_weight,
used_weight=item.used_weight,
used_length=used_length,
remaining_weight=remaining_weight,

View File

@@ -42,12 +42,12 @@ class SpoolParameters(BaseModel):
)
initial_weight: Optional[float] = Field(
ge=0,
description="The initial total weight of the spool.",
description="The initial total weight of the filament and spool. (gross weight)",
example=200,
)
empty_weight: Optional[float] = Field(
ge=0,
description="The weight of an empty spool.",
description="The weight of an empty spool. (tare weight)",
example=200,
)
remaining_weight: Optional[float] = Field(
@@ -85,6 +85,10 @@ class SpoolUseParameters(BaseModel):
use_weight: Optional[float] = Field(description="Filament weight to reduce by, in g.", example=5.3)
class SpoolMeasureParameters(BaseModel):
weight: float = Field(description="Current gross weight of the spool.", example=200)
@router.get(
"",
name="Find spool",
@@ -492,3 +496,23 @@ async def use( # noqa: ANN201
status_code=400,
content={"message": "Either use_weight or use_length must be specified."},
)
@router.put(
"/{spool_id}/measure",
name="Use spool filament based on the current weight measurement",
description=("Use some weight of filament from the spool. Specify the current gross weight of the spool."),
response_model_exclude_none=True,
response_model=Spool,
responses={
400: {"model": Message},
404: {"model": Message},
},
)
async def measure( # noqa: ANN201
db: Annotated[AsyncSession, Depends(get_db_session)],
spool_id: int,
body: SpoolMeasureParameters,
):
db_item = await spool.measure(db, spool_id, body.weight)
return Spool.from_db(db_item)

View File

@@ -323,6 +323,53 @@ async def use_length(db: AsyncSession, spool_id: int, length: float) -> models.S
return spool
async def measure(db: AsyncSession, spool_id: int, weight: float) -> models.Spool:
"""Record usage based on current gross weight of spool.
Increases the used_weight attribute of the spool.
Updates the first_used and last_used attributes where appropriate.
Args:
db (AsyncSession): Database session
spool_id (int): Spool ID
weight (float): Length of filament to consume, in mm
Returns:
models.Spool: Updated spool object
"""
spool_result = await db.execute(
sqlalchemy.select(models.Spool.initial_weight, models.Spool.used_weight).where(models.Spool.id == spool_id),
)
try:
spool_info = spool_result.one()
except NoResultFound as exc:
raise ItemNotFoundError("Spool not found.") from exc
initial_weight = spool_info[0]
if initial_weight is None:
# Get filament weight and spool_weight
result = await db.execute(
sqlalchemy.select(models.Filament.weight, models.Filament.spool_weight)
.join(models.Spool, models.Spool.filament_id == models.Filament.id)
.where(models.Spool.id == spool_id),
)
try:
filament_info = result.one()
except NoResultFound as exc:
raise ItemNotFoundError("Filament not found for spool.") from exc
initial_weight = filament_info[0] + filament_info[1]
# Calculate the current gross weight (initial_weight - used_weight)
current_use = initial_weight - spool_info[1]
# Calculate the weight used since last measure
weight_to_use = current_use - weight
return await use_weight(db, spool_id, weight_to_use)
async def find_locations(
*,
db: AsyncSession,