2D: Bemassungs-Werkzeug nach SIA 400 B.5.3

Neue Masslinie (shape:"dimension"): zwei Punkte + Lage der Masslinie. Folgt
der Norm statt DIN-Gewohnheiten: Massbegrenzungslinien als Massstriche
(doppelte Strichstärke, keine Pfeilspitzen), Masszahl mit Komma-
Dezimaltrennzeichen, nie kopfüber lesbar (dreht sich automatisch). Die
Masszahl wird nicht gespeichert, sondern immer aus dem aktuellen Abstand der
beiden Punkte abgeleitet — bleibt beim Griff-Ziehen automatisch korrekt.

Drei Griffe (beide Endpunkte + ein Griff auf der Masslinie zum Verschieben
des Abstands), Verschieben/Rotieren, Georef-Rebase und Plan-Einpassen
entsprechend erweitert.
This commit is contained in:
2026-08-22 02:10:38 +02:00
parent ad16202c12
commit 6fd5a50d77
19 changed files with 491 additions and 0 deletions
+23
View File
@@ -1520,6 +1520,15 @@ export function drawingVertices(d: import("../model/types").Drawing2D): Vec2[] {
}
// Bezier/Spline: alle Kontroll-/Wegpunkte als Griffe (wie polyline).
if (g.shape === "bezier" || g.shape === "spline") return [...g.pts];
// Bemassung: Griff 0/1 = die beiden gemessenen Punkte (a/b); Griff 2 sitzt
// AUF der Masslinie (Mitte) — ihn zu ziehen ändert nur `offset` (s. moveGrip).
if (g.shape === "dimension") {
const dx = g.b.x - g.a.x, dy = g.b.y - g.a.y;
const len = Math.hypot(dx, dy) || 1;
const nx = (-dy / len) * g.offset, ny = (dx / len) * g.offset;
const mid: Vec2 = { x: (g.a.x + g.b.x) / 2 + nx, y: (g.a.y + g.b.y) / 2 + ny };
return [g.a, g.b, mid];
}
// Text: EIN Griff am Ankerpunkt (zum Verschieben; siehe moveGrip).
if (g.shape === "text") return [g.at];
return [];
@@ -1581,6 +1590,18 @@ function moveGrip(
const pts = g.pts.map((p, i) => (i === index ? pt : p));
return { ...d, geom: { ...g, pts } };
}
// Bemassung: Griff 0/1 verschiebt den gemessenen Punkt a/b (Masszahl
// folgt automatisch, s. Rendering); Griff 2 (auf der Masslinie) ändert
// nur `offset` — Normalabstand von `pt` zur (unveränderten) Strecke a-b.
if (g.shape === "dimension") {
if (index === 0) return { ...d, geom: { ...g, a: pt } };
if (index === 1) return { ...d, geom: { ...g, b: pt } };
const dx = g.b.x - g.a.x, dy = g.b.y - g.a.y;
const len = Math.hypot(dx, dy) || 1;
const nx = -dy / len, ny = dx / len;
const offset = (pt.x - g.a.x) * nx + (pt.y - g.a.y) * ny;
return { ...d, geom: { ...g, offset } };
}
if (g.shape === "rect" || g.shape === "image") {
const corners = [
g.min,
@@ -1674,6 +1695,8 @@ function moveElementBy(
case "bezier":
case "spline":
return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
case "dimension":
return { ...d, geom: { ...g, a: mv(g.a), b: mv(g.b) } };
case "text":
return { ...d, geom: { ...g, at: mv(g.at) } };
default:
+10
View File
@@ -544,6 +544,16 @@ function geomBBox(g: Drawing2DGeom): SelectionBBox {
case "spline":
for (const p of g.pts) acc(p.x, p.y);
break;
case "dimension": {
acc(g.a.x, g.a.y);
acc(g.b.x, g.b.y);
const dx = g.b.x - g.a.x, dy = g.b.y - g.a.y;
const len = Math.hypot(dx, dy) || 1;
const nx = (-dy / len) * g.offset, ny = (dx / len) * g.offset;
acc(g.a.x + nx, g.a.y + ny);
acc(g.b.x + nx, g.b.y + ny);
break;
}
case "text":
acc(g.at.x, g.at.y);
break;
+4
View File
@@ -54,6 +54,9 @@ function drawingsBBox(
acc(g.center.x + g.rx, g.center.y + g.ry);
} else if (g.shape === "bezier" || g.shape === "spline") {
for (const p of g.pts) acc(p.x, p.y);
} else if (g.shape === "dimension") {
acc(g.a.x, g.a.y);
acc(g.b.x, g.b.y);
} else if (g.shape === "text") acc(g.at.x, g.at.y);
}
return Number.isFinite(minX) ? { minX, minY, maxX, maxY } : null;
@@ -72,6 +75,7 @@ function translateDrawing(d: Drawing2D, dx: number, dy: number): Drawing2D {
case "ellipse": return { ...d, geom: { ...g, center: mv(g.center) } };
case "bezier": return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
case "spline": return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
case "dimension": return { ...d, geom: { ...g, a: mv(g.a), b: mv(g.b) } };
case "text": return { ...d, geom: { ...g, at: mv(g.at) } };
default: return d;
}