diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 1931964..09db15b 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -263,7 +263,8 @@ "import_external_description": "Select a filament in the list to automatically populate its details in the filament creation form. This will overwrite any data you have entered in the form.

A Manufacturer object will be created automatically when you click Ok, if necessary.", "created_success": "Created filament \"{{name}}\"", "vendor_placeholder": "Select a manufacturer", - "name_placeholder": "E.g. Matte Black" + "name_placeholder": "E.g. Matte Black", + "material_placeholder": "PLA, PETG, ABS, etc." }, "buttons": { "add_spool": "Add Spool" diff --git a/client/src/components/filamentCreateModal.tsx b/client/src/components/filamentCreateModal.tsx index 1e763ba..1d92f8e 100644 --- a/client/src/components/filamentCreateModal.tsx +++ b/client/src/components/filamentCreateModal.tsx @@ -1,11 +1,17 @@ import { useSelect } from "@refinedev/antd"; import { useInvalidate, useTranslate } from "@refinedev/core"; -import { Button, ColorPicker, Divider, Form, Input, InputNumber, Modal, Select, message } from "antd"; +import { AutoComplete, Button, ColorPicker, Divider, Form, Input, InputNumber, Modal, Select, message } from "antd"; import { PlusOutlined } from "@ant-design/icons"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { Color } from "antd/es/color-picker"; import { IFilament } from "../pages/filaments/model"; import { IVendor } from "../pages/vendors/model"; +import { + DEFAULT_DENSITY, + DEFAULT_DIAMETER, + getDensityForMaterial, + KNOWN_MATERIALS, +} from "../utils/materialDefaults"; import { getAPIURL } from "../utils/url"; import { VendorCreateModal } from "./vendorCreateModal"; @@ -15,10 +21,6 @@ export interface FilamentCreateModalProps { onClose: () => void; } -// Default values for common filament properties -const DEFAULT_DIAMETER = 1.75; -const DEFAULT_DENSITY = 1.24; // PLA - /** * Modal for quickly creating a filament inline without navigating away from the current page. * Includes essential fields and the ability to create a vendor inline. @@ -29,6 +31,23 @@ export function FilamentCreateModal(props: FilamentCreateModalProps) { const invalidate = useInvalidate(); const [isSubmitting, setIsSubmitting] = useState(false); const [isVendorModalOpen, setIsVendorModalOpen] = useState(false); + const [materialSearch, setMaterialSearch] = useState(""); + + // Material options for AutoComplete + const materialOptions = useMemo(() => { + const filtered = materialSearch + ? KNOWN_MATERIALS.filter((m) => m.toLowerCase().includes(materialSearch.toLowerCase())) + : KNOWN_MATERIALS; + return filtered.map((material) => ({ value: material, label: material })); + }, [materialSearch]); + + // Auto-fill density when material is selected or entered + const handleMaterialChange = (value: string) => { + const density = getDensityForMaterial(value); + if (density !== undefined) { + form.setFieldValue("density", density); + } + }; // Vendor select const { selectProps: vendorSelectProps } = useSelect({ @@ -171,7 +190,14 @@ export function FilamentCreateModal(props: FilamentCreateModalProps) { label={t("filament.fields.material")} name="material" > - + handleMaterialChange((e.target as HTMLInputElement).value)} + placeholder={t("filament.form.material_placeholder")} + maxLength={64} + /> ("single"); + const [materialSearch, setMaterialSearch] = useState(""); + + // Material options for AutoComplete + const materialOptions = useMemo(() => { + const filtered = materialSearch + ? KNOWN_MATERIALS.filter((m) => m.toLowerCase().includes(materialSearch.toLowerCase())) + : KNOWN_MATERIALS; + return filtered.map((material) => ({ value: material, label: material })); + }, [materialSearch]); const { form, formProps, formLoading, onFinish, redirect } = useForm< IFilament, @@ -56,6 +71,10 @@ export const FilamentCreate: React.FC { @@ -79,6 +98,14 @@ export const FilamentCreate: React.FC { + const density = getDensityForMaterial(value); + if (density !== undefined) { + form.setFieldValue("density", density); + } + }; + const importFilament = async (filament: ExternalFilament) => { const vendor = await getOrCreateVendorFromExternal(filament.manufacturer); await invalidate({ @@ -265,7 +292,14 @@ export const FilamentCreate: React.FC - + handleMaterialChange((e.target as HTMLInputElement).value)} + placeholder={t("filament.form.material_placeholder")} + maxLength={64} + /> = { + PLA: 1.24, + ABS: 1.04, + PETG: 1.27, + NYLON: 1.52, + TPU: 1.21, + PC: 1.3, + WOOD: 1.28, + CF: 1.3, + "PC/ABS": 1.19, + HIPS: 1.03, + PVA: 1.23, + ASA: 1.05, + PP: 0.9, + POM: 1.4, + PMMA: 1.18, + FPE: 2.16, +}; + +/** + * Default filament diameter in mm (most common standard). + */ +export const DEFAULT_DIAMETER = 1.75; + +/** + * Default density in g/cm³ (PLA, most common material). + */ +export const DEFAULT_DENSITY = 1.24; + +/** + * List of known materials for AutoComplete suggestions. + */ +export const KNOWN_MATERIALS = Object.keys(MATERIAL_DENSITY_MAP); + +/** + * Get the density for a material type using fuzzy matching. + * Handles variations like "PLA Silk", "PETG-CF", "Wood PLA", etc. + * + * @param material - The material string to look up + * @returns The density if a match is found, undefined otherwise + */ +export function getDensityForMaterial(material: string | undefined): number | undefined { + if (!material) return undefined; + + const normalizedInput = material.toUpperCase().trim(); + + // Exact match first + if (MATERIAL_DENSITY_MAP[normalizedInput]) { + return MATERIAL_DENSITY_MAP[normalizedInput]; + } + + // Fuzzy match: check if input starts with or contains a known material + // Sort by length descending to match longer patterns first (e.g., "PC/ABS" before "PC") + const sortedMaterials = KNOWN_MATERIALS.sort((a, b) => b.length - a.length); + + for (const knownMaterial of sortedMaterials) { + // Check if the input starts with the material (e.g., "PLA Silk" starts with "PLA") + if (normalizedInput.startsWith(knownMaterial)) { + return MATERIAL_DENSITY_MAP[knownMaterial]; + } + // Check if the input contains the material as a word boundary + // This handles cases like "Wood PLA" or "CF-PETG" + const regex = new RegExp(`\\b${knownMaterial}\\b`, "i"); + if (regex.test(normalizedInput)) { + return MATERIAL_DENSITY_MAP[knownMaterial]; + } + } + + return undefined; +} + +/** + * Filter materials for AutoComplete based on input. + * Returns materials that contain the search string (case-insensitive). + * + * @param searchValue - The search string + * @returns Filtered list of material suggestions + */ +export function filterMaterials(searchValue: string): string[] { + if (!searchValue) return KNOWN_MATERIALS; + + const normalizedSearch = searchValue.toUpperCase().trim(); + return KNOWN_MATERIALS.filter((material) => material.includes(normalizedSearch)); +}