2D: Masskette ist jetzt EIN Element (verlängerbar + Punkte einfügbar)
Bemassungs-Modell umgebaut: eine Reihenbemassung war bisher N unabhängige Einzel-Elemente, jetzt EIN Element mit einer Punktreihe (pts statt a/b). Alle Segmente teilen weiterhin einen Offset, aber Verschieben/Selektieren/ Löschen betrifft die ganze Kette auf einmal. Ergänzen einer bestehenden Kette folgt dem "erst selektieren, dann Befehl"- Muster (wie move/copy/offset): ist beim Start eine Masskette selektiert, sortiert der erste Klick sich per Geraden-Projektion in die Kette ein — vor dem ersten Punkt (verlängert rückwärts), nach dem letzten (verlängert vorwärts) oder zwischen zwei bestehenden Punkten (fügt ein) — und der Commit ersetzt dasselbe Element statt eine Kopie zu erzeugen.
This commit is contained in:
@@ -1,15 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* `dimensionCommand` — Punktreihe (wie Polylinie) + EIN Offset-Klick →
|
* `dimensionCommand` — Punktreihe (wie Polylinie) + EIN Offset-Klick → EIN
|
||||||
* eine oder mehrere `shape:"dimension"`-Elemente (Reihenbemassung, SIA 400
|
* `shape:"dimension"`-Element für die ganze Kette (Reihenbemassung, SIA 400
|
||||||
* Figur 14). Die Masszahl wird NICHT gespeichert (aus dist(a,b) beim Rendern
|
* Figur 14). Ist beim Start bereits eine Kette selektiert, ergänzt der erste
|
||||||
|
* Klick sie (verlängert ODER fügt dazwischen ein) statt eine neue zu
|
||||||
|
* beginnen. Die Masszahl wird NICHT gespeichert (aus dist(a,b) beim Rendern
|
||||||
* abgeleitet).
|
* abgeleitet).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { dimensionCommand } from "./dimension";
|
import { dimensionCommand } from "./dimension";
|
||||||
import type { CommandContext, Project } from "../types";
|
import type { CommandContext, Project } from "../types";
|
||||||
|
import type { Drawing2D } from "../../model/types";
|
||||||
|
|
||||||
function project(): Project {
|
function project(extra: Drawing2D[] = []): Project {
|
||||||
return {
|
return {
|
||||||
id: "t",
|
id: "t",
|
||||||
name: "T",
|
name: "T",
|
||||||
@@ -27,12 +30,12 @@ function project(): Project {
|
|||||||
ceilings: [],
|
ceilings: [],
|
||||||
stairs: [],
|
stairs: [],
|
||||||
rooms: [],
|
rooms: [],
|
||||||
drawings2d: [],
|
drawings2d: extra,
|
||||||
context: [],
|
context: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCtx(p: Project): CommandContext {
|
function makeCtx(p: Project, selectedDrawingId: string | null = null): CommandContext {
|
||||||
return {
|
return {
|
||||||
project: p,
|
project: p,
|
||||||
level: p.drawingLevels[0],
|
level: p.drawingLevels[0],
|
||||||
@@ -40,11 +43,11 @@ function makeCtx(p: Project): CommandContext {
|
|||||||
activeLineStyleId: "solid",
|
activeLineStyleId: "solid",
|
||||||
activeWallTypeId: "aw",
|
activeWallTypeId: "aw",
|
||||||
lastPoint: null,
|
lastPoint: null,
|
||||||
selection: { wallIds: [], drawingId: null },
|
selection: { wallIds: [], drawingId: selectedDrawingId },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("dimensionCommand", () => {
|
describe("dimensionCommand — neu zeichnen", () => {
|
||||||
it("zwei Punkte + Enter + Offset-Klick (links positiv) → ein shape:dimension mit korrektem Vorzeichen", () => {
|
it("zwei Punkte + Enter + Offset-Klick (links positiv) → ein shape:dimension mit korrektem Vorzeichen", () => {
|
||||||
const p = project();
|
const p = project();
|
||||||
const ctx = makeCtx(p);
|
const ctx = makeCtx(p);
|
||||||
@@ -63,8 +66,7 @@ describe("dimensionCommand", () => {
|
|||||||
const g = next.drawings2d[0].geom;
|
const g = next.drawings2d[0].geom;
|
||||||
expect(g.shape).toBe("dimension");
|
expect(g.shape).toBe("dimension");
|
||||||
if (g.shape === "dimension") {
|
if (g.shape === "dimension") {
|
||||||
expect(g.a).toEqual({ x: 0, y: 0 });
|
expect(g.pts).toEqual([{ x: 0, y: 0 }, { x: 10, y: 0 }]);
|
||||||
expect(g.b).toEqual({ x: 10, y: 0 });
|
|
||||||
expect(g.offset).toBeCloseTo(2, 6);
|
expect(g.offset).toBeCloseTo(2, 6);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -85,7 +87,7 @@ describe("dimensionCommand", () => {
|
|||||||
if (g.shape === "dimension") expect(g.offset).toBe(1.5);
|
if (g.shape === "dimension") expect(g.offset).toBe(1.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Kette aus drei Punkten (Reihenbemassung) → zwei Segmente mit demselben Offset", () => {
|
it("Kette aus drei Punkten (Reihenbemassung) → EIN Element mit drei Punkten", () => {
|
||||||
const p = project();
|
const p = project();
|
||||||
const ctx = makeCtx(p);
|
const ctx = makeCtx(p);
|
||||||
let [state] = dimensionCommand.onInput(
|
let [state] = dimensionCommand.onInput(
|
||||||
@@ -98,10 +100,12 @@ describe("dimensionCommand", () => {
|
|||||||
[state] = dimensionCommand.onConfirm(state, ctx);
|
[state] = dimensionCommand.onConfirm(state, ctx);
|
||||||
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 2 }, ctx);
|
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 2 }, ctx);
|
||||||
const next = result.commit!(p);
|
const next = result.commit!(p);
|
||||||
expect(next.drawings2d.length).toBe(2);
|
expect(next.drawings2d.length).toBe(1);
|
||||||
const segs = next.drawings2d.map((d) => d.geom).filter((g) => g.shape === "dimension");
|
const g = next.drawings2d[0].geom;
|
||||||
expect(segs[0]).toMatchObject({ a: { x: 0, y: 0 }, b: { x: 4, y: 0 }, offset: 2 });
|
if (g.shape === "dimension") {
|
||||||
expect(segs[1]).toMatchObject({ a: { x: 4, y: 0 }, b: { x: 10, y: 0 }, offset: 2 });
|
expect(g.pts).toEqual([{ x: 0, y: 0 }, { x: 4, y: 0 }, { x: 10, y: 0 }]);
|
||||||
|
expect(g.offset).toBe(2);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("dritter Punkt ausserhalb der Geraden wird auf sie projiziert (keine Polylinie, immer gerade)", () => {
|
it("dritter Punkt ausserhalb der Geraden wird auf sie projiziert (keine Polylinie, immer gerade)", () => {
|
||||||
@@ -119,11 +123,10 @@ describe("dimensionCommand", () => {
|
|||||||
[state] = dimensionCommand.onConfirm(state, ctx);
|
[state] = dimensionCommand.onConfirm(state, ctx);
|
||||||
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 1 }, ctx);
|
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 1 }, ctx);
|
||||||
const next = result.commit!(p);
|
const next = result.commit!(p);
|
||||||
expect(next.drawings2d.length).toBe(2);
|
const g = next.drawings2d[0].geom;
|
||||||
const second = next.drawings2d[1].geom;
|
if (g.shape === "dimension") {
|
||||||
if (second.shape === "dimension") {
|
expect(g.pts[2].y).toBeCloseTo(0, 6); // auf die Gerade y=0 projiziert
|
||||||
expect(second.b.y).toBeCloseTo(0, 6); // auf die Gerade y=0 projiziert
|
expect(g.pts[2].x).toBeCloseTo(10, 6); // x-Komponente bleibt (Projektion entlang x)
|
||||||
expect(second.b.x).toBeCloseTo(10, 6); // x-Komponente bleibt (Projektion entlang x)
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,13 +144,11 @@ describe("dimensionCommand", () => {
|
|||||||
[state] = dimensionCommand.onConfirm(state, ctx);
|
[state] = dimensionCommand.onConfirm(state, ctx);
|
||||||
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 1 }, ctx);
|
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 1 }, ctx);
|
||||||
const next = result.commit!(p);
|
const next = result.commit!(p);
|
||||||
// Nach Zurück nur noch [0,0]→[4,0] übrig → ein Segment.
|
|
||||||
expect(next.drawings2d.length).toBe(1);
|
|
||||||
const g = next.drawings2d[0].geom;
|
const g = next.drawings2d[0].geom;
|
||||||
if (g.shape === "dimension") expect(g.b).toEqual({ x: 4, y: 0 });
|
if (g.shape === "dimension") expect(g.pts).toEqual([{ x: 0, y: 0 }, { x: 4, y: 0 }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("entartetes Segment (identische Punkte) innerhalb der Kette wird übersprungen", () => {
|
it("entartete Kette (identische Punkte) committet nichts", () => {
|
||||||
const p = project();
|
const p = project();
|
||||||
const ctx = makeCtx(p);
|
const ctx = makeCtx(p);
|
||||||
let [state] = dimensionCommand.onInput(
|
let [state] = dimensionCommand.onInput(
|
||||||
@@ -175,3 +176,88 @@ describe("dimensionCommand", () => {
|
|||||||
expect(result.done).toBe(true);
|
expect(result.done).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("dimensionCommand — bestehende Kette ergänzen (erst selektieren, dann Befehl)", () => {
|
||||||
|
function chain(pts: { x: number; y: number }[], offset = 1): Drawing2D {
|
||||||
|
return {
|
||||||
|
id: "dim-existing",
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: "eg",
|
||||||
|
categoryCode: "20",
|
||||||
|
geom: { shape: "dimension", pts, offset },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it("Klick jenseits des letzten Punkts verlängert die Kette (dasselbe Element)", () => {
|
||||||
|
const existing = chain([{ x: 0, y: 0 }, { x: 4, y: 0 }]);
|
||||||
|
const p = project([existing]);
|
||||||
|
const ctx = makeCtx(p, "dim-existing");
|
||||||
|
let [state] = dimensionCommand.onInput(
|
||||||
|
dimensionCommand.init(),
|
||||||
|
{ kind: "point", point: { x: 10, y: 0 } },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
[state] = dimensionCommand.onConfirm(state, ctx);
|
||||||
|
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 1 }, ctx);
|
||||||
|
const next = result.commit!(p);
|
||||||
|
expect(next.drawings2d.length).toBe(1); // kein neues Element
|
||||||
|
expect(next.drawings2d[0].id).toBe("dim-existing");
|
||||||
|
const g = next.drawings2d[0].geom;
|
||||||
|
if (g.shape === "dimension") {
|
||||||
|
expect(g.pts).toEqual([{ x: 0, y: 0 }, { x: 4, y: 0 }, { x: 10, y: 0 }]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Klick vor dem ersten Punkt verlängert rückwärts (wird vorangestellt)", () => {
|
||||||
|
const existing = chain([{ x: 4, y: 0 }, { x: 10, y: 0 }]);
|
||||||
|
const p = project([existing]);
|
||||||
|
const ctx = makeCtx(p, "dim-existing");
|
||||||
|
let [state] = dimensionCommand.onInput(
|
||||||
|
dimensionCommand.init(),
|
||||||
|
{ kind: "point", point: { x: 0, y: 0 } },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
[state] = dimensionCommand.onConfirm(state, ctx);
|
||||||
|
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 1 }, ctx);
|
||||||
|
const next = result.commit!(p);
|
||||||
|
const g = next.drawings2d[0].geom;
|
||||||
|
if (g.shape === "dimension") {
|
||||||
|
expect(g.pts).toEqual([{ x: 0, y: 0 }, { x: 4, y: 0 }, { x: 10, y: 0 }]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Klick zwischen zwei bestehenden Punkten fügt DAZWISCHEN ein", () => {
|
||||||
|
const existing = chain([{ x: 0, y: 0 }, { x: 10, y: 0 }]);
|
||||||
|
const p = project([existing]);
|
||||||
|
const ctx = makeCtx(p, "dim-existing");
|
||||||
|
let [state] = dimensionCommand.onInput(
|
||||||
|
dimensionCommand.init(),
|
||||||
|
{ kind: "point", point: { x: 4, y: 0 } },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
[state] = dimensionCommand.onConfirm(state, ctx);
|
||||||
|
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 1 }, ctx);
|
||||||
|
const next = result.commit!(p);
|
||||||
|
expect(next.drawings2d.length).toBe(1);
|
||||||
|
const g = next.drawings2d[0].geom;
|
||||||
|
if (g.shape === "dimension") {
|
||||||
|
expect(g.pts).toEqual([{ x: 0, y: 0 }, { x: 4, y: 0 }, { x: 10, y: 0 }]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keine Selektion (drawingId null) startet stattdessen eine neue Kette", () => {
|
||||||
|
const existing = chain([{ x: 0, y: 0 }, { x: 4, y: 0 }]);
|
||||||
|
const p = project([existing]);
|
||||||
|
const ctx = makeCtx(p, null);
|
||||||
|
let [state] = dimensionCommand.onInput(
|
||||||
|
dimensionCommand.init(),
|
||||||
|
{ kind: "point", point: { x: 20, y: 20 } },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
[state] = dimensionCommand.onInput(state, { kind: "point", point: { x: 25, y: 20 } }, ctx);
|
||||||
|
[state] = dimensionCommand.onConfirm(state, ctx);
|
||||||
|
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 1 }, ctx);
|
||||||
|
const next = result.commit!(p);
|
||||||
|
expect(next.drawings2d.length).toBe(2); // bestehendes Element bleibt, neues kommt dazu
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,14 +4,20 @@
|
|||||||
// Klick setzt den Startpunkt, der zweite legt die Richtung fest — jeder
|
// Klick setzt den Startpunkt, der zweite legt die Richtung fest — jeder
|
||||||
// weitere Klick wird auf diese Gerade projiziert (Reihenbemassung misst
|
// weitere Klick wird auf diese Gerade projiziert (Reihenbemassung misst
|
||||||
// mehrere Punkte entlang EINER Referenzlinie, z. B. einer Wand, nie einen
|
// mehrere Punkte entlang EINER Referenzlinie, z. B. einer Wand, nie einen
|
||||||
// Zickzack-Pfad). Schritte:
|
// Zickzack-Pfad). Die ganze Kette ist EIN Element (`pts.length` ≥ 2).
|
||||||
|
//
|
||||||
|
// Neu zeichnen:
|
||||||
// 1) „Erster Punkt:" → Punkt
|
// 1) „Erster Punkt:" → Punkt
|
||||||
// 2) „Nächster Punkt ( Fertig Zurück ):" → Punkt … (ab dem 3. Punkt auf die
|
// 2) „Nächster Punkt ( Fertig Zurück ):" → Punkt … (ab dem 3. Punkt auf die
|
||||||
// durch Punkt 1/2 definierte Gerade projiziert)
|
// durch Punkt 1/2 definierte Gerade projiziert)
|
||||||
// 3) „Lage der Masslinie:" → Punkt/Zahl (EIN Normalabstand
|
// 3) „Lage der Masslinie:" → Punkt/Zahl → commit neues Element
|
||||||
// für ALLE Segmente) → commit: je aufeinanderfolgendem Punktpaar EIN
|
//
|
||||||
// `shape:"dimension"`-Element (unabhängig editierbar, teilt nur den
|
// Ergänzen einer bestehenden Kette (Rhino-Muster: erst selektieren, dann
|
||||||
// beim Zeichnen gewählten Offset).
|
// Befehl — s. `ctx.selection`): ist beim Start eine `dimension`-Kette
|
||||||
|
// gewählt, lädt der Befehl deren Punkte/Offset vor und der erste Klick wird
|
||||||
|
// per Geraden-Projektion in die Kette EINSORTIERT (`insertIntoChain`) — vor
|
||||||
|
// dem ersten, nach dem letzten (verlängert) ODER zwischen zwei bestehenden
|
||||||
|
// Punkten (fügt ein). Der Commit ersetzt dasselbe Element (keine Kopie).
|
||||||
//
|
//
|
||||||
// Die Masszahl wird beim Rendern aus dist(a,b) abgeleitet (nicht gespeichert)
|
// Die Masszahl wird beim Rendern aus dist(a,b) abgeleitet (nicht gespeichert)
|
||||||
// — bleibt beim späteren Griff-Ziehen automatisch korrekt.
|
// — bleibt beim späteren Griff-Ziehen automatisch korrekt.
|
||||||
@@ -52,6 +58,37 @@ function projectOnLine(p0: Vec2, p1: Vec2, pt: Vec2): Vec2 {
|
|||||||
return { x: p0.x + dx * t, y: p0.y + dy * t };
|
return { x: p0.x + dx * t, y: p0.y + dy * t };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sortiert `raw` (auf die Kettengerade projiziert) an die richtige Stelle in
|
||||||
|
* `pts` ein — VOR dem ersten (verlängert rückwärts), NACH dem letzten
|
||||||
|
* (verlängert vorwärts) oder ZWISCHEN zwei bestehenden Punkten (fügt ein).
|
||||||
|
* `pts` muss bereits kollinear sein (≥ 2 Punkte); die Skalarposition entlang
|
||||||
|
* der durch `pts[0]`/`pts[1]` festgelegten Geraden entscheidet die Reihenfolge.
|
||||||
|
*/
|
||||||
|
function insertIntoChain(pts: Vec2[], raw: Vec2): Vec2[] {
|
||||||
|
if (pts.length < 2) return [...pts, raw];
|
||||||
|
const p0 = pts[0], p1 = pts[1];
|
||||||
|
const dx = p1.x - p0.x, dy = p1.y - p0.y;
|
||||||
|
const len2 = dx * dx + dy * dy || 1;
|
||||||
|
const tOf = (p: Vec2): number => ((p.x - p0.x) * dx + (p.y - p0.y) * dy) / len2;
|
||||||
|
const placed = projectOnLine(p0, p1, raw);
|
||||||
|
const newT = tOf(placed);
|
||||||
|
let idx = pts.findIndex((p) => newT < tOf(p));
|
||||||
|
if (idx === -1) idx = pts.length;
|
||||||
|
const out = [...pts];
|
||||||
|
out.splice(idx, 0, placed);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bestehende, selektierte Masskette (Rhino: erst selektieren, dann Befehl) oder null. */
|
||||||
|
function selectedChain(ctx: CommandContext): { id: string; pts: Vec2[]; offset: number } | null {
|
||||||
|
const id = ctx.selection.drawingId;
|
||||||
|
if (!id) return null;
|
||||||
|
const d = ctx.project.drawings2d.find((x) => x.id === id);
|
||||||
|
if (!d || d.geom.shape !== "dimension") return null;
|
||||||
|
return { id: d.id, pts: d.geom.pts, offset: d.geom.offset };
|
||||||
|
}
|
||||||
|
|
||||||
/** Endpunkte der um `offset` parallel verschobenen Masslinie (Strecke a-b). */
|
/** Endpunkte der um `offset` parallel verschobenen Masslinie (Strecke a-b). */
|
||||||
function offsetPoints(a: Vec2, b: Vec2, offset: number): { a: Vec2; b: Vec2 } {
|
function offsetPoints(a: Vec2, b: Vec2, offset: number): { a: Vec2; b: Vec2 } {
|
||||||
const dx = b.x - a.x, dy = b.y - a.y;
|
const dx = b.x - a.x, dy = b.y - a.y;
|
||||||
@@ -67,11 +104,14 @@ interface DimNext extends CommandState {
|
|||||||
phase: "next";
|
phase: "next";
|
||||||
pts: Vec2[];
|
pts: Vec2[];
|
||||||
cursor: Vec2 | null;
|
cursor: Vec2 | null;
|
||||||
|
/** ID der bearbeiteten Kette (Ergänzen-Modus) — null = neue Kette. */
|
||||||
|
editId: string | null;
|
||||||
}
|
}
|
||||||
interface DimOffset extends CommandState {
|
interface DimOffset extends CommandState {
|
||||||
phase: "offset";
|
phase: "offset";
|
||||||
pts: Vec2[];
|
pts: Vec2[];
|
||||||
cursor: Vec2 | null;
|
cursor: Vec2 | null;
|
||||||
|
editId: string | null;
|
||||||
}
|
}
|
||||||
type DimState = DimStart | DimNext | DimOffset;
|
type DimState = DimStart | DimNext | DimOffset;
|
||||||
|
|
||||||
@@ -110,21 +150,26 @@ function offsetDraft(pts: Vec2[], offset: number, at: Vec2 | null): ToolDraft {
|
|||||||
return draft;
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendDimensionChain(p: Project, pts: Vec2[], offset: number, ctx: CommandContext): Project {
|
/** Committet eine Kette: neu anhängen (editId null) ODER dasselbe Element ersetzen. */
|
||||||
const added: Drawing2D[] = [];
|
function commitChain(p: Project, pts: Vec2[], offset: number, editId: string | null, ctx: CommandContext): Project {
|
||||||
for (let i = 0; i < pts.length - 1; i++) {
|
const cleaned = pts.filter((pt, i) => i === 0 || dist(pts[i - 1], pt) >= EPS);
|
||||||
const a = pts[i], b = pts[i + 1];
|
if (cleaned.length < 2) return p; // zu wenig gültige Punkte → nichts committen/ändern
|
||||||
if (dist(a, b) < EPS) continue;
|
if (editId) {
|
||||||
added.push({
|
return {
|
||||||
|
...p,
|
||||||
|
drawings2d: p.drawings2d.map((d) =>
|
||||||
|
d.id === editId ? { ...d, geom: { shape: "dimension", pts: cleaned, offset } } : d,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const d: Drawing2D = {
|
||||||
id: uniqueId("dr2d"),
|
id: uniqueId("dr2d"),
|
||||||
type: "drawing2d",
|
type: "drawing2d",
|
||||||
levelId: ctx.level.id,
|
levelId: ctx.level.id,
|
||||||
categoryCode: ctx.defaultCategoryCode,
|
categoryCode: ctx.defaultCategoryCode,
|
||||||
geom: { shape: "dimension", a, b, offset },
|
geom: { shape: "dimension", pts: cleaned, offset },
|
||||||
});
|
};
|
||||||
}
|
return { ...p, drawings2d: [...p.drawings2d, d] };
|
||||||
if (added.length === 0) return p;
|
|
||||||
return { ...p, drawings2d: [...p.drawings2d, ...added] };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const idle = (): [CommandState, CommandResult] => [
|
const idle = (): [CommandState, CommandResult] => [
|
||||||
@@ -154,11 +199,11 @@ export const dimensionCommand: Command = {
|
|||||||
if (input.id === "undo") {
|
if (input.id === "undo") {
|
||||||
const pts = s.pts.slice(0, -1);
|
const pts = s.pts.slice(0, -1);
|
||||||
if (pts.length === 0) return [{ phase: "start", lastPoint: null }, { draft: null }];
|
if (pts.length === 0) return [{ phase: "start", lastPoint: null }, { draft: null }];
|
||||||
const ns: DimNext = { phase: "next", pts, cursor: s.cursor, lastPoint: pts[pts.length - 1] };
|
const ns: DimNext = { phase: "next", pts, cursor: s.cursor, editId: s.editId, lastPoint: pts[pts.length - 1] };
|
||||||
return [ns, { draft: chainDraft(pts, s.cursor) }];
|
return [ns, { draft: chainDraft(pts, s.cursor) }];
|
||||||
}
|
}
|
||||||
if (input.id === "finish" && s.pts.length >= 2) {
|
if (input.id === "finish" && s.pts.length >= 2) {
|
||||||
const ns: DimOffset = { phase: "offset", pts: s.pts, cursor: s.cursor, lastPoint: s.lastPoint };
|
const ns: DimOffset = { phase: "offset", pts: s.pts, cursor: s.cursor, editId: s.editId, lastPoint: s.lastPoint };
|
||||||
return [ns, { draft: offsetDraft(s.pts, 0, null) }];
|
return [ns, { draft: offsetDraft(s.pts, 0, null) }];
|
||||||
}
|
}
|
||||||
return [s, { draft: chainDraft(s.pts, s.cursor) }];
|
return [s, { draft: chainDraft(s.pts, s.cursor) }];
|
||||||
@@ -168,14 +213,19 @@ export const dimensionCommand: Command = {
|
|||||||
if (input.kind !== "point") return [s, { draft: null }];
|
if (input.kind !== "point") return [s, { draft: null }];
|
||||||
const pt = input.point;
|
const pt = input.point;
|
||||||
if (s.phase !== "next") {
|
if (s.phase !== "next") {
|
||||||
const ns: DimNext = { phase: "next", pts: [pt], cursor: pt, lastPoint: pt };
|
// Erster Klick: bei selektierter Kette wird er EINSORTIERT (Ergänzen);
|
||||||
return [ns, { draft: chainDraft([pt], pt) }];
|
// sonst startet er eine neue Kette.
|
||||||
|
const existing = selectedChain(ctx);
|
||||||
|
const pts = existing ? insertIntoChain(existing.pts, pt) : [pt];
|
||||||
|
const editId = existing ? existing.id : null;
|
||||||
|
const ns: DimNext = { phase: "next", pts, cursor: pt, editId, lastPoint: pt };
|
||||||
|
return [ns, { draft: chainDraft(pts, pt) }];
|
||||||
}
|
}
|
||||||
// Ab dem dritten Punkt: auf die durch Punkt 1/2 festgelegte Gerade projizieren.
|
// Ab dem dritten Punkt (bzw. jedem weiteren Klick im Ergänzen-Modus):
|
||||||
const placed = s.pts.length >= 2 ? projectOnLine(s.pts[0], s.pts[1], pt) : pt;
|
// auf die durch Punkt 1/2 festgelegte Gerade projizieren/einsortieren.
|
||||||
const pts = [...s.pts, placed];
|
const pts = s.pts.length >= 2 ? insertIntoChain(s.pts, pt) : [...s.pts, pt];
|
||||||
const ns: DimNext = { phase: "next", pts, cursor: placed, lastPoint: placed };
|
const ns: DimNext = { phase: "next", pts, cursor: pt, editId: s.editId, lastPoint: pt };
|
||||||
return [ns, { draft: chainDraft(pts, placed) }];
|
return [ns, { draft: chainDraft(pts, pt) }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Offset-Schritt: Zahl = direkter Abstand, Punkt = Abstand zum Cursor.
|
// Offset-Schritt: Zahl = direkter Abstand, Punkt = Abstand zum Cursor.
|
||||||
@@ -184,9 +234,10 @@ export const dimensionCommand: Command = {
|
|||||||
else if (input.kind === "point") offset = signedOffset(s.pts[0], s.pts[1], input.point);
|
else if (input.kind === "point") offset = signedOffset(s.pts[0], s.pts[1], input.point);
|
||||||
else return [s, { draft: offsetDraft(s.pts, 0, null) }];
|
else return [s, { draft: offsetDraft(s.pts, 0, null) }];
|
||||||
const pts = s.pts;
|
const pts = s.pts;
|
||||||
|
const editId = s.editId;
|
||||||
return [
|
return [
|
||||||
{ phase: "start", lastPoint: null },
|
{ phase: "start", lastPoint: null },
|
||||||
{ draft: null, done: true, commit: (p) => appendDimensionChain(p, pts, offset, ctx) },
|
{ draft: null, done: true, commit: (p) => commitChain(p, pts, offset, editId, ctx) },
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -212,7 +263,7 @@ export const dimensionCommand: Command = {
|
|||||||
onConfirm: (state): [CommandState, CommandResult] => {
|
onConfirm: (state): [CommandState, CommandResult] => {
|
||||||
const s = state as DimState;
|
const s = state as DimState;
|
||||||
if (s.phase === "next" && s.pts.length >= 2) {
|
if (s.phase === "next" && s.pts.length >= 2) {
|
||||||
const ns: DimOffset = { phase: "offset", pts: s.pts, cursor: s.cursor, lastPoint: s.lastPoint };
|
const ns: DimOffset = { phase: "offset", pts: s.pts, cursor: s.cursor, editId: s.editId, lastPoint: s.lastPoint };
|
||||||
return [ns, { draft: offsetDraft(s.pts, 0, null) }];
|
return [ns, { draft: offsetDraft(s.pts, 0, null) }];
|
||||||
}
|
}
|
||||||
return idle();
|
return idle();
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export function translateProject(project: Project, dx: number, dy: number): Proj
|
|||||||
case "spline":
|
case "spline":
|
||||||
return { ...d, geom: { ...g, pts: shiftAll(g.pts, dx, dy) } };
|
return { ...d, geom: { ...g, pts: shiftAll(g.pts, dx, dy) } };
|
||||||
case "dimension":
|
case "dimension":
|
||||||
return { ...d, geom: { ...g, a: shift(g.a, dx, dy), b: shift(g.b, dx, dy) } };
|
return { ...d, geom: { ...g, pts: shiftAll(g.pts, dx, dy) } };
|
||||||
case "kote":
|
case "kote":
|
||||||
return { ...d, geom: { ...g, at: shift(g.at, dx, dy) } };
|
return { ...d, geom: { ...g, at: shift(g.at, dx, dy) } };
|
||||||
case "text":
|
case "text":
|
||||||
|
|||||||
+9
-1
@@ -1533,7 +1533,15 @@ export type Drawing2DGeom =
|
|||||||
* beim Verschieben der Griffe automatisch korrekt. Rein 2D (kein Bezug auf
|
* beim Verschieben der Griffe automatisch korrekt. Rein 2D (kein Bezug auf
|
||||||
* andere Elemente — Assoziativität nur über die eigenen Endpunkte).
|
* andere Elemente — Assoziativität nur über die eigenen Endpunkte).
|
||||||
*/
|
*/
|
||||||
| { shape: "dimension"; a: Vec2; b: Vec2; offset: number }
|
/**
|
||||||
|
* `pts.length` ≥ 2, IMMER kollinear (Werkzeug erzwingt das beim Zeichnen/
|
||||||
|
* Ergänzen — eine Masskette ist eine Gerade, kein Zickzack-Pfad). Jedes
|
||||||
|
* aufeinanderfolgende Punktpaar ist EIN gemessenes Segment (Reihenbemassung,
|
||||||
|
* SIA 400 Figur 14), alle teilen denselben `offset`. EIN Element für die
|
||||||
|
* ganze Kette (nicht mehrere) — spätere Bearbeitung kann Punkte ergänzen
|
||||||
|
* (verlängern ODER dazwischen einfügen, s. `commands/cmds/dimension.ts`).
|
||||||
|
*/
|
||||||
|
| { shape: "dimension"; pts: Vec2[]; offset: number }
|
||||||
/**
|
/**
|
||||||
* Höhenkote (SIA 400 B.5.4, Figur 18): Dreieck-Symbol + Höhenwert (Meter,
|
* Höhenkote (SIA 400 B.5.4, Figur 18): Dreieck-Symbol + Höhenwert (Meter,
|
||||||
* ±0.00-Referenz frei wählbar, s. Norm) an einem Punkt. `kind` bestimmt
|
* ±0.00-Referenz frei wählbar, s. Norm) an einem Punkt. `kind` bestimmt
|
||||||
|
|||||||
@@ -730,8 +730,7 @@ function computeBounds(
|
|||||||
} else if (g.shape === "bezier" || g.shape === "spline") {
|
} else if (g.shape === "bezier" || g.shape === "spline") {
|
||||||
g.pts.forEach(acc);
|
g.pts.forEach(acc);
|
||||||
} else if (g.shape === "dimension") {
|
} else if (g.shape === "dimension") {
|
||||||
acc(g.a);
|
g.pts.forEach(acc);
|
||||||
acc(g.b);
|
|
||||||
} else if (g.shape === "kote") acc(g.at);
|
} else if (g.shape === "kote") acc(g.at);
|
||||||
else if (g.shape === "text") acc(g.at);
|
else if (g.shape === "text") acc(g.at);
|
||||||
else if (g.shape === "image") { acc(g.min); acc(g.max); }
|
else if (g.shape === "image") { acc(g.min); acc(g.max); }
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ describe("addDrawing2D — dimension (SIA 400 B.5.3)", () => {
|
|||||||
type: "drawing2d",
|
type: "drawing2d",
|
||||||
levelId: "eg",
|
levelId: "eg",
|
||||||
categoryCode: "20",
|
categoryCode: "20",
|
||||||
geom: { shape: "dimension", a: { x: 0, y: 0 }, b: { x: 4, y: 0 }, offset: 1 },
|
geom: { shape: "dimension", pts: [{ x: 0, y: 0 }, { x: 4, y: 0 }], offset: 1 },
|
||||||
};
|
};
|
||||||
const out: Primitive[] = [];
|
const out: Primitive[] = [];
|
||||||
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
||||||
@@ -152,7 +152,7 @@ describe("addDrawing2D — dimension (SIA 400 B.5.3)", () => {
|
|||||||
type: "drawing2d",
|
type: "drawing2d",
|
||||||
levelId: "eg",
|
levelId: "eg",
|
||||||
categoryCode: "20",
|
categoryCode: "20",
|
||||||
geom: { shape: "dimension", a: { x: 0, y: 0 }, b: { x: 3, y: 4 }, offset: 0.5 }, // 3-4-5-Dreieck → 5m
|
geom: { shape: "dimension", pts: [{ x: 0, y: 0 }, { x: 3, y: 4 }], offset: 0.5 }, // 3-4-5-Dreieck → 5m
|
||||||
};
|
};
|
||||||
const out: Primitive[] = [];
|
const out: Primitive[] = [];
|
||||||
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
||||||
@@ -167,7 +167,7 @@ describe("addDrawing2D — dimension (SIA 400 B.5.3)", () => {
|
|||||||
type: "drawing2d",
|
type: "drawing2d",
|
||||||
levelId: "eg",
|
levelId: "eg",
|
||||||
categoryCode: "20",
|
categoryCode: "20",
|
||||||
geom: { shape: "dimension", a: { x: 2, y: 2 }, b: { x: 2, y: 2 }, offset: 1 },
|
geom: { shape: "dimension", pts: [{ x: 2, y: 2 }, { x: 2, y: 2 }], offset: 1 },
|
||||||
};
|
};
|
||||||
const out: Primitive[] = [];
|
const out: Primitive[] = [];
|
||||||
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
||||||
|
|||||||
@@ -30,6 +30,77 @@ function formatDimLabel(m: number): string {
|
|||||||
return m.toFixed(2);
|
return m.toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein einzelnes Mass-Segment (Masshilfslinien + Masslinie + Massstriche +
|
||||||
|
* Masszahl) zwischen `a` und `b` bei gegebenem `offset` — s. `formatDimLabel`.
|
||||||
|
* Extrahiert, damit eine Masskette (`shape:"dimension"`, `pts.length` ≥ 2)
|
||||||
|
* je aufeinanderfolgendem Punktpaar dasselbe Rendering wiederverwenden kann.
|
||||||
|
*/
|
||||||
|
function addDimensionSegment(
|
||||||
|
out: Primitive[],
|
||||||
|
a: Vec2,
|
||||||
|
b: Vec2,
|
||||||
|
offset: number,
|
||||||
|
weightMm: number,
|
||||||
|
dash: number[] | null,
|
||||||
|
color: string,
|
||||||
|
greyed: boolean,
|
||||||
|
drawingId: string,
|
||||||
|
): void {
|
||||||
|
const dx = b.x - a.x, dy = b.y - a.y;
|
||||||
|
const lineLen = Math.hypot(dx, dy);
|
||||||
|
if (lineLen < 1e-6) return;
|
||||||
|
const ux = dx / lineLen, uy = dy / lineLen; // Einheitsrichtung a→b
|
||||||
|
const nx = -uy, ny = ux; // Links-Normale (Einheit) — Richtung des Offsets
|
||||||
|
const sign = offset >= 0 ? 1 : -1;
|
||||||
|
const dimA: Vec2 = { x: a.x + nx * offset, y: a.y + ny * offset };
|
||||||
|
const dimB: Vec2 = { x: b.x + nx * offset, y: b.y + ny * offset };
|
||||||
|
const extLine = (from: Vec2, to: Vec2) =>
|
||||||
|
out.push({ kind: "line", a: from, b: to, cls: "draw2d", weightMm, dash, color, greyed, drawingId });
|
||||||
|
extLine(
|
||||||
|
{ x: a.x + nx * DIM_GAP_M * sign, y: a.y + ny * DIM_GAP_M * sign },
|
||||||
|
{ x: dimA.x + nx * DIM_OVERSHOOT_M * sign, y: dimA.y + ny * DIM_OVERSHOOT_M * sign },
|
||||||
|
);
|
||||||
|
extLine(
|
||||||
|
{ x: b.x + nx * DIM_GAP_M * sign, y: b.y + ny * DIM_GAP_M * sign },
|
||||||
|
{ x: dimB.x + nx * DIM_OVERSHOOT_M * sign, y: dimB.y + ny * DIM_OVERSHOOT_M * sign },
|
||||||
|
);
|
||||||
|
extLine(dimA, dimB);
|
||||||
|
// Massstriche: 45°-Diagonale (Winkelhalbierende von Richtung/Normale), an
|
||||||
|
// beiden Enden PARALLEL (nicht gespiegelt) — wie in SIA 400 Figur 13.
|
||||||
|
const tx = (ux + nx) / Math.SQRT2, ty = (uy + ny) / Math.SQRT2;
|
||||||
|
const half = DIM_TICK_LEN_M / 2;
|
||||||
|
const tick = (pt: Vec2) =>
|
||||||
|
out.push({
|
||||||
|
kind: "line",
|
||||||
|
a: { x: pt.x - tx * half, y: pt.y - ty * half },
|
||||||
|
b: { x: pt.x + tx * half, y: pt.y + ty * half },
|
||||||
|
cls: "draw2d",
|
||||||
|
weightMm: weightMm * 2,
|
||||||
|
dash: null,
|
||||||
|
color,
|
||||||
|
greyed,
|
||||||
|
drawingId,
|
||||||
|
});
|
||||||
|
tick(dimA);
|
||||||
|
tick(dimB);
|
||||||
|
// Masszahl: Mitte der Masslinie, leicht abgesetzt, nie kopfüber lesbar.
|
||||||
|
let angle = Math.atan2(dy, dx);
|
||||||
|
if (Math.cos(angle) < 0) angle += Math.PI;
|
||||||
|
const upx = -Math.sin(angle), upy = Math.cos(angle); // Links-Normale der ENDGÜLTIGEN Richtung
|
||||||
|
const mid: Vec2 = { x: (dimA.x + dimB.x) / 2, y: (dimA.y + dimB.y) / 2 };
|
||||||
|
out.push({
|
||||||
|
kind: "drawingText",
|
||||||
|
at: { x: mid.x + upx * DIM_TEXT_HEIGHT_M * 0.6, y: mid.y + upy * DIM_TEXT_HEIGHT_M * 0.6 },
|
||||||
|
text: formatDimLabel(lineLen),
|
||||||
|
heightM: DIM_TEXT_HEIGHT_M,
|
||||||
|
angle,
|
||||||
|
color,
|
||||||
|
greyed,
|
||||||
|
drawingId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Höhenkote (SIA 400 B.5.4) ────────────────────────────────────────────────
|
// ── Höhenkote (SIA 400 B.5.4) ────────────────────────────────────────────────
|
||||||
const KOTE_TRI_SIZE_M = 0.15; // Dreieck-Grösse (Spitze bis Basis)
|
const KOTE_TRI_SIZE_M = 0.15; // Dreieck-Grösse (Spitze bis Basis)
|
||||||
const KOTE_TRI_HALF_W_M = KOTE_TRI_SIZE_M * 0.6;
|
const KOTE_TRI_HALF_W_M = KOTE_TRI_SIZE_M * 0.6;
|
||||||
@@ -185,62 +256,11 @@ export function addDrawing2D(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "dimension": {
|
case "dimension": {
|
||||||
// SIA 400 B.5.3: Masshilfslinien + Masslinie in der dünnsten Linie;
|
// SIA 400 B.5.3, Figur 14 (Reihenbemassung): jedes aufeinanderfolgende
|
||||||
// Massbegrenzungslinien (Massstriche) doppelt so dick; Masszahl aus
|
// Punktpaar der Kette ist ein eigenes Mass-Segment, alle teilen `offset`.
|
||||||
// dist(a,b) — nicht gespeichert, folgt Griff-Änderungen automatisch.
|
for (let i = 0; i < g.pts.length - 1; i++) {
|
||||||
const dx = g.b.x - g.a.x, dy = g.b.y - g.a.y;
|
addDimensionSegment(out, g.pts[i], g.pts[i + 1], g.offset, weightMm, dash, color, greyed, d.id);
|
||||||
const lineLen = Math.hypot(dx, dy);
|
}
|
||||||
if (lineLen < 1e-6) break;
|
|
||||||
const ux = dx / lineLen, uy = dy / lineLen; // Einheitsrichtung a→b
|
|
||||||
const nx = -uy, ny = ux; // Links-Normale (Einheit) — Richtung des Offsets
|
|
||||||
const off = g.offset;
|
|
||||||
const sign = off >= 0 ? 1 : -1;
|
|
||||||
const dimA: Vec2 = { x: g.a.x + nx * off, y: g.a.y + ny * off };
|
|
||||||
const dimB: Vec2 = { x: g.b.x + nx * off, y: g.b.y + ny * off };
|
|
||||||
const extLine = (from: Vec2, to: Vec2) =>
|
|
||||||
out.push({ kind: "line", a: from, b: to, cls: "draw2d", weightMm, dash, color, greyed, drawingId: d.id });
|
|
||||||
extLine(
|
|
||||||
{ x: g.a.x + nx * DIM_GAP_M * sign, y: g.a.y + ny * DIM_GAP_M * sign },
|
|
||||||
{ x: dimA.x + nx * DIM_OVERSHOOT_M * sign, y: dimA.y + ny * DIM_OVERSHOOT_M * sign },
|
|
||||||
);
|
|
||||||
extLine(
|
|
||||||
{ x: g.b.x + nx * DIM_GAP_M * sign, y: g.b.y + ny * DIM_GAP_M * sign },
|
|
||||||
{ x: dimB.x + nx * DIM_OVERSHOOT_M * sign, y: dimB.y + ny * DIM_OVERSHOOT_M * sign },
|
|
||||||
);
|
|
||||||
extLine(dimA, dimB);
|
|
||||||
// Massstriche: 45°-Diagonale (Winkelhalbierende von Richtung/Normale),
|
|
||||||
// an beiden Enden PARALLEL (nicht gespiegelt) — wie in SIA 400 Figur 13.
|
|
||||||
const tx = (ux + nx) / Math.SQRT2, ty = (uy + ny) / Math.SQRT2;
|
|
||||||
const half = DIM_TICK_LEN_M / 2;
|
|
||||||
const tick = (pt: Vec2) =>
|
|
||||||
out.push({
|
|
||||||
kind: "line",
|
|
||||||
a: { x: pt.x - tx * half, y: pt.y - ty * half },
|
|
||||||
b: { x: pt.x + tx * half, y: pt.y + ty * half },
|
|
||||||
cls: "draw2d",
|
|
||||||
weightMm: weightMm * 2,
|
|
||||||
dash: null,
|
|
||||||
color,
|
|
||||||
greyed,
|
|
||||||
drawingId: d.id,
|
|
||||||
});
|
|
||||||
tick(dimA);
|
|
||||||
tick(dimB);
|
|
||||||
// Masszahl: Mitte der Masslinie, leicht abgesetzt, nie kopfüber lesbar.
|
|
||||||
let angle = Math.atan2(dy, dx);
|
|
||||||
if (Math.cos(angle) < 0) angle += Math.PI;
|
|
||||||
const upx = -Math.sin(angle), upy = Math.cos(angle); // Links-Normale der ENDGÜLTIGEN Richtung
|
|
||||||
const mid: Vec2 = { x: (dimA.x + dimB.x) / 2, y: (dimA.y + dimB.y) / 2 };
|
|
||||||
out.push({
|
|
||||||
kind: "drawingText",
|
|
||||||
at: { x: mid.x + upx * DIM_TEXT_HEIGHT_M * 0.6, y: mid.y + upy * DIM_TEXT_HEIGHT_M * 0.6 },
|
|
||||||
text: formatDimLabel(lineLen),
|
|
||||||
heightM: DIM_TEXT_HEIGHT_M,
|
|
||||||
angle,
|
|
||||||
color,
|
|
||||||
greyed,
|
|
||||||
drawingId: d.id,
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "kote": {
|
case "kote": {
|
||||||
|
|||||||
+19
-14
@@ -1520,14 +1520,16 @@ export function drawingVertices(d: import("../model/types").Drawing2D): Vec2[] {
|
|||||||
}
|
}
|
||||||
// Bezier/Spline: alle Kontroll-/Wegpunkte als Griffe (wie polyline).
|
// Bezier/Spline: alle Kontroll-/Wegpunkte als Griffe (wie polyline).
|
||||||
if (g.shape === "bezier" || g.shape === "spline") return [...g.pts];
|
if (g.shape === "bezier" || g.shape === "spline") return [...g.pts];
|
||||||
// Bemassung: Griff 0/1 = die beiden gemessenen Punkte (a/b); Griff 2 sitzt
|
// Bemassung: Griff 0..N-1 = jeder Punkt der Kette (wie polyline); EIN
|
||||||
// AUF der Masslinie (Mitte) — ihn zu ziehen ändert nur `offset` (s. moveGrip).
|
// zusätzlicher Griff (Index N) sitzt AUF der Masslinie (Mitte der Kette) —
|
||||||
|
// ihn zu ziehen ändert nur `offset`, nicht die Punkte (s. moveGrip).
|
||||||
if (g.shape === "dimension") {
|
if (g.shape === "dimension") {
|
||||||
const dx = g.b.x - g.a.x, dy = g.b.y - g.a.y;
|
const a = g.pts[0], b = g.pts[g.pts.length - 1];
|
||||||
|
const dx = b.x - a.x, dy = b.y - a.y;
|
||||||
const len = Math.hypot(dx, dy) || 1;
|
const len = Math.hypot(dx, dy) || 1;
|
||||||
const nx = (-dy / len) * g.offset, ny = (dx / len) * g.offset;
|
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 };
|
const mid: Vec2 = { x: (a.x + b.x) / 2 + nx, y: (a.y + b.y) / 2 + ny };
|
||||||
return [g.a, g.b, mid];
|
return [...g.pts, mid];
|
||||||
}
|
}
|
||||||
// Kote: EIN Griff an der Dreieck-Spitze (zum Verschieben; siehe moveGrip).
|
// Kote: EIN Griff an der Dreieck-Spitze (zum Verschieben; siehe moveGrip).
|
||||||
if (g.shape === "kote") return [g.at];
|
if (g.shape === "kote") return [g.at];
|
||||||
@@ -1592,16 +1594,20 @@ function moveGrip(
|
|||||||
const pts = g.pts.map((p, i) => (i === index ? pt : p));
|
const pts = g.pts.map((p, i) => (i === index ? pt : p));
|
||||||
return { ...d, geom: { ...g, pts } };
|
return { ...d, geom: { ...g, pts } };
|
||||||
}
|
}
|
||||||
// Bemassung: Griff 0/1 verschiebt den gemessenen Punkt a/b (Masszahl
|
// Bemassung: Griff 0..N-1 verschiebt den jeweiligen Kettenpunkt
|
||||||
// folgt automatisch, s. Rendering); Griff 2 (auf der Masslinie) ändert
|
// (Masszahlen folgen automatisch, s. Rendering); der LETZTE Griff
|
||||||
// nur `offset` — Normalabstand von `pt` zur (unveränderten) Strecke a-b.
|
// (auf der Masslinie) ändert nur `offset` — Normalabstand von `pt`
|
||||||
|
// zur (unveränderten) Punktreihe.
|
||||||
if (g.shape === "dimension") {
|
if (g.shape === "dimension") {
|
||||||
if (index === 0) return { ...d, geom: { ...g, a: pt } };
|
if (index < g.pts.length) {
|
||||||
if (index === 1) return { ...d, geom: { ...g, b: pt } };
|
const pts = g.pts.map((p, i) => (i === index ? pt : p));
|
||||||
const dx = g.b.x - g.a.x, dy = g.b.y - g.a.y;
|
return { ...d, geom: { ...g, pts } };
|
||||||
|
}
|
||||||
|
const a = g.pts[0], b = g.pts[g.pts.length - 1];
|
||||||
|
const dx = b.x - a.x, dy = b.y - a.y;
|
||||||
const len = Math.hypot(dx, dy) || 1;
|
const len = Math.hypot(dx, dy) || 1;
|
||||||
const nx = -dy / len, ny = dx / len;
|
const nx = -dy / len, ny = dx / len;
|
||||||
const offset = (pt.x - g.a.x) * nx + (pt.y - g.a.y) * ny;
|
const offset = (pt.x - a.x) * nx + (pt.y - a.y) * ny;
|
||||||
return { ...d, geom: { ...g, offset } };
|
return { ...d, geom: { ...g, offset } };
|
||||||
}
|
}
|
||||||
if (g.shape === "rect" || g.shape === "image") {
|
if (g.shape === "rect" || g.shape === "image") {
|
||||||
@@ -1698,9 +1704,8 @@ function moveElementBy(
|
|||||||
return { ...d, geom: { ...g, center: mv(g.center) } };
|
return { ...d, geom: { ...g, center: mv(g.center) } };
|
||||||
case "bezier":
|
case "bezier":
|
||||||
case "spline":
|
case "spline":
|
||||||
return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
|
|
||||||
case "dimension":
|
case "dimension":
|
||||||
return { ...d, geom: { ...g, a: mv(g.a), b: mv(g.b) } };
|
return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
|
||||||
case "kote":
|
case "kote":
|
||||||
return { ...d, geom: { ...g, at: mv(g.at) } };
|
return { ...d, geom: { ...g, at: mv(g.at) } };
|
||||||
case "text":
|
case "text":
|
||||||
|
|||||||
@@ -545,13 +545,14 @@ function geomBBox(g: Drawing2DGeom): SelectionBBox {
|
|||||||
for (const p of g.pts) acc(p.x, p.y);
|
for (const p of g.pts) acc(p.x, p.y);
|
||||||
break;
|
break;
|
||||||
case "dimension": {
|
case "dimension": {
|
||||||
acc(g.a.x, g.a.y);
|
for (const p of g.pts) acc(p.x, p.y);
|
||||||
acc(g.b.x, g.b.y);
|
if (g.pts.length >= 2) {
|
||||||
const dx = g.b.x - g.a.x, dy = g.b.y - g.a.y;
|
const a = g.pts[0], b = g.pts[g.pts.length - 1];
|
||||||
|
const dx = b.x - a.x, dy = b.y - a.y;
|
||||||
const len = Math.hypot(dx, dy) || 1;
|
const len = Math.hypot(dx, dy) || 1;
|
||||||
const nx = (-dy / len) * g.offset, ny = (dx / len) * g.offset;
|
const nx = (-dy / len) * g.offset, ny = (dx / len) * g.offset;
|
||||||
acc(g.a.x + nx, g.a.y + ny);
|
for (const p of g.pts) acc(p.x + nx, p.y + ny);
|
||||||
acc(g.b.x + nx, g.b.y + ny);
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "kote":
|
case "kote":
|
||||||
|
|||||||
@@ -55,8 +55,7 @@ function drawingsBBox(
|
|||||||
} else if (g.shape === "bezier" || g.shape === "spline") {
|
} else if (g.shape === "bezier" || g.shape === "spline") {
|
||||||
for (const p of g.pts) acc(p.x, p.y);
|
for (const p of g.pts) acc(p.x, p.y);
|
||||||
} else if (g.shape === "dimension") {
|
} else if (g.shape === "dimension") {
|
||||||
acc(g.a.x, g.a.y);
|
for (const p of g.pts) acc(p.x, p.y);
|
||||||
acc(g.b.x, g.b.y);
|
|
||||||
} else if (g.shape === "kote") acc(g.at.x, g.at.y);
|
} else if (g.shape === "kote") acc(g.at.x, g.at.y);
|
||||||
else if (g.shape === "text") acc(g.at.x, g.at.y);
|
else if (g.shape === "text") acc(g.at.x, g.at.y);
|
||||||
}
|
}
|
||||||
@@ -76,7 +75,7 @@ function translateDrawing(d: Drawing2D, dx: number, dy: number): Drawing2D {
|
|||||||
case "ellipse": return { ...d, geom: { ...g, center: mv(g.center) } };
|
case "ellipse": return { ...d, geom: { ...g, center: mv(g.center) } };
|
||||||
case "bezier": return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
|
case "bezier": return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
|
||||||
case "spline": 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 "dimension": return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
|
||||||
case "kote": return { ...d, geom: { ...g, at: mv(g.at) } };
|
case "kote": return { ...d, geom: { ...g, at: mv(g.at) } };
|
||||||
case "text": return { ...d, geom: { ...g, at: mv(g.at) } };
|
case "text": return { ...d, geom: { ...g, at: mv(g.at) } };
|
||||||
default: return d;
|
default: return d;
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ const mvGeom = (geom: Drawing2DGeom, fn: (p: Vec2) => Vec2): Drawing2DGeom => {
|
|||||||
case "spline":
|
case "spline":
|
||||||
return { ...geom, pts: geom.pts.map(fn) };
|
return { ...geom, pts: geom.pts.map(fn) };
|
||||||
case "dimension":
|
case "dimension":
|
||||||
return { ...geom, a: fn(geom.a), b: fn(geom.b) };
|
return { ...geom, pts: geom.pts.map(fn) };
|
||||||
case "kote":
|
case "kote":
|
||||||
return { ...geom, at: fn(geom.at) };
|
return { ...geom, at: fn(geom.at) };
|
||||||
case "text":
|
case "text":
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ export function drawingGripVertices(d: Drawing2D): Vec2[] {
|
|||||||
{ x: g.min.x, y: g.max.y },
|
{ x: g.min.x, y: g.max.y },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (g.shape === "bezier" || g.shape === "spline") return [...g.pts];
|
if (g.shape === "bezier" || g.shape === "spline" || g.shape === "dimension") return [...g.pts];
|
||||||
if (g.shape === "dimension") return [g.a, g.b];
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user