File size: 14,810 Bytes
9de795a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
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");
  });
});