From 3ea4937f24c67e67bf862089bb6e99e4794a86a6 Mon Sep 17 00:00:00 2001 From: Matt Gerega Date: Wed, 27 Mar 2024 14:47:05 -0400 Subject: [PATCH] Updated spool UIs (create/edit) --- client/public/locales/en/common.json | 4 + client/src/pages/spools/create.tsx | 148 +++++++++++++++++------ client/src/pages/spools/edit.tsx | 168 ++++++++++++++++++++------- client/src/pages/spools/model.tsx | 8 ++ spoolman/api/v1/models.py | 26 ++++- spoolman/api/v1/spool.py | 28 ++++- spoolman/database/spool.py | 47 ++++++++ 7 files changed, 349 insertions(+), 80 deletions(-) diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 16d7800..368846c 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -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." }, diff --git a/client/src/pages/spools/create.tsx b/client/src/pages/spools/create.tsx index deeaf4e..e60b71c 100644 --- a/client/src/pages/spools/create.tsx +++ b/client/src/pages/spools/create.tsx @@ -107,9 +107,6 @@ export const SpoolCreate: React.FC 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 { 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 { + 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 ( - + - + @@ -362,9 +443,10 @@ export const SpoolCreate: React.FC { - weightChange(filamentWeight - ((value ?? 0) - spoolWeight)); + const totalWeight = getSpoolTotalWeight(); + weightChange(totalWeight - (value ?? 0)); }} /> diff --git a/client/src/pages/spools/edit.tsx b/client/src/pages/spools/edit.tsx index 35b335b..e118e97 100644 --- a/client/src/pages/spools/edit.tsx +++ b/client/src/pages/spools/edit.tsx @@ -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 = () => { const t = useTranslate(); const [messageApi, contextHolder] = message.useMessage(); @@ -56,9 +50,6 @@ export const SpoolEdit: React.FC = () => { 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 = () => { 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 = () => { 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 = () => { }, ]} > - + = () => { }, ]} > - + + { @@ -296,10 +369,10 @@ export const SpoolEdit: React.FC = () => { value={weightToEnter} > {t("spool.fields.used_weight")} - + {t("spool.fields.remaining_weight")} - + {t("spool.fields.measured_weight")} @@ -318,7 +391,10 @@ export const SpoolEdit: React.FC = () => { }} /> - + = () => { 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)); }} /> - + = () => { 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)); }} /> diff --git a/client/src/pages/spools/model.tsx b/client/src/pages/spools/model.tsx index a98a49f..3e49b92 100644 --- a/client/src/pages/spools/model.tsx +++ b/client/src/pages/spools/model.tsx @@ -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; diff --git a/spoolman/api/v1/models.py b/spoolman/api/v1/models.py index 3b7f267..64d0b98 100644 --- a/spoolman/api/v1/models.py +++ b/spoolman/api/v1/models.py @@ -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, diff --git a/spoolman/api/v1/spool.py b/spoolman/api/v1/spool.py index e701fca..3ecbefd 100644 --- a/spoolman/api/v1/spool.py +++ b/spoolman/api/v1/spool.py @@ -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) diff --git a/spoolman/database/spool.py b/spoolman/database/spool.py index 42d2e5e..fec26fa 100644 --- a/spoolman/database/spool.py +++ b/spoolman/database/spool.py @@ -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,