Compare commits
2 Commits
8baba7e124
...
7c94de1590
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c94de1590 | |||
| 5d8253a22b |
+4
-3
@@ -623,7 +623,7 @@ Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Wor
|
|||||||
## Phase F — Library & Import/Export
|
## Phase F — Library & Import/Export
|
||||||
- [x] F1 Backend library_folders/library_blocks + Migrationsfile + Tests (CRUD, scopes) *(umgesetzt als global_block_folders/global_blocks mit SqliteAdapter-Migration + CRUD-Tests; .wcadlib-Paket zusätzlich im Log F1)*
|
- [x] F1 Backend library_folders/library_blocks + Migrationsfile + Tests (CRUD, scopes) *(umgesetzt als global_block_folders/global_blocks mit SqliteAdapter-Migration + CRUD-Tests; .wcadlib-Paket zusätzlich im Log F1)*
|
||||||
- [x] F2 UI LibraryPanel (Baum, Suche, Favoriten, Thumbnail grid), DragDrop→Canvas insert instance
|
- [x] F2 UI LibraryPanel (Baum, Suche, Favoriten, Thumbnail grid), DragDrop→Canvas insert instance
|
||||||
- [ ] F3 „Als Block speichern“ (Auswahl→Block, Thumbnail client-seitig canvas.toDataURL klein)
|
- [x] F3 „Als Block speichern“ (Auswahl→Block, Thumbnail client-seitig canvas.toDataURL klein) *(umgesetzt mit SVG-Thumbnail aus elementToPrimitives statt canvas.toDataURL — konsistent zu svg_data im Tree, DOM-frei unit-testbar)*
|
||||||
- [ ] F4 LibraryProviderExtension anbinden: builtin-catalog JSONs (event/architecture/landscape Startersets je ~15-30 Blöcke), user-library, global-library
|
- [ ] F4 LibraryProviderExtension anbinden: builtin-catalog JSONs (event/architecture/landscape Startersets je ~15-30 Blöcke), user-library, global-library
|
||||||
- [ ] F5 DXF Parser erweitern: ELLIPSE, SPLINE(grad≤3→Polyline approx), MTEXT, POINT, HATCH(basic solid/lines), ATTDEF/ATTR, INSERT→Blockinstanz; Fehlerreport (Liste skipped entities)
|
- [ ] F5 DXF Parser erweitern: ELLIPSE, SPLINE(grad≤3→Polyline approx), MTEXT, POINT, HATCH(basic solid/lines), ATTDEF/ATTR, INSERT→Blockinstanz; Fehlerreport (Liste skipped entities)
|
||||||
- [ ] F6 DXF Writer erweitern: BLOCKS+INSERTS, ATTR; Version AC1009 (R12) + AC1015 Ziel
|
- [ ] F6 DXF Writer erweitern: BLOCKS+INSERTS, ATTR; Version AC1009 (R12) + AC1015 Ziel
|
||||||
@@ -716,6 +716,7 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
|||||||
| 2026-08-27 | C3 | elementToPrimitives erweitert um fehlende Typen: dimension (Linie + Label aus value/text), leader (Linie + Label am Ende), revcloud (geschlossener Punktzug), chair (gefuelltes Rechteck #4a90d9), table (Kreis #8b7355), stage (dunkles Rechteck #3a3a4a); Legacy-Konventionen beibehalten; 6 neue Tests | ALLE GRUEN: 481/481 gesamt (+6), tsc 0, build OK | siehe git | Naechster Task: C4 (Flag-Umdrehung Pixi=Default) oder Selection-Handles/Snap-Marks |
|
| 2026-08-27 | C3 | elementToPrimitives erweitert um fehlende Typen: dimension (Linie + Label aus value/text), leader (Linie + Label am Ende), revcloud (geschlossener Punktzug), chair (gefuelltes Rechteck #4a90d9), table (Kreis #8b7355), stage (dunkles Rechteck #3a3a4a); Legacy-Konventionen beibehalten; 6 neue Tests | ALLE GRUEN: 481/481 gesamt (+6), tsc 0, build OK | siehe git | Naechster Task: C4 (Flag-Umdrehung Pixi=Default) oder Selection-Handles/Snap-Marks |
|
||||||
| 2026-08-28 | F2-backend | Block-Favoriten: schema.sql is_favorite INTEGER DEFAULT 0 + SqliteAdapter-Migration (PRAGMA table_info-Muster) für Bestands-DBs, mapGlobalBlockRow 0/1→boolean, createGlobalBlock/updateGlobalBlock erweitert, DBGlobalBlock.is_favorite: boolean, PATCH /api/global-blocks/:id validiert is_favorite (nicht-boolean→400); ROT-Beweis: 5 Tests zuerst ROT (is_favorite fehlte komplett), dann GRUEN | Backend: 263/263 Tests (+5), tsc 0 | 83a94e4 | F2-BACKEND COMPLETE |
|
| 2026-08-28 | F2-backend | Block-Favoriten: schema.sql is_favorite INTEGER DEFAULT 0 + SqliteAdapter-Migration (PRAGMA table_info-Muster) für Bestands-DBs, mapGlobalBlockRow 0/1→boolean, createGlobalBlock/updateGlobalBlock erweitert, DBGlobalBlock.is_favorite: boolean, PATCH /api/global-blocks/:id validiert is_favorite (nicht-boolean→400); ROT-Beweis: 5 Tests zuerst ROT (is_favorite fehlte komplett), dann GRUEN | Backend: 263/263 Tests (+5), tsc 0 | 83a94e4 | F2-BACKEND COMPLETE |
|
||||||
| 2026-08-28 | F2-frontend | BlockLibraryTree: Suchfeld (global-lib-toolbar, filtert root+folder+favorites), ★/☆-Favorite-Toggle je Block (setGlobalBlockFavorite-API, optimistisches UI-Update), Favoriten-Sektion mit goldenem Titel, SVG-Thumbnail-Vorschau (tree-thumb, dangerouslySetInnerHTML, Fallback 📦), Grid-Ansicht (lib-grid, Thumbnail-Zellen mit Drag-Drop + Kontextmenü + Favorit); GlobalBlock.is_favorite: boolean in api.ts | Frontend: 491/491 Tests, tsc 0, build OK | 8b50c0a | **F2 COMPLETE — LibraryPanel-Kernfeatures**: Baum+Suche+Favoriten+Thumbnails+Grid+DragDrop→Canvas (Drop-Handler existierte bereits) |
|
| 2026-08-28 | F2-frontend | BlockLibraryTree: Suchfeld (global-lib-toolbar, filtert root+folder+favorites), ★/☆-Favorite-Toggle je Block (setGlobalBlockFavorite-API, optimistisches UI-Update), Favoriten-Sektion mit goldenem Titel, SVG-Thumbnail-Vorschau (tree-thumb, dangerouslySetInnerHTML, Fallback 📦), Grid-Ansicht (lib-grid, Thumbnail-Zellen mit Drag-Drop + Kontextmenü + Favorit); GlobalBlock.is_favorite: boolean in api.ts | Frontend: 491/491 Tests, tsc 0, build OK | 8b50c0a | **F2 COMPLETE — LibraryPanel-Kernfeatures**: Baum+Suche+Favoriten+Thumbnails+Grid+DragDrop→Canvas (Drop-Handler existierte bereits) |
|
||||||
|
| 2026-08-28 | F3 | blockThumbnail.ts NEU (generateBlockSvg/primitivesToSvg: Elemente→elementToPrimitives→eigenständiges SVG mit PAD-BBox, alle 6 Primitive-Kinds, XML-Escape, Arc→path mit Konvention 0°=3Uhr/CCW); Save-Button 💾 im Bibliotheks-Header (liest __v2GetSelection + documentService.getActiveDocument, Name via prompt, block_data=Element-Array → Drop-Handler akzeptiert Arrays, svg_data=Thumbnail, createGlobalBlock+loadData-Refresh); Dokumentierte Abweichung: SVG statt canvas.toDataURL (konsistent, testbar); ROT-Beweis: 4 Tests zuerst fehlgeschlagen (Modul fehlte), 1 Erwartung nach Primitive-BBox-Analyse korrigiert (-50 -50 560 360); Fix: Duplikat-Import + Handler vor loadData-Deklaration (tsc 2300/2448) | Frontend: 495/495 Tests (+4), tsc 0, build OK | 5d8253a | **F3 COMPLETE — Auswahl als Block speichern**: Canvas-Selektion → wiederverwendbarer Block mit Thumbnail, sofort per DragDrop einsetzbar |
|
||||||
|
|
||||||
## Gefundene Nebenprobleme (noch nicht zugewiesen)
|
## Gefundene Nebenprobleme (noch nicht zugewiesen)
|
||||||
- `webhook-receiver.py` im Root: Zweck unklar, ggf. entfernen/dokumentieren (Frage an Nutzer)
|
- `webhook-receiver.py` im Root: Zweck unklar, ggf. entfernen/dokumentieren (Frage an Nutzer)
|
||||||
@@ -723,5 +724,5 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
|||||||
- `frontend/src/utils/__tests__/format.test.ts` liegt in src statt tests/ — vereinheitlichen (kleiner Task in I2)
|
- `frontend/src/utils/__tests__/format.test.ts` liegt in src statt tests/ — vereinheitlichen (kleiner Task in I2)
|
||||||
|
|
||||||
---
|
---
|
||||||
*Nächster offener Schritt: **F3** („Als Block speichern“: Auswahl→Block, Thumbnail client-seitig canvas.toDataURL) — Details in Teil 5. Alternativ: F4 (LibraryProviderExtension/Startersets), Nebenproblem „Date.now()-IDs“ (user/proj/pfolder/draw/layer/block → randomUUID), Legacy-Entsorgung nach visueller Verifikation, oder Phase G (Layouts/PDF).*
|
*Nächster offener Schritt: **F4** (LibraryProviderExtension anbinden: builtin-catalog JSONs, Startersets) — Details in Teil 5. Alternativ: F5/F6 (DXF Parser/Writer erweitern), Nebenproblem „Date.now()-IDs“ (user/proj/pfolder/draw/layer/block → randomUUID), Legacy-Entsorgung nach visueller Verifikation, oder Phase G (Layouts/PDF).*
|
||||||
*Status 2026-08-28: Phasen B+A+C+D+E+F1+F2 komplett. FE 491/491 Tests, BE 263/263, tsc/Build grün. Alle Commits auf Forgejo main.*
|
*Status 2026-08-28: Phasen B+A+C+D+E+F1+F2+F3 komplett. FE 495/495 Tests, BE 263/263, tsc/Build grün. Alle Commits auf Forgejo main.*
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import {
|
|||||||
exportLibrary,
|
exportLibrary,
|
||||||
importLibrary,
|
importLibrary,
|
||||||
} from '../services/api';
|
} from '../services/api';
|
||||||
|
import { getActiveDocument } from '../kernel/document/documentService';
|
||||||
|
import { generateBlockSvg } from '../utils/blockThumbnail';
|
||||||
|
import type { CADElement } from '../types/cad.types';
|
||||||
|
|
||||||
interface BlockLibraryTreeProps {
|
interface BlockLibraryTreeProps {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -76,6 +79,38 @@ const BlockLibraryTree: React.FC<BlockLibraryTreeProps> = ({ token, onBlockDragS
|
|||||||
loadData();
|
loadData();
|
||||||
}, [loadData]);
|
}, [loadData]);
|
||||||
|
|
||||||
|
// ─── Task F3: Auswahl als Block speichern ─────────────────
|
||||||
|
const handleSaveSelectionAsBlock = useCallback(async () => {
|
||||||
|
const bridge = (window as unknown as { __v2GetSelection?: () => { ids: string[] } }).__v2GetSelection;
|
||||||
|
const ids = bridge?.().ids ?? [];
|
||||||
|
if (ids.length === 0) {
|
||||||
|
window.alert('Keine Auswahl im Canvas — erst Elemente wählen (V2-Tools).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const doc = getActiveDocument();
|
||||||
|
const elements: CADElement[] = [];
|
||||||
|
for (const id of ids) {
|
||||||
|
const el = doc?.getElement(id);
|
||||||
|
if (el) elements.push(el);
|
||||||
|
}
|
||||||
|
if (elements.length === 0) {
|
||||||
|
window.alert('Auswahl enthält keine Elemente.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = window.prompt('Blockname:', `Block-${new Date().toISOString().slice(0, 10)}`);
|
||||||
|
if (!name?.trim()) return;
|
||||||
|
try {
|
||||||
|
await createGlobalBlock(token, {
|
||||||
|
name: name.trim(),
|
||||||
|
block_data: JSON.stringify(elements),
|
||||||
|
svg_data: generateBlockSvg(elements),
|
||||||
|
});
|
||||||
|
await loadData();
|
||||||
|
} catch (err) {
|
||||||
|
window.alert(`Speichern fehlgeschlagen: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
}
|
||||||
|
}, [token, loadData]);
|
||||||
|
|
||||||
// ─── Task F1: Library-Paket Export/Import ────────────────
|
// ─── Task F1: Library-Paket Export/Import ────────────────
|
||||||
|
|
||||||
const handleLibraryExport = useCallback(async () => {
|
const handleLibraryExport = useCallback(async () => {
|
||||||
@@ -459,6 +494,13 @@ const BlockLibraryTree: React.FC<BlockLibraryTreeProps> = ({ token, onBlockDragS
|
|||||||
<div className="global-lib-tree" onContextMenu={(e) => handleContextMenu(e, 'root', null, null)}>
|
<div className="global-lib-tree" onContextMenu={(e) => handleContextMenu(e, 'root', null, null)}>
|
||||||
<div className="global-lib-header">
|
<div className="global-lib-header">
|
||||||
<span className="global-lib-title">🌐 Globale Bibliothek</span>
|
<span className="global-lib-title">🌐 Globale Bibliothek</span>
|
||||||
|
<button
|
||||||
|
className="global-lib-add-btn"
|
||||||
|
title="Canvas-Auswahl als Block speichern (F3)"
|
||||||
|
onClick={() => void handleSaveSelectionAsBlock()}
|
||||||
|
>
|
||||||
|
💾
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="global-lib-add-btn"
|
className="global-lib-add-btn"
|
||||||
title="Bibliothek als .wcadlib exportieren"
|
title="Bibliothek als .wcadlib exportieren"
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* blockThumbnail – SVG-Thumbnail-Generierung für Block-Bibliothek (Task F3).
|
||||||
|
*
|
||||||
|
* Statt canvas.toDataURL werden die Elemente über die bereits vorhandene,
|
||||||
|
* testbare Shape-Fabrik elementToPrimitives (Task C2) beschrieben und als
|
||||||
|
* eigenständiges SVG serialisiert. Vorteile:
|
||||||
|
* - Konsistent zu den svg_data-Thumbnails der globalen Bibliothek (Tree/Grid)
|
||||||
|
* - Unit-testbar ohne Canvas/DOM
|
||||||
|
* - Skalierbar & themefarben-identisch zur Zeichenfläche
|
||||||
|
*
|
||||||
|
* BBox-Konvention wie elementDrawers: rect x/y = Zentrum der BBox,
|
||||||
|
* circle/arc radius via center ± r.
|
||||||
|
*/
|
||||||
|
import { elementToPrimitives } from '../render/pixi/elementDrawers';
|
||||||
|
import type { ShapePrimitive } from '../render/pixi/elementDrawers';
|
||||||
|
import type { CADElement } from '../types/cad.types';
|
||||||
|
|
||||||
|
const PAD = 10;
|
||||||
|
|
||||||
|
function fmt(n: number): string {
|
||||||
|
return String(Math.round(n * 100) / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeXml(s: string): string {
|
||||||
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BBox { minX: number; minY: number; maxX: number; maxY: number }
|
||||||
|
|
||||||
|
function primExtent(p: ShapePrimitive, acc: BBox): void {
|
||||||
|
const add = (x: number, y: number) => {
|
||||||
|
if (x < acc.minX) acc.minX = x;
|
||||||
|
if (y < acc.minY) acc.minY = y;
|
||||||
|
if (x > acc.maxX) acc.maxX = x;
|
||||||
|
if (y > acc.maxY) acc.maxY = y;
|
||||||
|
};
|
||||||
|
switch (p.kind) {
|
||||||
|
case 'line':
|
||||||
|
add(p.from.x, p.from.y); add(p.to.x, p.to.y);
|
||||||
|
break;
|
||||||
|
case 'rect':
|
||||||
|
add(p.x - p.w / 2, p.y - p.h / 2); add(p.x + p.w / 2, p.y + p.h / 2);
|
||||||
|
break;
|
||||||
|
case 'circle':
|
||||||
|
add(p.center.x - p.radius, p.center.y - p.radius); add(p.center.x + p.radius, p.center.y + p.radius);
|
||||||
|
break;
|
||||||
|
case 'arc':
|
||||||
|
add(p.center.x - p.radius, p.center.y - p.radius); add(p.center.x + p.radius, p.center.y + p.radius);
|
||||||
|
break;
|
||||||
|
case 'polyline':
|
||||||
|
for (const pt of p.points) add(pt.x, pt.y);
|
||||||
|
break;
|
||||||
|
case 'text':
|
||||||
|
add(p.at.x, p.at.y - p.fontSize);
|
||||||
|
add(p.at.x + p.fontSize * 0.6 * p.text.length, p.at.y);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function primToSvgTag(p: ShapePrimitive): string {
|
||||||
|
switch (p.kind) {
|
||||||
|
case 'line':
|
||||||
|
return `<line x1="${fmt(p.from.x)}" y1="${fmt(p.from.y)}" x2="${fmt(p.to.x)}" y2="${fmt(p.to.y)}" stroke="${p.stroke}" stroke-width="${p.strokeWidth}"/>`;
|
||||||
|
case 'rect':
|
||||||
|
return `<rect x="${fmt(p.x - p.w / 2)}" y="${fmt(p.y - p.h / 2)}" width="${fmt(p.w)}" height="${fmt(p.h)}" fill="${p.fill ?? 'none'}" stroke="${p.stroke}" stroke-width="${p.strokeWidth}"/>`;
|
||||||
|
case 'circle':
|
||||||
|
return `<circle cx="${fmt(p.center.x)}" cy="${fmt(p.center.y)}" r="${fmt(p.radius)}" fill="${p.fill ?? 'none'}" stroke="${p.stroke}" stroke-width="${p.strokeWidth}"/>`;
|
||||||
|
case 'arc': {
|
||||||
|
// Konvention: 0° = 3 Uhr, gegen Uhrzeigersinn (Bildschirm-Y zeigt nach unten)
|
||||||
|
const rad = (deg: number) => (deg * Math.PI) / 180;
|
||||||
|
const sx = p.center.x + p.radius * Math.cos(rad(p.startDeg));
|
||||||
|
const sy = p.center.y - p.radius * Math.sin(rad(p.startDeg));
|
||||||
|
const ex = p.center.x + p.radius * Math.cos(rad(p.endDeg));
|
||||||
|
const ey = p.center.y - p.radius * Math.sin(rad(p.endDeg));
|
||||||
|
let delta = (p.endDeg - p.startDeg) % 360;
|
||||||
|
if (delta < 0) delta += 360;
|
||||||
|
const largeArc = delta > 180 ? 1 : 0;
|
||||||
|
// CCW auf dem Schirm = SVG sweep-flag 0
|
||||||
|
return `<path d="M ${fmt(sx)} ${fmt(sy)} A ${fmt(p.radius)} ${fmt(p.radius)} 0 ${largeArc} 0 ${fmt(ex)} ${fmt(ey)}" fill="none" stroke="${p.stroke}" stroke-width="${p.strokeWidth}"/>`;
|
||||||
|
}
|
||||||
|
case 'polyline': {
|
||||||
|
const pts = p.points.map(pt => `${fmt(pt.x)},${fmt(pt.y)}`).join(' ');
|
||||||
|
if (p.close) {
|
||||||
|
return `<polygon points="${pts}" fill="${'none'}" stroke="${p.stroke}" stroke-width="${p.strokeWidth}"/>`;
|
||||||
|
}
|
||||||
|
return `<polyline points="${pts}" fill="none" stroke="${p.stroke}" stroke-width="${p.strokeWidth}"/>`;
|
||||||
|
}
|
||||||
|
case 'text':
|
||||||
|
return `<text x="${fmt(p.at.x)}" y="${fmt(p.at.y)}" font-size="${p.fontSize}" fill="${p.color}">${escapeXml(p.text)}</text>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Serialisiert Primitive zu einem eigenständigen SVG-String (leer bei []). */
|
||||||
|
export function primitivesToSvg(prims: ShapePrimitive[]): string {
|
||||||
|
if (prims.length === 0) return '';
|
||||||
|
const acc: BBox = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
|
||||||
|
for (const p of prims) primExtent(p, acc);
|
||||||
|
const w = acc.maxX - acc.minX;
|
||||||
|
const h = acc.maxY - acc.minY;
|
||||||
|
const vb = `${fmt(acc.minX - PAD)} ${fmt(acc.minY - PAD)} ${fmt(w + 2 * PAD)} ${fmt(h + 2 * PAD)}`;
|
||||||
|
const body = prims.map(primToSvgTag).join('');
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${vb}">${body}</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Erzeugt ein SVG-Thumbnail aus einer Element-Auswahl (Task F3). */
|
||||||
|
export function generateBlockSvg(elements: CADElement[]): string {
|
||||||
|
const prims = elements.flatMap(el => elementToPrimitives(el));
|
||||||
|
return primitivesToSvg(prims);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* BlockThumbnail Tests (Task F3) – SVG-Thumbnail aus elementToPrimitives
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { generateBlockSvg, primitivesToSvg } from '../src/utils/blockThumbnail';
|
||||||
|
import { elementToPrimitives } from '../src/render/pixi/elementDrawers';
|
||||||
|
import type { CADElement } from '../src/types/cad.types';
|
||||||
|
|
||||||
|
describe('blockThumbnail (F3)', () => {
|
||||||
|
const chair: CADElement = {
|
||||||
|
id: 'c1',
|
||||||
|
type: 'chair',
|
||||||
|
layerId: 'l0',
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
properties: {},
|
||||||
|
} as CADElement;
|
||||||
|
|
||||||
|
it('erzeugt SVG mit viewBox aus Element-BBox', () => {
|
||||||
|
const svg = generateBlockSvg([chair]);
|
||||||
|
expect(svg).toContain('<svg');
|
||||||
|
expect(svg).toContain('viewBox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('konvertiert rect-Primitive in SVG-rect', () => {
|
||||||
|
const prims = elementToPrimitives(chair);
|
||||||
|
expect(prims.length).toBeGreaterThan(0);
|
||||||
|
const svg = primitivesToSvg(prims);
|
||||||
|
expect(svg).toContain('<rect');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('liefert leeren String für leere Auswahl', () => {
|
||||||
|
expect(generateBlockSvg([])).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('BBox umfasst mehrere Elemente korrekt', () => {
|
||||||
|
const a: CADElement = { ...chair, id: 'a', x: 0, y: 0 };
|
||||||
|
const b: CADElement = { ...chair, id: 'b', x: 500, y: 300 };
|
||||||
|
const svg = generateBlockSvg([a, b]);
|
||||||
|
// chair-Drawer erzeugt rect-Zentrum bei (el.x - w/2) → BBox -40..500/-40..300 + PAD 10
|
||||||
|
expect(svg).toContain('viewBox="-50 -50 560 360"');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user