import { describe, expect, it } from "vitest"; import { bausatzSchluessel, bauteile, clampTeil, MATERIAL_FALLBACK, MATERIAL_FARBE, materialFarbe, mischen, passtInBbox, rolleFarbe, type Teil, } from "./moebel3d.tsx"; // Registrierte 3D-Varianten (funktionsTyp trägt mehrere Bausätze). const VARIANTEN = ["wc-wandhaengend", "lavabo-aufsatz", "lavabo-doppel", "sofa-l", "dusche-eck"]; // Alle im Katalog vorkommenden funktionsTypen (data/catalog/*.json). const KATALOG_TYPEN = [ "wc", "lavabo", "dusche", "badewanne", "spiegel", "schrank", "handtuchstange", "handtuchheizung", "badmoebel", "badteppich", "sofa", "esstisch", "stuhl", "couchtisch", "beistelltisch", "regal", "sideboard", "stehleuchte", "teppich", "pflanze", "wandbild", "wandleuchte", "tvmoebel", "deko", "unterschrank", "hochschrank", "haengeschrank", "spuele", "kochfeld", "kuehlschrank", "geschirrspueler", "dunstabzug", "eckschrank", "fuellstueck", ]; // Repräsentative Katalog-Grössen (Meter), inkl. extremer Seitenverhältnisse. const MASSE: [number, number, number][] = [ [0.37, 0.54, 0.4], // WC-kompakt [2.1, 0.9, 0.85], // Sofa [0.6, 0.6, 2.0], // Hochschrank [1.2, 0.05, 0.02], // flacher Teppich [0.9, 0.9, 0.9], // Würfel ]; describe("bauteile – bbox-Treue (Norm-Ampel)", () => { it("hält jeden Bausatz vollständig innerhalb der bbox w×d×h", () => { for (const typ of KATALOG_TYPEN) { for (const [w, d, h] of MASSE) { const teile = bauteile(typ, w, d, h); expect(teile.length, `${typ} liefert Bauteile`).toBeGreaterThan(0); expect(passtInBbox(teile, w, d, h), `${typ} bleibt in bbox`).toBe(true); } } }); it("formt bekannte Typen aus mehreren Primitiven, nicht als nackte Box", () => { for (const typ of KATALOG_TYPEN) { expect(bauteile(typ, 1, 1, 1).length, `${typ} ist mehrteilig`).toBeGreaterThan(1); } }); }); describe("bauteile – Fallback", () => { it("gibt für unbekannte Typen genau die bisherige volle Box zurück", () => { const teile = bauteile("gibtsnicht", 0.8, 0.6, 1.2); expect(teile).toEqual([ { form: "box", groesse: [0.8, 1.2, 0.6], pos: [0, 0, 0], rolle: "koerper" }, ]); }); }); describe("bausatzSchluessel – Varianten-Fallback-Kette", () => { it("wählt eine registrierte Variante vor dem funktionsTyp", () => { expect(bausatzSchluessel("wc", "wc-wandhaengend")).toBe("wc-wandhaengend"); expect(bausatzSchluessel("lavabo", "lavabo-doppel")).toBe("lavabo-doppel"); }); it("fällt bei unregistrierter oder fehlender Variante auf den funktionsTyp zurück", () => { expect(bausatzSchluessel("wc", "gibtsnicht")).toBe("wc"); expect(bausatzSchluessel("wc", undefined)).toBe("wc"); expect(bausatzSchluessel("wc")).toBe("wc"); }); }); describe("Varianten-Bausätze – mehrteilig & bbox-treu", () => { it("liefert für jede Variante mehrteilige, bbox-treue Bausätze", () => { for (const variante of VARIANTEN) { for (const [w, d, h] of MASSE) { const teile = bauteile(variante, w, d, h); expect(teile.length, `${variante} ist mehrteilig`).toBeGreaterThan(1); expect(passtInBbox(teile, w, d, h), `${variante} bleibt in bbox`).toBe(true); } } }); it("lässt den Standard-Bausatz unverändert, wenn kein modell3d gesetzt ist", () => { // wc ohne Variante = bisheriger wc-Bausatz (Schlüssel bleibt funktionsTyp). expect(bausatzSchluessel("wc")).toBe("wc"); const standard = bauteile("wc", 0.4, 0.6, 0.42); const ueberVariante = bauteile(bausatzSchluessel("wc", undefined), 0.4, 0.6, 0.42); expect(ueberVariante).toEqual(standard); // Die Variante unterscheidet sich sichtbar vom Standard. expect(bauteile("wc-wandhaengend", 0.4, 0.6, 0.42)).not.toEqual(standard); }); }); describe("Neue Primitive – rundbox/lathe/torus (Volumetrie)", () => { it("erfasst rundbox/lathe/torus in passtInBbox (passende Teile liegen drin)", () => { const teile: Teil[] = [ { form: "rundbox", groesse: [0.8, 0.4, 0.6], pos: [0, 0, 0], radius: 0.1, rolle: "koerper" }, { form: "lathe", profil: [ [0, -0.2], [0.25, 0], [0.28, 0.18], ], segmente: 24, pos: [0, 0, 0], rolle: "koerper", }, { form: "torus", radius: 0.2, roehre: 0.03, achse: "y", pos: [0, 0, 0], rolle: "chrom" }, ]; expect(passtInBbox(teile, 1, 1, 1)).toBe(true); }); it("erkennt Überstand neuer Formen und clampt sie zurück in die bbox", () => { const zuGross: Teil[] = [ { form: "rundbox", groesse: [2, 2, 2], pos: [1, 1, 1], radius: 0.5, rolle: "koerper" }, { form: "lathe", profil: [ [0, -1], [0.9, 0], [0.9, 1], ], segmente: 24, pos: [0.5, 0.4, -0.6], rolle: "koerper", }, { form: "torus", radius: 0.9, roehre: 0.2, achse: "x", pos: [0.7, -0.8, 0.5], rolle: "chrom", }, ]; for (const t of zuGross) { expect(passtInBbox([t], 0.6, 0.5, 0.9), "vorher ausserhalb").toBe(false); expect(passtInBbox([clampTeil(t, 0.6, 0.5, 0.9)], 0.6, 0.5, 0.9), "nach clamp drin").toBe( true, ); } }); }); describe("Flagship-Bad-Modelle – Volumetrie & Chrom", () => { const formen = (typ: string) => new Set(bauteile(typ, 0.6, 0.5, 0.85).map((t) => t.form)); const rollen = (typ: string) => new Set(bauteile(typ, 0.6, 0.5, 0.85).map((t) => t.rolle)); it("WC nutzt gerundete Boxen und einen Chrom-Drücker (>=6 Teile)", () => { expect(bauteile("wc", 0.4, 0.6, 0.42).length).toBeGreaterThanOrEqual(6); expect(formen("wc").has("rundbox")).toBe(true); expect(rollen("wc").has("chrom")).toBe(true); }); it("Lavabo nutzt Rotationskörper (lathe) + Torus-Bogen + Chrom-Armatur", () => { expect(formen("lavabo").has("lathe")).toBe(true); expect(formen("lavabo").has("torus")).toBe(true); expect(rollen("lavabo").has("chrom")).toBe(true); }); it("Dusche hat echtes Glas, Chrom-Armaturen und einen Torus-Ablauf/Ring", () => { expect(rollen("dusche").has("glas")).toBe(true); expect(rollen("dusche").has("chrom")).toBe(true); expect(formen("dusche").has("torus")).toBe(true); }); }); describe("Flagship-Wohnen/Schlafen-Modelle – Volumetrie & Materialien", () => { const formen = (typ: string) => new Set(bauteile(typ, 1.2, 0.9, 0.85).map((t) => t.form)); const rollen = (typ: string) => new Set(bauteile(typ, 1.2, 0.9, 0.85).map((t) => t.rolle)); // Polster-/Korpus-Möbel: gerundete Kanten (rundbox) + genug Teile. const gerundet = [ "sofa", "sessel", "sideboard", "kommode", "nachttisch", "kleiderschrank", "tvmoebel", "bett", "doppelbett", "kinderbett", "stuhl", "buerostuhl", "hocker", "schreibtisch", ]; it("nutzt gerundete Boxen (rundbox) und ist klar mehrteilig (>=6 Teile)", () => { for (const typ of gerundet) { expect(formen(typ).has("rundbox"), `${typ} nutzt rundbox`).toBe(true); expect(bauteile(typ, 1.2, 0.9, 0.85).length, `${typ} >=6 Teile`).toBeGreaterThanOrEqual(6); } }); it("verbaut Chrom für Gestelle/Griffe/Füsse bei Polster- und Korpus-Möbeln", () => { for (const typ of ["sofa", "sessel", "sideboard", "kommode", "nachttisch", "stuhl", "hocker"]) { expect(rollen(typ).has("chrom"), `${typ} nutzt chrom`).toBe(true); } }); it("gibt dem TV-Möbel einen Glas-Screen und der Stehleuchte einen Rotationskörper", () => { expect(rollen("tvmoebel").has("glas"), "tvmoebel hat Glas-Screen").toBe(true); expect(formen("stehleuchte").has("lathe"), "stehleuchte hat Rotationskörper").toBe(true); expect(rollen("stehleuchte").has("glas"), "stehleuchte hat Glas-Diffusor").toBe(true); }); it("modelliert die Deko als Rotationskörper-Gefäss mit Chrom-Rand", () => { expect(formen("deko").has("lathe"), "deko ist Rotationskörper").toBe(true); expect(formen("deko").has("torus"), "deko hat Chrom-Ring").toBe(true); }); }); describe("Flagship-Küche-Modelle – Volumetrie & Materialien", () => { const formen = (typ: string) => new Set(bauteile(typ, 0.6, 0.6, 0.85).map((t) => t.form)); const rollen = (typ: string) => new Set(bauteile(typ, 0.6, 0.6, 0.85).map((t) => t.rolle)); // Korpus-/Geräte-Bauer der Küche: gerundete Kanten (rundbox) + genug Teile. const gerundet = [ "unterschrank", "hochschrank", "haengeschrank", "spuele", "kochfeld", "eckschrank", "fuellstueck", "kuehlschrank", "geschirrspueler", "backofen", "mikrowelle", "dunstabzug", "waschmaschine", "tumbler", ]; it("nutzt gerundete Boxen (rundbox) und ist klar mehrteilig (>=4 Teile)", () => { for (const typ of gerundet) { expect(formen(typ).has("rundbox"), `${typ} nutzt rundbox`).toBe(true); expect(bauteile(typ, 0.6, 0.6, 0.85).length, `${typ} >=4 Teile`).toBeGreaterThanOrEqual(4); } }); it("verbaut Chrom für Griffe/Armaturen an Schränken und Geräten", () => { for (const typ of [ "unterschrank", "haengeschrank", "spuele", "kochfeld", "eckschrank", "kuehlschrank", "geschirrspueler", "backofen", "mikrowelle", "dunstabzug", "waschmaschine", "tumbler", ]) { expect(rollen(typ).has("chrom"), `${typ} nutzt chrom`).toBe(true); } }); it("gibt Glas-Flächen an Kochfeld, Backofen, Mikrowelle und Waschgeräten", () => { for (const typ of ["kochfeld", "backofen", "mikrowelle", "waschmaschine", "tumbler"]) { expect(rollen(typ).has("glas"), `${typ} hat Glas`).toBe(true); } }); it("modelliert Spüle und Waschgeräte mit Torus (Armatur-Bogen bzw. Bullauge-Ring)", () => { for (const typ of ["spuele", "waschmaschine", "tumbler"]) { expect(formen(typ).has("torus"), `${typ} nutzt torus`).toBe(true); } }); it("bleibt für alle Küchen-Bauer vollständig in der bbox (auch extreme Masse)", () => { const masse: [number, number, number][] = [ [0.6, 0.6, 0.85], [0.15, 0.6, 0.85], // schmales Füllstück [0.6, 0.6, 2.0], // Hochschrank/Kühlschrank [0.9, 0.9, 0.9], ]; for (const typ of gerundet) { for (const [w, d, h] of masse) { expect(passtInBbox(bauteile(typ, w, d, h), w, d, h), `${typ} ${w}×${d}×${h} in bbox`).toBe( true, ); } } }); }); describe("Neue 3D-Kits – Registrierung, Volumetrie & bbox-Treue", () => { const NEUE_KITS = [ "waeschekorb", "badhocker", "abfalleimer", "midischrank", "pouf", "vitrine", "konsolentisch", "barwagen", "recamiere", "barhocker", "servierwagen", "vorratsschrank", "weinkuehlschrank", ]; // Repräsentative Masse inkl. extremer Seitenverhältnisse (schlank/hoch, breit/flach). const MASSE_NEU: [number, number, number][] = [ [0.6, 0.5, 0.85], [0.4, 0.4, 0.5], [0.3, 0.3, 1.4], // schmal & hoch [1.9, 0.6, 0.7], // breit & niedrig (Wagen/Recamiere) [0.15, 0.6, 2.0], // extrem schlank & hoch [0.9, 0.9, 0.9], ]; it("registriert jeden neuen Typ (kein Box-Fallback) und hat eine MATERIAL_FARBE", () => { for (const typ of NEUE_KITS) { // Registriert = bausatzSchluessel bleibt der Typ und liefert nicht die nackte Box. expect(bausatzSchluessel(typ)).toBe(typ); expect(typ in MATERIAL_FARBE, `${typ} in MATERIAL_FARBE`).toBe(true); expect(materialFarbe(typ), `${typ} hat gültige Farbe`).toMatch(/^#[0-9a-f]{6}$/i); } }); it("liefert je Kit genug Teile (>=5) und bleibt in jeder bbox (auch extreme Masse)", () => { for (const typ of NEUE_KITS) { for (const [w, d, h] of MASSE_NEU) { const teile = bauteile(typ, w, d, h); expect(teile.length, `${typ} genug Teile (>=5)`).toBeGreaterThanOrEqual(5); expect(passtInBbox(teile, w, d, h), `${typ} ${w}×${d}×${h} in bbox`).toBe(true); } } }); it("nutzt Volumetrie-Primitive statt einer nackten Box", () => { for (const typ of NEUE_KITS) { const teile = bauteile(typ, 0.6, 0.5, 0.85); const formen = new Set(teile.map((t) => t.form)); expect(teile.length, `${typ} ist mehrteilig`).toBeGreaterThan(1); const hatVolumetrie = formen.has("rundbox") || formen.has("lathe") || formen.has("torus") || formen.has("zylinder") || formen.has("kugel"); expect(hatVolumetrie, `${typ} nutzt Volumetrie (rundbox/lathe/torus/zyl/kugel)`).toBe(true); } }); it("verbaut passende Materialien: Glas an Vitrine/Barwagen/Weinkühler, Chrom an Wagen/Geräten", () => { const rollen = (typ: string, w = 0.6, d = 0.5, hg = 0.85) => new Set(bauteile(typ, w, d, hg).map((t) => t.rolle)); for (const typ of ["vitrine", "barwagen", "weinkuehlschrank"]) { expect(rollen(typ).has("glas"), `${typ} hat Glas`).toBe(true); } for (const typ of ["abfalleimer", "midischrank", "barwagen", "servierwagen", "barhocker"]) { expect(rollen(typ).has("chrom"), `${typ} nutzt Chrom`).toBe(true); } }); }); describe("Farbableitung – Chrom-Rolle", () => { it("leitet Chrom als hellen Metallton aus der Basis-/Ampelfarbe ab", () => { // Aus der Ampelfarbe abgeleitet → Status dominiert weiter (kein Eigenwert). expect(rolleFarbe("chrom", "#000000")).not.toBe("#000000"); expect(rolleFarbe("chrom", "#808080")).toMatch(/^#[0-9a-f]{6}$/i); }); }); describe("MATERIAL_FARBE – Möbel-Materialfarben", () => { it("liefert für jeden Katalog-Typ eine gültige Farbe (Map oder Salbei-Fallback)", () => { for (const typ of KATALOG_TYPEN) { const f = materialFarbe(typ); expect(f, `${typ} hat eine Farbe`).toMatch(/^#[0-9a-f]{6}$/i); // Nicht kartierte Typen müssen exakt auf Salbei zurückfallen. if (!(typ in MATERIAL_FARBE)) { expect(f, `${typ} fällt auf Salbei zurück`).toBe(MATERIAL_FALLBACK); } } }); it("gibt für unbekannte Typen den Salbei-Fallback zurück", () => { expect(materialFarbe("gibtsnicht")).toBe(MATERIAL_FALLBACK); }); }); describe("Farbableitung – Ampel bleibt führend", () => { it("nutzt für den Hauptkörper exakt die übergebene Ampelfarbe", () => { expect(rolleFarbe("koerper", "#c96f2e")).toBe("#c96f2e"); }); it("leitet Akzente nur heller/dunkler ab und lässt Ungültiges unverändert", () => { expect(mischen("#808080", 1)).toBe("#ffffff"); expect(mischen("#808080", -1)).toBe("#000000"); expect(mischen("#808080", 0)).toBe("#808080"); expect(mischen("keinhex", 0.5)).toBe("keinhex"); }); });