task(CAD-4): otrack computeTrackPoint with angle projection, divide/measure path point tools on defpoints in one undo transaction
This commit is contained in:
@@ -494,3 +494,44 @@ export class SnapEngine {
|
||||
export function fromOffset(base: { x: number; y: number }, dx: number, dy: number): { x: number; y: number } {
|
||||
return { x: base.x + dx, y: base.y + dy };
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────
|
||||
// Task CAD-4: OTRACK — Objekt-Fang-Tracking. computeTrackPoint
|
||||
// projiziert den Cursor auf Tracking-Linien durch den Basispunkt
|
||||
// (Default 0/90/180/270°, optional erweiterte Winkel) und liefert
|
||||
// den nächstgelegenen Track-Punkt. Reine Funktion (UI-testbar).
|
||||
// ───────────────────────────────────────────────────
|
||||
|
||||
export function computeTrackPoint(
|
||||
base: { x: number; y: number },
|
||||
cursor: { x: number; y: number },
|
||||
angles?: number[],
|
||||
): { x: number; y: number } | null {
|
||||
const dx = cursor.x - base.x;
|
||||
const dy = cursor.y - base.y;
|
||||
if (dx === 0 && dy === 0) return null;
|
||||
const angs = angles ?? [0, 90, 180, 270];
|
||||
let best: { x: number; y: number } | null = null;
|
||||
let bestDist = Infinity;
|
||||
for (const a of angs) {
|
||||
const rad = (a * Math.PI) / 180;
|
||||
const dxc = Math.cos(rad);
|
||||
const dyc = Math.sin(rad);
|
||||
let pt: { x: number; y: number };
|
||||
if (Math.abs(dxc) >= Math.abs(dyc)) {
|
||||
if (dxc === 0) continue;
|
||||
const t = dx / dxc;
|
||||
pt = { x: cursor.x, y: base.y + t * dyc };
|
||||
} else {
|
||||
if (dyc === 0) continue;
|
||||
const t = dy / dyc;
|
||||
pt = { x: base.x + t * dxc, y: cursor.y };
|
||||
}
|
||||
const dist = Math.hypot(pt.x - cursor.x, pt.y - cursor.y);
|
||||
if (dist < bestDist) {
|
||||
bestDist = dist;
|
||||
best = pt;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
@@ -1715,6 +1715,152 @@ export const offsetTool: ToolExtensionV2 = {
|
||||
};
|
||||
|
||||
/** V2-Plugin: Kern-Bearbeitungswerkzeuge. */
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// Task CAD-4: DIVIDE/MEASURE — Punkte entlang eines Pfads
|
||||
// (Polyline) verteilen. DIVIDE: n gleiche Segmente → n+1 Punkte
|
||||
// inkl. Enden. MEASURE: fester Abstand inkl. Start, Rest verworfen.
|
||||
// Punkte = Konstruktionsmarker auf defpoints (1 Transaktion).
|
||||
// ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Punkte entlang eines Polylinien-Pfads (Task CAD-4, rein).
|
||||
* segments > 0: DIVIDE — n+1 gleichmäßige Punkte inkl. Enden.
|
||||
* fixedSpacing > 0: MEASURE — Punkte bei 0, spacing, 2*spacing, …
|
||||
* solange s ≤ Gesamtlänge; Rest am Ende wird verworfen.
|
||||
*/
|
||||
export function pointsAlongPath(pts: Pt[], segments: number, fixedSpacing?: number): Pt[] {
|
||||
if (!Array.isArray(pts) || pts.length < 2) return [];
|
||||
let total = 0;
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
total += Math.hypot(pts[i + 1].x - pts[i].x, pts[i + 1].y - pts[i].y);
|
||||
}
|
||||
if (total <= 0) return [...pts];
|
||||
const out: Pt[] = [];
|
||||
if (segments > 0) {
|
||||
for (let i = 0; i <= segments; i++) {
|
||||
out.push(pointAlongPolyline(pts, (total * i) / segments));
|
||||
}
|
||||
} else if (fixedSpacing && fixedSpacing > 0) {
|
||||
for (let s = 0; s <= total + 1e-9; s += fixedSpacing) {
|
||||
out.push(pointAlongPolyline(pts, s));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** defpoints-Layer sicherstellen (CAD-4, lokal im core-modify). */
|
||||
function ensureDefpoints(ctx: FullToolContext): void {
|
||||
const doc = ctx.doc as unknown as {
|
||||
getLayer(id: string): unknown;
|
||||
addLayer(l: unknown): void;
|
||||
} | undefined;
|
||||
if (!doc || doc.getLayer('defpoints')) return;
|
||||
doc.addLayer({
|
||||
id: 'defpoints', name: 'defpoints', visible: true, locked: false,
|
||||
color: '#8a8f98', lineType: 'dotted', transparency: 0.5, sortOrder: 999, parentId: null,
|
||||
});
|
||||
}
|
||||
|
||||
/** Pfad-Treffer validieren: muss polyline/polygon mit ≥2 Punkten sein. */
|
||||
function requirePathHit(
|
||||
c: FullToolContext,
|
||||
wx: number,
|
||||
wy: number,
|
||||
): { pts: Pt[]; path: CADElement } | null {
|
||||
if (!c.selection) return null;
|
||||
const hit = c.selection.hitTest(wx, wy, 8);
|
||||
if (!hit || (hit.type !== 'polyline' && hit.type !== 'polygon')) return null;
|
||||
const pts = (hit.properties as Record<string, unknown>).points as Pt[] | undefined;
|
||||
if (!pts || pts.length < 2) return null;
|
||||
return { pts, path: hit };
|
||||
}
|
||||
|
||||
/** Konstruktions-Marker-Punkt auf defpoints platzieren. */
|
||||
function makePathMarker(p: Pt): CADElement {
|
||||
return {
|
||||
id: uid('pathmark'),
|
||||
type: 'circle',
|
||||
layerId: 'defpoints',
|
||||
x: p.x,
|
||||
y: p.y,
|
||||
width: 3,
|
||||
height: 3,
|
||||
properties: { radius: 1.5, point: true, construction: true, stroke: '#8a8f98', strokeWidth: 1 },
|
||||
} as unknown as CADElement;
|
||||
}
|
||||
|
||||
export const divideTool: ToolExtensionV2 = {
|
||||
manifest: {
|
||||
id: 'divide',
|
||||
label: 'Teilen',
|
||||
icon: '÷',
|
||||
ribbonTab: 'canvas',
|
||||
tags: ['pro'],
|
||||
description: 'Pfad in n gleiche Segmente teilen (DIVIDE, Task CAD-4)',
|
||||
},
|
||||
optionsSchema: [
|
||||
{ key: 'count', label: 'Anzahl Segmente', type: 'number', min: 1, max: 100 },
|
||||
],
|
||||
handlers: {
|
||||
down(e, ctx) {
|
||||
const c = ctx as FullToolContext;
|
||||
if (!c.doc) return;
|
||||
const found = requirePathHit(c, e.world.x, e.world.y);
|
||||
if (!found) {
|
||||
ctx.setStatus('Teilen: Polyline als Pfad anklicken');
|
||||
return;
|
||||
}
|
||||
const segments = Math.max(1, (c.options.count as number | undefined) ?? 4);
|
||||
ensureDefpoints(c);
|
||||
const marks = pointsAlongPath(found.pts, segments);
|
||||
c.doc.transact(() => {
|
||||
for (const p of marks) c.doc!.addElement(makePathMarker(p));
|
||||
});
|
||||
ctx.setStatus(`Geteilt: ${marks.length} Punkte auf ${segments} Segmente`);
|
||||
},
|
||||
cancel(ctx) {
|
||||
ctx.setPreview?.(null);
|
||||
ctx.setStatus('Teilen abgebrochen');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const measureTool: ToolExtensionV2 = {
|
||||
manifest: {
|
||||
id: 'measure',
|
||||
label: 'Abmessen (Punkte)',
|
||||
icon: '⋮',
|
||||
ribbonTab: 'canvas',
|
||||
tags: ['pro'],
|
||||
description: 'Punkte im festen Abstand auf Pfad (MEASURE, Task CAD-4)',
|
||||
},
|
||||
optionsSchema: [
|
||||
{ key: 'spacing', label: 'Abstand', type: 'number', min: 1 },
|
||||
],
|
||||
handlers: {
|
||||
down(e, ctx) {
|
||||
const c = ctx as FullToolContext;
|
||||
if (!c.doc) return;
|
||||
const found = requirePathHit(c, e.world.x, e.world.y);
|
||||
if (!found) {
|
||||
ctx.setStatus('Abmessen: Polyline als Pfad anklicken');
|
||||
return;
|
||||
}
|
||||
const spacing = Math.max(1, (c.options.spacing as number | undefined) ?? 20);
|
||||
ensureDefpoints(c);
|
||||
const marks = pointsAlongPath(found.pts, 0, spacing);
|
||||
c.doc.transact(() => {
|
||||
for (const p of marks) c.doc!.addElement(makePathMarker(p));
|
||||
});
|
||||
ctx.setStatus(`Abgemessen: ${marks.length} Punkte alle ${spacing} Einheiten`);
|
||||
},
|
||||
cancel(ctx) {
|
||||
ctx.setPreview?.(null);
|
||||
ctx.setStatus('Abmessen abgebrochen');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const coreModifyPlugin: PluginV2 = {
|
||||
manifest: {
|
||||
id: 'core-modify',
|
||||
@@ -1725,5 +1871,5 @@ export const coreModifyPlugin: PluginV2 = {
|
||||
category: 'tools',
|
||||
enabledByDefault: true,
|
||||
},
|
||||
tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool, trimTool, extendTool, filletTool, arrayRectTool, arrayPolarTool, chamferTool, lengthenTool, joinTool, explodeTool, alignTool, polylineEditTool, stretchTool, breakTool, arrayPathTool],
|
||||
tools: [selectTool, hatchTool, moveTool, copyTool, rotateTool, scaleTool, mirrorTool, deleteTool, offsetTool, trimTool, extendTool, filletTool, arrayRectTool, arrayPolarTool, chamferTool, lengthenTool, joinTool, explodeTool, alignTool, polylineEditTool, stretchTool, breakTool, arrayPathTool, divideTool, measureTool],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user