159 lines
6.7 KiB
Markdown
159 lines
6.7 KiB
Markdown
|
|
# Phase 4: Bearbeitungs-Tools (Modification Tools)
|
||
|
|
|
||
|
|
## Requirements
|
||
|
|
- F-CAD-02: Verschieben (MOVE), Kopieren (COPY/CO), Rotieren (ROTATE/RO), Skalieren (SCALE/SC), Spiegeln (MIRROR/MI), Trimmen (TRIM), Verlängern (EXTEND), Abrunden (FILLET), Versatz (OFFSET)
|
||
|
|
- F-CAD-07: Auswahl-Methoden (already in SelectionEngine, ensure modification tools use it)
|
||
|
|
- F-CAD-08: Gruppierung — Elemente zu Gruppen zusammenfassen, verschachteln, speichern
|
||
|
|
|
||
|
|
## Current State
|
||
|
|
- `src/interaction/index.ts`: `handleModifyDown()` is a stub (lines 411-421) — only checks if selection exists, sets phase to 'modifying', pushes point. No actual modification logic.
|
||
|
|
- `onElementsModified` callback exists but is never called.
|
||
|
|
- App.tsx has `handleElementsDeleted` but no `handleElementsModified`.
|
||
|
|
- No tool classes exist in `src/tools/`.
|
||
|
|
|
||
|
|
## Architecture Decision
|
||
|
|
Instead of separate tool class files (BAUPLAN suggests 10+ files), implement modification logic directly in InteractionEngine with a `handleModifyMove(pt)` method that handles all modify tools via switch. This is simpler, matches the existing pattern (handleDrawDown handles all draw tools), and avoids over-engineering. Create a `src/tools/modification/` directory with pure geometry utility functions that InteractionEngine calls.
|
||
|
|
|
||
|
|
## Tasks
|
||
|
|
|
||
|
|
### Task 1: Create geometry utility functions
|
||
|
|
File: `src/tools/modification/geometry.ts`
|
||
|
|
|
||
|
|
Pure functions for element transformation:
|
||
|
|
```typescript
|
||
|
|
export function moveElement(el: CADElement, dx: number, dy: number): CADElement
|
||
|
|
export function rotateElement(el: CADElement, cx: number, cy: number, angle: number): CADElement
|
||
|
|
export function scaleElement(el: CADElement, cx: number, cy: number, sx: number, sy: number): CADElement
|
||
|
|
export function mirrorElement(el: CADElement, x1: number, y1: number, x2: number, y2: number): CADElement
|
||
|
|
export function offsetElement(el: CADElement, distance: number): CADElement
|
||
|
|
export function trimElement(el: CADElement, boundary: CADElement): CADElement | null
|
||
|
|
export function extendElement(el: CADElement, boundary: CADElement): CADElement | null
|
||
|
|
export function filletElements(el1: CADElement, el2: CADElement, radius: number): [CADElement, CADElement] | null
|
||
|
|
```
|
||
|
|
|
||
|
|
Each function returns a NEW CADElement (immutable). For lines: update x1/y1/x2/y2 in properties. For circles: update x/y/radius. For polylines: update points array. For text: update x/y. Apply rotation to properties.rotation.
|
||
|
|
|
||
|
|
### Task 2: Implement modification logic in InteractionEngine
|
||
|
|
File: `src/interaction/index.ts`
|
||
|
|
|
||
|
|
Replace the stub `handleModifyDown` with real multi-step logic:
|
||
|
|
|
||
|
|
**Move/Copy (2 clicks):**
|
||
|
|
1. First click: set base point (state.points[0])
|
||
|
|
2. Mouse move: update preview with moved elements
|
||
|
|
3. Second click: confirm — call onElementsModified with moved elements (Move) or onElementCreated for copies (Copy)
|
||
|
|
4. Reset to idle
|
||
|
|
|
||
|
|
**Rotate (2 clicks):**
|
||
|
|
1. First click: set pivot point
|
||
|
|
2. Mouse move: preview rotation (angle = atan2(mouse - pivot))
|
||
|
|
3. Second click: confirm with current angle
|
||
|
|
|
||
|
|
**Scale (2 clicks):**
|
||
|
|
1. First click: set base point
|
||
|
|
2. Mouse move: preview scale (factor = dist(mouse, base) / dist(firstElement, base))
|
||
|
|
3. Second click: confirm
|
||
|
|
|
||
|
|
**Mirror (2 clicks):**
|
||
|
|
1. First click: first point of mirror axis
|
||
|
|
2. Mouse move: preview mirror
|
||
|
|
3. Second click: second point of mirror axis, confirm
|
||
|
|
|
||
|
|
**Trim/Extend (2 clicks):**
|
||
|
|
1. First click: select boundary element (hitTest)
|
||
|
|
2. Second click: select element to trim/extend (hitTest), apply, confirm
|
||
|
|
|
||
|
|
**Fillet (3 clicks):**
|
||
|
|
1. First click: select first element
|
||
|
|
2. Second click: select second element
|
||
|
|
3. Third click or Enter: set radius (use distance between elements or prompt)
|
||
|
|
|
||
|
|
**Offset (2 clicks):**
|
||
|
|
1. First click: select element to offset
|
||
|
|
2. Second click: direction + distance = offset
|
||
|
|
|
||
|
|
Add `updateModifyPreview(pt)` method similar to `updatePreview(pt)` but for modifications.
|
||
|
|
Add `confirmModify()` method that calls `onElementsModified` with modified elements.
|
||
|
|
|
||
|
|
For Copy: call `onElementCreated` for each copied element instead of `onElementsModified`.
|
||
|
|
|
||
|
|
### Task 3: Wire onElementsModified in App.tsx
|
||
|
|
File: `src/App.tsx`
|
||
|
|
|
||
|
|
- Add `handleElementsModified` callback: push current elements to undoStack, replace modified elements by ID, clear redoStack
|
||
|
|
- Pass `onElementsModified` to CanvasArea
|
||
|
|
- CanvasArea wires it to `interaction.setCallbacks({ onElementsModified: ... })`
|
||
|
|
|
||
|
|
### Task 4: Add GroupTool
|
||
|
|
File: `src/tools/modification/GroupTool.ts`
|
||
|
|
|
||
|
|
Simple group management:
|
||
|
|
```typescript
|
||
|
|
export interface ElementGroup {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
elementIds: string[];
|
||
|
|
parentGroupId: string | null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class GroupManager {
|
||
|
|
private groups: Map<string, ElementGroup> = new Map();
|
||
|
|
createGroup(elementIds: string[], name?: string): ElementGroup
|
||
|
|
ungroup(groupId: string): void
|
||
|
|
getGroup(id: string): ElementGroup | undefined
|
||
|
|
getGroups(): ElementGroup[]
|
||
|
|
getGroupedElements(): Set<string> // all element IDs in any group
|
||
|
|
moveGroup(groupId: string, dx: number, dy: number): string[] // returns modified element IDs
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Wire in App.tsx: add `groups` state, add command 'G' or 'GROUP' to create group from selection.
|
||
|
|
|
||
|
|
### Task 5: Update command shortcuts
|
||
|
|
File: `src/App.tsx`
|
||
|
|
|
||
|
|
Add to handleCommand:
|
||
|
|
- `M` or `MOVE` → setTool('move')
|
||
|
|
- `CO` or `COPY` → setTool('copy')
|
||
|
|
- `RO` or `ROTATE` → setTool('rotate')
|
||
|
|
- `SC` or `SCALE` → setTool('scale')
|
||
|
|
- `MI` or `MIRROR` → setTool('mirror')
|
||
|
|
- `TR` or `TRIM` → setTool('trim')
|
||
|
|
- `EX` or `EXTEND` → setTool('extend')
|
||
|
|
- `F` or `FILLET` → setTool('fillet')
|
||
|
|
- `O` or `OFFSET` → setTool('offset')
|
||
|
|
- `E` or `ERASE` → setTool('delete')
|
||
|
|
- `G` or `GROUP` → create group from selection
|
||
|
|
- `UNG` or `UNGROUP` → ungroup selection
|
||
|
|
|
||
|
|
Note: Some of these already exist from Phase 3. Check and avoid duplicates.
|
||
|
|
|
||
|
|
### Task 6: Update CanvasAreaProps
|
||
|
|
File: `src/types/ui.types.ts`
|
||
|
|
|
||
|
|
Add `onElementsModified` to CanvasAreaProps:
|
||
|
|
```typescript
|
||
|
|
onElementsModified: (els: CADElement[]) => void;
|
||
|
|
```
|
||
|
|
|
||
|
|
## Constraints
|
||
|
|
- TypeScript functional components, no inline styles (except where mockup uses style)
|
||
|
|
- No styled-components, no Tailwind
|
||
|
|
- `npx tsc --noEmit` must pass with 0 errors
|
||
|
|
- `npx vite build` must succeed
|
||
|
|
- Minimal changes — extend existing files, don't rewrite
|
||
|
|
- Keep all existing CSS class names
|
||
|
|
- Pure geometry functions in separate file, not in InteractionEngine
|
||
|
|
|
||
|
|
## File Summary
|
||
|
|
1. `src/tools/modification/geometry.ts` — NEW: pure geometry transform functions
|
||
|
|
2. `src/tools/modification/GroupTool.ts` — NEW: GroupManager class
|
||
|
|
3. `src/interaction/index.ts` — PATCH: implement handleModifyDown, updateModifyPreview, confirmModify
|
||
|
|
4. `src/App.tsx` — PATCH: handleElementsModified, groups state, command shortcuts
|
||
|
|
5. `src/types/ui.types.ts` — PATCH: add onElementsModified to CanvasAreaProps
|
||
|
|
6. `src/components/CanvasArea.tsx` — PATCH: wire onElementsModified callback
|
||
|
|
|
||
|
|
## Test Criteria
|
||
|
|
1. `npx tsc --noEmit` — 0 errors
|
||
|
|
2. `npx vite build` — success
|