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));
+}