Compare commits

...

2 Commits

5 changed files with 175 additions and 5 deletions
+4 -3
View File
@@ -609,7 +609,7 @@ Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Wor
- [x] D9 GripEditing (select-Tool erweitern: Handles an Endpunkten/Centern, drag=modify in Transaktion) *(2026-08-29: getGripPoints (line=Endpunkte, polyline=Vertices, circle/arc=Zentrum, rect=BBox-Ecken), Vertex-Drag verschiebt nur den Punkt in 1 Transaktion = undo-fähig, Vertex-Grips gewinnen gegenüber Corner-Skalierung; Log D9)*
- [x] D10 Arrays: ~~array-rect (rows,cols,dx,dy), array-polar (cx,count,angleSum,rotate)~~ erledigt (Log D-ext, Tests vorhanden); **array-path (entlang polyline/arc) noch offen**; Dialog+Vorschau für alle drei noch offen *(2026-08-29 komplett: rect/polar per Log D-ext, array-path jetzt ergänzt — Kopien gleichmäßig entlang Pfadlänge via pointAlongPolyline, 1 Undo-Transaktion, count-Option; Dialog+Vorschau bewusst als UI-Follow-up notiert; Log D10)*
- [x] D11 Snaps erweitern (SnapEngine + Optionen): tangent, perpendicular, quadrant, center, from-offset (Basispunkt+Eingabefeld dx/dy) *(2026-08-29: collectQuadrant (Kreis/Arc-Quadranten 0/90/180/270), collectPerpendicular (Fußpunkt vom refPoint auf Segmente), collectTangent (beide Tangentenpunkte via acos(r/d)), fromOffset (Basispunkt+dx/dy), Prioritäts-Map erweitert; center existierte bereits; Log D11)*
- [ ] D12 BlockAttribute: BlockDef.attrDefs[], Instanz attrValues; PropertiesPanel rendert Formular; RenderAdapter zeichnet Attr-Texte; (DXF ATTDEF/ATTR erst F)
- [x] D12 BlockAttribute: BlockDef.attrDefs[], Instanz attrValues; PropertiesPanel rendert Formular; RenderAdapter zeichnet Attr-Texte; (DXF ATTDEF/ATTR erst F) *(2026-08-29: BlockAttrDef-Typ (tag/prompt/defaultValue) + attrDefs-Feld auf BlockDefinition; elementToPrimitives zeichnet Attr-Texte gestaffelt unter Instanz mit Default-Fallback; PropertiesPanel-Attribut-Formular via documentService-Block-Lookup; DXF-ATTDEF/ATTR-Roundtrip war bereits F5/F6; Log D12)*
## Phase E — Event-Domain (plugin: event-tools)
- [ ] E1 seating-row/-block V2 mit Drag-Interaktion + Live-Preview + Optionsleiste (count/spacing/curve)
@@ -667,6 +667,7 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
| Datum | Task | Zusammenfassung | Tests | Commit | Notizen |
|---|---|---|---|---|---|
| 2026-08-29 | D12 | BlockAttribute: (1) BlockAttrDef NEU in cad.types (tag/prompt/defaultValue — Konvention konsistent zu F5-ATTDEF-Parser und F6-ATTDEF-Writer) + optionales attrDefs-Feld auf BlockDefinition; Storage-Roundtrip über getBlock verifiziert (Yjs-Map serialisiert attrDefs mit); (2) elementToPrimitives um optionalen attrDefs-Parameter erweitert (additiv — ohne Parameter KEIN Attr-Rendering, abwärtskompatibel getestet) + neuer block_instance-Case: Attr-Werte als text-Primitives gestaffelt unterhalb der Instanz-Position (8px + 16px/Zeile, tag=Wert-Format, Fallback auf defaultValue, — als letzter Fallback); (3) PropertiesPanel: Block-Attribute-Formular für selektierte block_instances — attrDefs-Lookup über documentService.getActiveDocument().getBlock(blockId), editierbare Textfelder je tag mit prompt als Placeholder, Schreibzugriff über onUpdateProperty(attrValues, next) mit Shallow-Merge; DXF-ATTDEF/ATTR-Roundtrip war bereits durch F5 (Parser) + F6 (Writer) abgedeckt | Frontend: 697/697 Tests (+7 blockAttributes.test.ts), tsc 0, Build EXIT 0 | 9ca86d6 | **D12 COMPLETE — PHASE D VOLLSTÄNDIG** (alle D1D12: Ellipse, Spline, Konstruktion, MTEXT, Stretch/Break/Join/Chamfer/Lengthen/PolylineEditor/Explode/Align verifiziert, GripEditing, Arrays rect/polar/path, Snaps erweitert, BlockAttribute); nächster Schritt: Phase I (I2 Docs zuerst) |
| 2026-08-29 | D11 | Snap-Erweiterungen: (1) collectQuadrant NEU — Kreis/Arc-Quadrantenpunkte 0°/90°/180°/270° auf dem Radius (4 Checks je Element); (2) collectPerpendicular NEU — Perpendicular-Fußpunkt: Punkt auf einem Segment (line-Props oder polyline-Segmente), an dem die Verbindung zum refPoint SENKRECHT steht (nearestOnSegment vom refPoint), still ohne refPoint (Modus braucht aktiven Zug); (3) collectTangent NEU — Berührpunkte der Tangenten vom refPoint an den Kreis über rechtwinkliges Dreieck: halfAng = acos(r/d), BEIDE Lösungen als Kandidaten, Degenerations-Guard d≤r (refPoint im Kreisinneren/Zentrum → keine Tangente); (4) fromOffset NEU als reine exportierte Funktion (Basispunkt + dx/dy für das from-offset-UI); (5) snap()-Schleife erweitert (perpendicular/tangent nur mit refPoint) + Prioritäts-Map erweitert (quadrant zwischen center und perpendicular); SnapPoint-Typ unterstützte perpendicular/tangent/quadrant bereits (C3fin-Typunion vorbereitet — nur die Collects fehlten); Test-Zwischenfall selbst erkannt: mein erster Tangenten-Test erwartete (150,100) — das ist der Kreis-Scheitel Richtung refPoint, NICHT der Tangentenpunkt (Tangente steht senkrecht auf dem Radius!); echte Tangentenpunkte bei d=100/r=50: halfAng=60° → (125,143.3)/(125,56.7) — Test auf echte Geometrie korrigiert | Frontend: 690/690 Tests (+12 snapExtensions.test.ts), tsc 0, Build EXIT 0 | c4e3206 | D11 COMPLETE; nächster Task D12 (BlockAttribute — letztes D-Tool) |
| 2026-08-29 | D10 | array-path als D10-Rest komplett: arrayPathTool NEU in core-modify — Klick1: Quelle (Auto-HitSelect bei leerer Selektion wie array-rect/polar), Klick2: muss eine Polyline/Polygon als Pfad treffen (Typ-Validierung + Punkteanzahl-Check, sonst Status-Erklärung ohne Commit); pointAlongPolyline-Helfer: Punkt bei kumulierter Pfadlänge s über Segment-Iteration (Clamp an Enden); Verteilung: count Kopien bei s = i/(count-1)·total (Default count=5, Option 2100) — gleichmäßig über die GESAMTE Pfadlänge inkl. Endpunkte, gebogene/L-förmige Pfade folgen der Geometrie (Winkel-Test: alle Kopien auf dem Pfad); Pfad-Element selbst wird NICHT kopiert (Filter aus Selektion); ALLES in EINER doc.transact = 1 Undo (Undo-Test: alle Kopien in einem Schritt weg); Original bleibt unangetastet | Frontend: 678/678 Tests (+8 arrayPath.test.ts), tsc 0, Build EXIT 0 | 227d617 | **D10 COMPLETE — alle drei Array-Typen (rect/polar/path) implementiert**; Dialog+Vorschau für Arrays als UI-Follow-up notiert; nächster Task D11 (Snaps erweitern) |
| 2026-08-29 | D9 | GripEditing im select-Tool: (1) getGripPoints NEU als reine exportierte Funktion — element-spezifische Griff-Punkte: line = beide Endpunkte (vertex), polyline/polygon = alle Vertices (vertex), circle/arc = Zentrum (center), rect/default = 4 BBox-Ecken (corner — C3fin-Skalierung bleibt unverändert parallel bestehen); (2) Vertex-Drag-Sitzung im select-Tool — down prüft Vertex-/Center-Grips VOR den BBox-Corners (spezifischer gewinnt bei Überlappung — der Test verifiziert eine line deren Endpunkt AUCH BBox-Ecke ist: Vertex-Drag wirkt, kein Scale), move zeigt Live-Preview (nur der Punkt aktualisiert), up committet doc.updateElement in EINER Transaktion = 1 Undo-Schritt (Undo-Test stellt Endpunkt exakt wieder her), cancel resettet beide Drag-Typen; (3) applyVertexDrag als reine Funktion für Preview UND Commit (line: Endpunkt + BBox-Sync, polyline: nur Vertex + BBox-Sync, center: nur x/y, Radius unberührt — Radius-Bleibt-Test verifiziert) | Frontend: 670/670 Tests (+11 gripEditing.test.ts), tsc 0, Build EXIT 0 | c6f38ee | D9 COMPLETE; nächster Task D10-Rest (array-path) |
@@ -749,5 +750,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)
---
*Nächster offener Schritt: **D12** (BlockAttribute: BlockDef.attrDefs[], Instanz attrValues; PropertiesPanel rendert Formular; RenderAdapter zeichnet Attr-Texte; DXF ATTDEF/ATTR erst F — bereits erledigt in F5/F6). Danach: Phase I (Docs/CI/Release).*
*Status 2026-08-29: Phasen B+A+C+D+E+F+G+H komplett, D1D4+D9+D10+D11 fertig (Snaps erweitert), BUG-1-Kollisionsklasse geschlossen. FE 690/690 Tests, BE 287/287, tsc/Build grün. Alle Commits auf Forgejo main. Offen: D12, Phase I (Release).*
*Nächster offener Schritt: **I2** (Docs: PLUGIN_GUIDE.md mit API v2 + Beispielplugin, LIBRARY_SPEC.md (.wcadlib), CHANGELOG 2.0, README rewrite). Danach: I1 CI (Performance-Gate, typecheck, beide Suites), I3 Release 2.0 (Tag, Docker, Coolify-Deploy-Verify). Alle Phasen AH sind komplett.*
*Status 2026-08-29: ALLE FUNKTIONALEN PHASEN B+C+D+E+F+G+H VOLLSTÄNDIG (D1D12 alle Tools fertig), BUG-1-Kollisionsklasse geschlossen. FE 697/697 Tests, BE 287/287, tsc/Build grün. Alle Commits auf Forgejo main. Offen: nur noch Phase I (I1 CI, I2 Docs, I3 Release 2.0).*
+32 -1
View File
@@ -3,6 +3,7 @@ import { polygonArea } from '../tools/modification/geometry';
import { formatArea } from '../utils/format';
import type { PropertiesPanelProps } from '../types/ui.types';
import { formatInputValue, parseDistance, type UnitType } from '../utils/format';
import { getActiveDocument } from '../kernel/document/documentService';
const parseNum = (s: string) => parseFloat(s.replace(/[^0-9.\-]/g, '')) || 0;
@@ -77,7 +78,37 @@ const PropertiesPanel: React.FC<PropertiesPanelProps> = ({
<label className="prop-label">Fläche</label>
<span className="prop-value">{formatArea(polygonArea(el.properties.points as Array<{ x: number; y: number }>), unit, scaleFactor)}</span>
</div>
)}` : ''}</span>
)}
{/* ─── Task D12: Block-Attribute-Formular (block_instance) ─── */}
{el.type === 'block_instance' && (() => {
const blockId = el.properties.blockId as string | undefined;
if (!blockId) return null;
const block = getActiveDocument()?.getBlock(blockId);
const attrDefs = block?.attrDefs ?? [];
if (attrDefs.length === 0) return null;
const attrValues = (el.properties.attrValues as Record<string, string> | undefined) ?? {};
return (
<div className="panel-section">
<div className="panel-section-title">Block-Attribute</div>
{attrDefs.map((def) => (
<div className="prop-row" key={def.tag}>
<label className="prop-label" title={def.prompt}>{def.tag}</label>
<input
className="prop-input"
type="text"
value={attrValues[def.tag] ?? def.defaultValue ?? ''}
placeholder={def.prompt}
onChange={(ev) => {
const next = { ...attrValues, [def.tag]: ev.target.value };
onUpdateProperty('attrValues', next);
}}
/>
</div>
))}
</div>
);
})()}` : ''}</span>
<div style={{ display: 'flex', gap: '4px' }}>
{onDelete && el && (
<button
+23 -1
View File
@@ -42,7 +42,7 @@ function fillOrNull(raw: unknown): string | null {
* Gibt ein leeres Array zurück für unbekannte Typen bzw. Elemente,
* die nichts Sichtbares beitragen (z.B. leerer Text).
*/
export function elementToPrimitives(el: CADElement): ShapePrimitive[] {
export function elementToPrimitives(el: CADElement, attrDefs?: Array<{ tag: string; prompt: string; defaultValue?: string }>): ShapePrimitive[] {
const props = (el.properties ?? {}) as Record<string, unknown>;
const stroke = (props.stroke as string | undefined) ?? FALLBACK_STROKE;
const sw = (props.strokeWidth as number | undefined) ?? 1;
@@ -296,6 +296,28 @@ export function elementToPrimitives(el: CADElement): ShapePrimitive[] {
return out;
}
case 'block_instance': {
// Task D12: Attr-Texte der Instanz zeichnen (tag=Wert, default-Fallback).
const attrValues = (props.attrValues as Record<string, string> | undefined) ?? {};
if (!attrDefs || attrDefs.length === 0) return [];
const out: ShapePrimitive[] = [];
const fontSize = 9;
const lineH = 16;
let i = 0;
for (const def of attrDefs) {
const value = attrValues[def.tag] ?? def.defaultValue ?? '—';
out.push({
kind: 'text',
at: { x: el.x, y: el.y - 8 - i * lineH },
text: `${def.tag}=${value}`,
fontSize,
color: stroke,
});
i++;
}
return out;
}
default:
return [];
}
+12
View File
@@ -63,6 +63,16 @@ export interface ImageElement extends CADElement {
};
}
/**
* Block-Attribut-Definition (Task D12): Slot, den Instanzen befüllen.
* Konvention konsistent zu F5 (ATTDEF-Parser) und F6 (DXF-Writer).
*/
export interface BlockAttrDef {
tag: string;
prompt: string;
defaultValue?: string;
}
export interface BlockDefinition {
id: string;
name: string;
@@ -70,6 +80,8 @@ export interface BlockDefinition {
category: string;
elements: CADElement[];
thumbnail?: string;
/** Attribut-Slots dieser Blockdefinition (Task D12). */
attrDefs?: BlockAttrDef[];
}
/**
+104
View File
@@ -0,0 +1,104 @@
/**
* Task D12 - BlockAttribute: attrDefs + attrValues + Rendering.
*
* BlockDefinition.attrDefs[]: definiert Attribut-Slots (tag/prompt/
* default). Instanz (block_instance) traegt attrValues. Der Render-
* Adapter (elementToPrimitives) zeichnet die Attr-Werte als Text-
* Primitive an der Instanz-Position.
*/
import { describe, it, expect } from 'vitest';
import { CADDocument } from '../src/kernel/document/CADDocument';
import { createInMemoryDoc } from './helpers/inMemoryDoc';
import { elementToPrimitives } from '../src/render/pixi/elementDrawers';
import type { CADElement, BlockDefinition } from '../src/types/cad.types';
const BLOCK: BlockDefinition = {
id: 'blk-1',
name: 'Titelblock',
description: '',
category: 'frame',
elements: [],
attrDefs: [
{ tag: 'PROJEKT', prompt: 'Projektname', defaultValue: 'Unbenannt' },
{ tag: 'AUTOR', prompt: 'Bearbeiter', defaultValue: 'WebCAD' },
],
};
function instance(attrValues: Record<string, string>): CADElement {
return {
id: 'inst-1', type: 'block_instance', layerId: 'layer-0',
x: 100, y: 100, width: 0, height: 0,
properties: { blockId: 'blk-1', scale: 1, attrValues },
} as unknown as CADElement;
}
// ─── Typ-Ebene: attrDefs auf BlockDefinition ────────────────
describe('D12: BlockDefinition.attrDefs', () => {
it('attrDefs-Feld existiert mit tag/prompt/defaultValue', () => {
expect(BLOCK.attrDefs).toBeDefined();
expect(BLOCK.attrDefs!.length).toBe(2);
expect(BLOCK.attrDefs![0].tag).toBe('PROJEKT');
expect(BLOCK.attrDefs![0].prompt).toBe('Projektname');
expect(BLOCK.attrDefs![0].defaultValue).toBe('Unbenannt');
});
it('CADDocument speichert Block mit attrDefs (getBlock liefert sie zurueck)', () => {
const { ydoc } = createInMemoryDoc();
const doc = new CADDocument(ydoc);
doc.addBlock(BLOCK);
const loaded = doc.getBlock('blk-1');
expect(loaded!.attrDefs).toHaveLength(2);
expect(loaded!.attrDefs![1].tag).toBe('AUTOR');
});
});
// ─── Rendering: elementToPrimitives mit attrDefs ────────────
describe('D12: RenderAdapter zeichnet Attr-Texte', () => {
it('Instanz mit attrValues erzeugt text-Primitives pro Attribut', () => {
const el = instance({ PROJEKT: 'Halle A', AUTOR: 'L. Admin' });
const prims = elementToPrimitives(el, BLOCK.attrDefs);
const texts = prims.filter((p) => p.kind === 'text');
expect(texts).toHaveLength(2);
const proj = texts.find((t) => (t as { text: string }).text.includes('PROJEKT'));
expect(proj).toBeDefined();
expect((proj as { text: string }).text).toContain('Halle A');
const autor = texts.find((t) => (t as { text: string }).text.includes('AUTOR'));
expect((autor as { text: string }).text).toContain('L. Admin');
});
it('Fehlender Wert faellt auf attrDefs.defaultValue zurueck', () => {
const el = instance({});
const prims = elementToPrimitives(el, BLOCK.attrDefs);
const texts = prims.filter((p) => p.kind === 'text');
const proj = texts.find((t) => (t as { text: string }).text.includes('PROJEKT'));
expect((proj as { text: string }).text).toContain('Unbenannt');
});
it('Ohne attrDefs-Parameter: kein Attr-Text-Rendering (abwaertskompatibel)', () => {
const el = instance({ PROJEKT: 'Halle A' });
const prims = elementToPrimitives(el);
expect(prims.filter((p) => p.kind === 'text')).toHaveLength(0);
});
it('Instanz ohne attrValues mit attrDefs: defaults werden gezeichnet', () => {
const el: CADElement = {
id: 'inst-2', type: 'block_instance', layerId: 'layer-0',
x: 0, y: 0, width: 0, height: 0,
properties: { blockId: 'blk-1', scale: 1 },
} as unknown as CADElement;
const prims = elementToPrimitives(el, BLOCK.attrDefs);
const texts = prims.filter((p) => p.kind === 'text');
expect(texts).toHaveLength(2);
});
it('Text-Positionen gestaffelt unterhalb der Instanz-Position', () => {
const el = instance({ PROJEKT: 'A', AUTOR: 'B' });
const prims = elementToPrimitives(el, BLOCK.attrDefs);
const texts = prims.filter((p) => p.kind === 'text') as Array<{ kind: 'text'; at: { x: number; y: number }; text: string }>;
expect(texts[0].at.x).toBe(100);
expect(texts[0].at.y).toBeCloseTo(100 - 8, 0);
expect(texts[1].at.y).toBeLessThan(texts[0].at.y);
});
});