2D: Höhenkoten-Werkzeug + Masskette bleibt gerade + Punkt-Notation
Neues Werkzeug "Höhenkote" (SIA 400 B.5.4, Figur 18): Dreieck-Symbol +
Höhenwert an einem Punkt. Vier Varianten (OK/UK × fertig/roh) bestimmen
Dreieck-Richtung und Füllung, per Option umschaltbar. Die Norm ordnet
Zahlen UNTERHALB einer Masslinie explizit Höhenmassen zu — das war der
eigentliche Grund, warum ein separater Typ statt einer Bemassungs-Variante
nötig war.
Bemassungs-Korrektur: eine Masskette bleibt beim Zeichnen immer eine
GERADE Linie (Klicks ab dem dritten Punkt werden auf die durch Punkt 1/2
festgelegte Gerade projiziert), auch wenn die Bedienung wie eine Polylinie
funktioniert — vorher konnte man versehentlich einen Zickzack-Pfad
erzeugen.
Ausserdem: Masszahlen nutzen jetzt Punkt- statt Komma-Notation ("3.96"
statt "3,96") — die tatsächlich gezeichneten Beispiele in der Norm
(Figur 13/14/18) verwenden durchgängig den Punkt, nicht das Komma aus der
abstrakten B.5.2-Beispielliste.
This commit is contained in:
@@ -223,6 +223,7 @@ const TOOL_COMMAND: Partial<Record<ToolId, string>> = {
|
|||||||
spline: "spline",
|
spline: "spline",
|
||||||
bezier: "bezier",
|
bezier: "bezier",
|
||||||
dimension: "dimension",
|
dimension: "dimension",
|
||||||
|
kote: "kote",
|
||||||
text: "text",
|
text: "text",
|
||||||
textbox: "textbox",
|
textbox: "textbox",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -104,6 +104,29 @@ describe("dimensionCommand", () => {
|
|||||||
expect(segs[1]).toMatchObject({ a: { x: 4, y: 0 }, b: { x: 10, y: 0 }, offset: 2 });
|
expect(segs[1]).toMatchObject({ a: { x: 4, y: 0 }, b: { x: 10, y: 0 }, offset: 2 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("dritter Punkt ausserhalb der Geraden wird auf sie projiziert (keine Polylinie, immer gerade)", () => {
|
||||||
|
const p = project();
|
||||||
|
const ctx = makeCtx(p);
|
||||||
|
let [state] = dimensionCommand.onInput(
|
||||||
|
dimensionCommand.init(),
|
||||||
|
{ kind: "point", point: { x: 0, y: 0 } },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
// Richtung: entlang +x (y=0).
|
||||||
|
[state] = dimensionCommand.onInput(state, { kind: "point", point: { x: 4, y: 0 } }, ctx);
|
||||||
|
// Dritter Klick liegt bewusst NICHT auf der Geraden (y=3) → wird projiziert.
|
||||||
|
[state] = dimensionCommand.onInput(state, { kind: "point", point: { x: 10, y: 3 } }, 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);
|
||||||
|
const second = next.drawings2d[1].geom;
|
||||||
|
if (second.shape === "dimension") {
|
||||||
|
expect(second.b.y).toBeCloseTo(0, 6); // auf die Gerade y=0 projiziert
|
||||||
|
expect(second.b.x).toBeCloseTo(10, 6); // x-Komponente bleibt (Projektion entlang x)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("Zurück-Option entfernt den letzten Punkt", () => {
|
it("Zurück-Option entfernt den letzten Punkt", () => {
|
||||||
const p = project();
|
const p = project();
|
||||||
const ctx = makeCtx(p);
|
const ctx = makeCtx(p);
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
// Bemassung (lineare Masslinie(n), SIA 400 B.5.3, Figur 14 „Reihenbemassung").
|
// Bemassung (lineare Masslinie(n), SIA 400 B.5.3, Figur 14 „Reihenbemassung").
|
||||||
// Wie eine Polylinie: beliebig viele Punkte, dann EIN weiterer Klick für die
|
// Bedienung wie eine Polylinie (beliebig viele Klicks, Doppelklick/Enter
|
||||||
// Lage der Masslinie(n) — erzeugt eine ganze Massketten in einer Geste.
|
// beendet), das ERGEBNIS ist aber IMMER eine GERADE Punktreihe: der erste
|
||||||
// Schritte:
|
// Klick setzt den Startpunkt, der zweite legt die Richtung fest — jeder
|
||||||
|
// weitere Klick wird auf diese Gerade projiziert (Reihenbemassung misst
|
||||||
|
// mehrere Punkte entlang EINER Referenzlinie, z. B. einer Wand, nie einen
|
||||||
|
// Zickzack-Pfad). Schritte:
|
||||||
// 1) „Erster Punkt:" → Punkt
|
// 1) „Erster Punkt:" → Punkt
|
||||||
// 2) „Nächster Punkt ( Fertig Zurück ):" → Punkt … (Doppelklick/Enter
|
// 2) „Nächster Punkt ( Fertig Zurück ):" → Punkt … (ab dem 3. Punkt auf die
|
||||||
// beendet die Punktreihe, sobald ≥2 Punkte gesetzt sind)
|
// durch Punkt 1/2 definierte Gerade projiziert)
|
||||||
// 3) „Lage der Masslinie:" → Punkt/Zahl (EIN Normalabstand
|
// 3) „Lage der Masslinie:" → Punkt/Zahl (EIN Normalabstand
|
||||||
// für ALLE Segmente) → commit: je aufeinanderfolgendem Punktpaar EIN
|
// für ALLE Segmente) → commit: je aufeinanderfolgendem Punktpaar EIN
|
||||||
// `shape:"dimension"`-Element (unabhängig editierbar, teilt nur den
|
// `shape:"dimension"`-Element (unabhängig editierbar, teilt nur den
|
||||||
@@ -40,6 +43,15 @@ function signedOffset(a: Vec2, b: Vec2, pt: Vec2): number {
|
|||||||
return (pt.x - a.x) * nx + (pt.y - a.y) * ny;
|
return (pt.x - a.x) * nx + (pt.y - a.y) * ny;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Projiziert `pt` auf die Gerade durch `p0` in Richtung `p1` (Kette bleibt gerade ab Punkt 3). */
|
||||||
|
function projectOnLine(p0: Vec2, p1: Vec2, pt: Vec2): Vec2 {
|
||||||
|
const dx = p1.x - p0.x, dy = p1.y - p0.y;
|
||||||
|
const len2 = dx * dx + dy * dy;
|
||||||
|
if (len2 < EPS) return pt;
|
||||||
|
const t = ((pt.x - p0.x) * dx + (pt.y - p0.y) * dy) / len2;
|
||||||
|
return { x: p0.x + dx * t, y: p0.y + dy * t };
|
||||||
|
}
|
||||||
|
|
||||||
/** 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;
|
||||||
@@ -159,9 +171,11 @@ export const dimensionCommand: Command = {
|
|||||||
const ns: DimNext = { phase: "next", pts: [pt], cursor: pt, lastPoint: pt };
|
const ns: DimNext = { phase: "next", pts: [pt], cursor: pt, lastPoint: pt };
|
||||||
return [ns, { draft: chainDraft([pt], pt) }];
|
return [ns, { draft: chainDraft([pt], pt) }];
|
||||||
}
|
}
|
||||||
const pts = [...s.pts, pt];
|
// Ab dem dritten Punkt: auf die durch Punkt 1/2 festgelegte Gerade projizieren.
|
||||||
const ns: DimNext = { phase: "next", pts, cursor: pt, lastPoint: pt };
|
const placed = s.pts.length >= 2 ? projectOnLine(s.pts[0], s.pts[1], pt) : pt;
|
||||||
return [ns, { draft: chainDraft(pts, pt) }];
|
const pts = [...s.pts, placed];
|
||||||
|
const ns: DimNext = { phase: "next", pts, cursor: placed, lastPoint: placed };
|
||||||
|
return [ns, { draft: chainDraft(pts, placed) }];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Offset-Schritt: Zahl = direkter Abstand, Punkt = Abstand zum Cursor.
|
// Offset-Schritt: Zahl = direkter Abstand, Punkt = Abstand zum Cursor.
|
||||||
@@ -179,8 +193,9 @@ export const dimensionCommand: Command = {
|
|||||||
onMove: (state, point): [CommandState, CommandResult] => {
|
onMove: (state, point): [CommandState, CommandResult] => {
|
||||||
const s = state as DimState;
|
const s = state as DimState;
|
||||||
if (s.phase === "next") {
|
if (s.phase === "next") {
|
||||||
const ns: DimNext = { ...s, cursor: point };
|
const cursor = s.pts.length >= 2 ? projectOnLine(s.pts[0], s.pts[1], point) : point;
|
||||||
return [ns, { draft: chainDraft(s.pts, point) }];
|
const ns: DimNext = { ...s, cursor };
|
||||||
|
return [ns, { draft: chainDraft(s.pts, cursor) }];
|
||||||
}
|
}
|
||||||
if (s.phase === "offset") {
|
if (s.phase === "offset") {
|
||||||
// Offset relativ zum ERSTEN Segment (repräsentativ für die ganze Kette).
|
// Offset relativ zum ERSTEN Segment (repräsentativ für die ganze Kette).
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* `koteCommand` — Punkt + getippter Höhenwert → `shape:"kote"` (SIA 400
|
||||||
|
* B.5.4, Figur 18). Default-Art „OK fertig"; per Option zyklisch umschaltbar.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { koteCommand } from "./kote";
|
||||||
|
import type { CommandContext, Project } from "../types";
|
||||||
|
|
||||||
|
function project(): Project {
|
||||||
|
return {
|
||||||
|
id: "t",
|
||||||
|
name: "T",
|
||||||
|
lineStyles: [],
|
||||||
|
hatches: [],
|
||||||
|
components: [],
|
||||||
|
wallTypes: [],
|
||||||
|
drawingLevels: [
|
||||||
|
{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, cutHeight: 1.0, baseElevation: 0 },
|
||||||
|
],
|
||||||
|
layers: [{ code: "20", name: "Zeichnung", color: "#0a0a0a", lw: 0.5, visible: true, locked: false }],
|
||||||
|
walls: [],
|
||||||
|
doors: [],
|
||||||
|
openings: [],
|
||||||
|
ceilings: [],
|
||||||
|
stairs: [],
|
||||||
|
rooms: [],
|
||||||
|
drawings2d: [],
|
||||||
|
context: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCtx(p: Project): CommandContext {
|
||||||
|
return {
|
||||||
|
project: p,
|
||||||
|
level: p.drawingLevels[0],
|
||||||
|
defaultCategoryCode: "20",
|
||||||
|
activeLineStyleId: "solid",
|
||||||
|
activeWallTypeId: "aw",
|
||||||
|
lastPoint: null,
|
||||||
|
selection: { wallIds: [], drawingId: null },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("koteCommand", () => {
|
||||||
|
it("Punkt + Zahl → shape:kote mit Default-Art OK fertig, rotation 0", () => {
|
||||||
|
const p = project();
|
||||||
|
const ctx = makeCtx(p);
|
||||||
|
let [state] = koteCommand.onInput(koteCommand.init(), { kind: "point", point: { x: 2, y: 3 } }, ctx);
|
||||||
|
const [, result] = koteCommand.onInput(state, { kind: "number", value: 3.25 }, ctx);
|
||||||
|
expect(result.commit).toBeDefined();
|
||||||
|
const next = result.commit!(p);
|
||||||
|
expect(next.drawings2d.length).toBe(1);
|
||||||
|
const g = next.drawings2d[0].geom;
|
||||||
|
expect(g.shape).toBe("kote");
|
||||||
|
if (g.shape === "kote") {
|
||||||
|
expect(g.at).toEqual({ x: 2, y: 3 });
|
||||||
|
expect(g.value).toBe(3.25);
|
||||||
|
expect(g.kind).toBe("okFertig");
|
||||||
|
expect(g.rotation).toBe(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Art-Option zyklt OK fertig → UK fertig → OK roh → UK roh → OK fertig", () => {
|
||||||
|
const p = project();
|
||||||
|
const ctx = makeCtx(p);
|
||||||
|
let [state] = koteCommand.onInput(koteCommand.init(), { kind: "point", point: { x: 0, y: 0 } }, ctx);
|
||||||
|
const seen: string[] = [];
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
[state] = koteCommand.onInput(state, { kind: "option", id: "kind" }, ctx);
|
||||||
|
const opts = koteCommand.options(state);
|
||||||
|
seen.push(opts[0].value!);
|
||||||
|
}
|
||||||
|
expect(seen).toEqual(["ukFertig", "okRoh", "ukRoh", "okFertig"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("negativer Wert wird unverändert übernommen (Formatierung passiert erst beim Rendern)", () => {
|
||||||
|
const p = project();
|
||||||
|
const ctx = makeCtx(p);
|
||||||
|
let [state] = koteCommand.onInput(koteCommand.init(), { kind: "point", point: { x: 0, y: 0 } }, ctx);
|
||||||
|
const [, result] = koteCommand.onInput(state, { kind: "number", value: -0.1 }, ctx);
|
||||||
|
const next = result.commit!(p);
|
||||||
|
const g = next.drawings2d[0].geom;
|
||||||
|
if (g.shape === "kote") expect(g.value).toBe(-0.1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// Höhenkote (SIA 400 B.5.4, Figur 18) — Dreieck-Symbol + Höhenwert an einem
|
||||||
|
// Punkt. Schritte:
|
||||||
|
// 1) „Punkt:" → Punkt (Dreieck-Spitze)
|
||||||
|
// 2) „Höhenwert ( Art ):" → Zahl (Meter, z. B. 3.25 oder -0.1) →
|
||||||
|
// commit. „Art" (Option, zyklisch: OK fertig/UK fertig/OK roh/UK roh)
|
||||||
|
// wechselt VOR dem Tippen die Dreieck-Variante (Default OK fertig).
|
||||||
|
|
||||||
|
import type { Drawing2D } from "../../model/types";
|
||||||
|
import { uniqueId } from "../../tools/types";
|
||||||
|
import type {
|
||||||
|
Command,
|
||||||
|
CommandContext,
|
||||||
|
CommandResult,
|
||||||
|
CommandState,
|
||||||
|
CmdOption,
|
||||||
|
DraftShape,
|
||||||
|
Project,
|
||||||
|
ToolDraft,
|
||||||
|
Vec2,
|
||||||
|
} from "../types";
|
||||||
|
import type { TranslationKey } from "../../i18n";
|
||||||
|
|
||||||
|
type KoteKind = "okFertig" | "ukFertig" | "okRoh" | "ukRoh";
|
||||||
|
const KIND_ORDER: KoteKind[] = ["okFertig", "ukFertig", "okRoh", "ukRoh"];
|
||||||
|
const nextKind = (k: KoteKind): KoteKind => KIND_ORDER[(KIND_ORDER.indexOf(k) + 1) % KIND_ORDER.length];
|
||||||
|
const KIND_LABEL: Record<KoteKind, TranslationKey> = {
|
||||||
|
okFertig: "cmd.kote.kind.okFertig",
|
||||||
|
ukFertig: "cmd.kote.kind.ukFertig",
|
||||||
|
okRoh: "cmd.kote.kind.okRoh",
|
||||||
|
ukRoh: "cmd.kote.kind.ukRoh",
|
||||||
|
};
|
||||||
|
|
||||||
|
const TRI_SIZE = 0.15; // Modell-Meter, Vorschau-Grösse (Rendering: DIM_… in drawing2d.ts)
|
||||||
|
const HALF_W = TRI_SIZE * 0.6;
|
||||||
|
|
||||||
|
/** Dreieck-Punkte für die Vorschau: `at` = Spitze; OK zeigt nach unten, UK nach oben. */
|
||||||
|
function triPts(at: Vec2, kind: KoteKind): Vec2[] {
|
||||||
|
const up = kind === "okFertig" || kind === "okRoh" ? 1 : -1; // Basis liegt in +up-Richtung von `at`
|
||||||
|
return [
|
||||||
|
at,
|
||||||
|
{ x: at.x - HALF_W, y: at.y + up * TRI_SIZE },
|
||||||
|
{ x: at.x + HALF_W, y: at.y + up * TRI_SIZE },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KotePoint extends CommandState {
|
||||||
|
phase: "point";
|
||||||
|
}
|
||||||
|
interface KoteValue extends CommandState {
|
||||||
|
phase: "value";
|
||||||
|
at: Vec2;
|
||||||
|
kind: KoteKind;
|
||||||
|
}
|
||||||
|
type KoteState = KotePoint | KoteValue;
|
||||||
|
|
||||||
|
function kindOption(kind: KoteKind): CmdOption {
|
||||||
|
return { id: "kind", labelKey: KIND_LABEL[kind], value: kind };
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueDraft(at: Vec2, kind: KoteKind): ToolDraft {
|
||||||
|
const preview: DraftShape[] = [{ kind: "poly", pts: triPts(at, kind), closed: true }];
|
||||||
|
return { preview, vertices: [at] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendKote(p: Project, at: Vec2, value: number, kind: KoteKind, ctx: CommandContext): Project {
|
||||||
|
const d: Drawing2D = {
|
||||||
|
id: uniqueId("dr2d"),
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: ctx.level.id,
|
||||||
|
categoryCode: ctx.defaultCategoryCode,
|
||||||
|
geom: { shape: "kote", at, value, kind, rotation: 0 },
|
||||||
|
};
|
||||||
|
return { ...p, drawings2d: [...p.drawings2d, d] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const idle = (): [CommandState, CommandResult] => [
|
||||||
|
{ phase: "point", lastPoint: null },
|
||||||
|
{ draft: null, done: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const koteCommand: Command = {
|
||||||
|
name: "kote",
|
||||||
|
labelKey: "cmd.kote.label",
|
||||||
|
prompt: (s) => ((s as KoteState).phase === "value" ? "cmd.kote.value" : "cmd.kote.point"),
|
||||||
|
accepts: (s) => ((s as KoteState).phase === "value" ? ["number", "option"] : ["point"]),
|
||||||
|
options: (s) => ((s as KoteState).phase === "value" ? [kindOption((s as KoteValue).kind)] : []),
|
||||||
|
init: (): KotePoint => ({ phase: "point", lastPoint: null }),
|
||||||
|
|
||||||
|
onInput: (state, input, ctx): [CommandState, CommandResult] => {
|
||||||
|
const s = state as KoteState;
|
||||||
|
if (s.phase === "point") {
|
||||||
|
if (input.kind !== "point") return [s, { draft: null }];
|
||||||
|
const ns: KoteValue = { phase: "value", at: input.point, kind: "okFertig", lastPoint: input.point };
|
||||||
|
return [ns, { draft: valueDraft(input.point, "okFertig") }];
|
||||||
|
}
|
||||||
|
if (input.kind === "option" && input.id === "kind") {
|
||||||
|
const ns: KoteValue = { ...s, kind: nextKind(s.kind) };
|
||||||
|
return [ns, { draft: valueDraft(s.at, ns.kind) }];
|
||||||
|
}
|
||||||
|
if (input.kind !== "number") return [s, { draft: valueDraft(s.at, s.kind) }];
|
||||||
|
const { at, kind } = s;
|
||||||
|
return [
|
||||||
|
{ phase: "point", lastPoint: null },
|
||||||
|
{ draft: null, done: true, commit: (p) => appendKote(p, at, input.value, kind, ctx) },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
onMove: (state): [CommandState, CommandResult] => [state, { draft: null }],
|
||||||
|
onConfirm: (): [CommandState, CommandResult] => idle(),
|
||||||
|
onCancel: (): [CommandState, CommandResult] => idle(),
|
||||||
|
};
|
||||||
@@ -21,6 +21,7 @@ import { ellipseCommand } from "./cmds/ellipse";
|
|||||||
import { splineCommand } from "./cmds/spline";
|
import { splineCommand } from "./cmds/spline";
|
||||||
import { bezierCommand } from "./cmds/bezier";
|
import { bezierCommand } from "./cmds/bezier";
|
||||||
import { dimensionCommand } from "./cmds/dimension";
|
import { dimensionCommand } from "./cmds/dimension";
|
||||||
|
import { koteCommand } from "./cmds/kote";
|
||||||
import { textCommand } from "./cmds/text";
|
import { textCommand } from "./cmds/text";
|
||||||
import { textboxCommand } from "./cmds/textbox";
|
import { textboxCommand } from "./cmds/textbox";
|
||||||
import { moveCommand } from "./cmds/move";
|
import { moveCommand } from "./cmds/move";
|
||||||
@@ -61,6 +62,7 @@ export const COMMANDS: Record<string, Command> = {
|
|||||||
spline: splineCommand,
|
spline: splineCommand,
|
||||||
bezier: bezierCommand,
|
bezier: bezierCommand,
|
||||||
dimension: dimensionCommand,
|
dimension: dimensionCommand,
|
||||||
|
kote: koteCommand,
|
||||||
text: textCommand,
|
text: textCommand,
|
||||||
textbox: textboxCommand,
|
textbox: textboxCommand,
|
||||||
move: moveCommand,
|
move: moveCommand,
|
||||||
@@ -116,6 +118,8 @@ export const ALIASES: Record<string, string> = {
|
|||||||
sp: "spline",
|
sp: "spline",
|
||||||
dim: "dimension",
|
dim: "dimension",
|
||||||
bemassung: "dimension",
|
bemassung: "dimension",
|
||||||
|
kt: "kote",
|
||||||
|
hoehenkote: "kote",
|
||||||
bz: "bezier",
|
bz: "bezier",
|
||||||
tx: "text",
|
tx: "text",
|
||||||
txt: "text",
|
txt: "text",
|
||||||
|
|||||||
+10
-1
@@ -185,7 +185,9 @@ export const de = {
|
|||||||
"tool.bezier": "Bezier",
|
"tool.bezier": "Bezier",
|
||||||
"tool.bezier.hint": "Bezier: Anker, Griff 1, Griff 2, Endpunkt — je Segment; Enter beendet",
|
"tool.bezier.hint": "Bezier: Anker, Griff 1, Griff 2, Endpunkt — je Segment; Enter beendet",
|
||||||
"tool.dimension": "Bemassung",
|
"tool.dimension": "Bemassung",
|
||||||
"tool.dimension.hint": "Bemassung: Punkte wie Polylinie klicken, Doppelklick/Enter beendet, dann Lage der Masslinie (Massketten in einer Geste)",
|
"tool.dimension.hint": "Bemassung: Punkte wie Polylinie klicken (ab Punkt 3 auf einer Geraden), Doppelklick/Enter beendet, dann Lage der Masslinie (Massketten in einer Geste)",
|
||||||
|
"tool.kote": "Höhenkote",
|
||||||
|
"tool.kote.hint": "Höhenkote: Punkt setzen, dann Höhenwert eintippen (Art umschaltbar: OK/UK, fertig/roh)",
|
||||||
"tool.text": "Text",
|
"tool.text": "Text",
|
||||||
"tool.text.hint": "Text: Ankerpunkt setzen, dann Text eintippen",
|
"tool.text.hint": "Text: Ankerpunkt setzen, dann Text eintippen",
|
||||||
"tool.textbox": "Textspalte",
|
"tool.textbox": "Textspalte",
|
||||||
@@ -1168,6 +1170,13 @@ export const de = {
|
|||||||
"cmd.dimension.offset": "Lage der Masslinie:",
|
"cmd.dimension.offset": "Lage der Masslinie:",
|
||||||
"cmd.dimension.undo": "Zurück",
|
"cmd.dimension.undo": "Zurück",
|
||||||
"cmd.dimension.finish": "Fertig",
|
"cmd.dimension.finish": "Fertig",
|
||||||
|
"cmd.kote.label": "Höhenkote",
|
||||||
|
"cmd.kote.point": "Punkt:",
|
||||||
|
"cmd.kote.value": "Höhenwert ( Art ):",
|
||||||
|
"cmd.kote.kind.okFertig": "OK fertig",
|
||||||
|
"cmd.kote.kind.ukFertig": "UK fertig",
|
||||||
|
"cmd.kote.kind.okRoh": "OK roh",
|
||||||
|
"cmd.kote.kind.ukRoh": "UK roh",
|
||||||
"cmd.field.offset": "Abstand",
|
"cmd.field.offset": "Abstand",
|
||||||
"cmd.text.label": "Text",
|
"cmd.text.label": "Text",
|
||||||
"cmd.text.point": "Ankerpunkt:",
|
"cmd.text.point": "Ankerpunkt:",
|
||||||
|
|||||||
+10
-1
@@ -183,7 +183,9 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"tool.bezier": "Bezier",
|
"tool.bezier": "Bezier",
|
||||||
"tool.bezier.hint": "Bezier: anchor, handle 1, handle 2, end point — per segment; Enter finishes",
|
"tool.bezier.hint": "Bezier: anchor, handle 1, handle 2, end point — per segment; Enter finishes",
|
||||||
"tool.dimension": "Dimension",
|
"tool.dimension": "Dimension",
|
||||||
"tool.dimension.hint": "Dimension: click points like a polyline, double-click/Enter finishes, then set the dimension line position (chains in one gesture)",
|
"tool.dimension.hint": "Dimension: click points like a polyline (point 3+ snaps to a straight line), double-click/Enter finishes, then set the dimension line position (chains in one gesture)",
|
||||||
|
"tool.kote": "Elevation mark",
|
||||||
|
"tool.kote.hint": "Elevation mark: set a point, then type the height value (type toggle: top/bottom, finished/raw)",
|
||||||
"tool.text": "Text",
|
"tool.text": "Text",
|
||||||
"tool.text.hint": "Text: set anchor point, then type the text",
|
"tool.text.hint": "Text: set anchor point, then type the text",
|
||||||
"tool.textbox": "Text column",
|
"tool.textbox": "Text column",
|
||||||
@@ -1157,6 +1159,13 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"cmd.dimension.offset": "Dimension line position:",
|
"cmd.dimension.offset": "Dimension line position:",
|
||||||
"cmd.dimension.undo": "Undo",
|
"cmd.dimension.undo": "Undo",
|
||||||
"cmd.dimension.finish": "Finish",
|
"cmd.dimension.finish": "Finish",
|
||||||
|
"cmd.kote.label": "Elevation mark",
|
||||||
|
"cmd.kote.point": "Point:",
|
||||||
|
"cmd.kote.value": "Height value ( Type ):",
|
||||||
|
"cmd.kote.kind.okFertig": "Top, finished",
|
||||||
|
"cmd.kote.kind.ukFertig": "Bottom, finished",
|
||||||
|
"cmd.kote.kind.okRoh": "Top, raw",
|
||||||
|
"cmd.kote.kind.ukRoh": "Bottom, raw",
|
||||||
"cmd.field.offset": "Offset",
|
"cmd.field.offset": "Offset",
|
||||||
"cmd.text.label": "Text",
|
"cmd.text.label": "Text",
|
||||||
"cmd.text.point": "Anchor point:",
|
"cmd.text.point": "Anchor point:",
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ export function translateProject(project: Project, dx: number, dy: number): Proj
|
|||||||
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, a: shift(g.a, dx, dy), b: shift(g.b, dx, dy) } };
|
||||||
|
case "kote":
|
||||||
|
return { ...d, geom: { ...g, at: shift(g.at, dx, dy) } };
|
||||||
case "text":
|
case "text":
|
||||||
return { ...d, geom: { ...g, at: shift(g.at, dx, dy) } };
|
return { ...d, geom: { ...g, at: shift(g.at, dx, dy) } };
|
||||||
case "image":
|
case "image":
|
||||||
|
|||||||
@@ -1534,6 +1534,22 @@ export type Drawing2DGeom =
|
|||||||
* 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 }
|
| { shape: "dimension"; a: Vec2; b: Vec2; offset: number }
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* Dreieck-Richtung (OK = Spitze nach unten, UK = Spitze nach oben) UND
|
||||||
|
* Füllung (fertig = Kontur/hohl, roh = voll gefüllt) — vier norm-genaue
|
||||||
|
* Varianten, die fünfte („OK fertig UND roh kombiniert") ist nicht gebaut.
|
||||||
|
* `at` ist die SPITZE des Dreiecks (der exakte referenzierte Punkt).
|
||||||
|
*/
|
||||||
|
| {
|
||||||
|
shape: "kote";
|
||||||
|
at: Vec2;
|
||||||
|
value: number;
|
||||||
|
kind: "okFertig" | "ukFertig" | "okRoh" | "ukRoh";
|
||||||
|
/** Drehung (Radiant); Werkzeug erzeugt bewusst nur 0, s. Ellipse-Muster. */
|
||||||
|
rotation: number;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
shape: "text";
|
shape: "text";
|
||||||
at: Vec2;
|
at: Vec2;
|
||||||
|
|||||||
@@ -179,6 +179,15 @@ export function ToolIcon({ id }: { id: ToolId }) {
|
|||||||
<line x1="12.7" y1="6.6" x2="14.3" y2="9.4" strokeWidth="1.6" />
|
<line x1="12.7" y1="6.6" x2="14.3" y2="9.4" strokeWidth="1.6" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
case "kote":
|
||||||
|
// Höhenkote: Dreieck (Spitze unten, OK) + Zahl.
|
||||||
|
return (
|
||||||
|
<svg {...common}>
|
||||||
|
<polygon points="8,10 5,5 11,5" fill="none" />
|
||||||
|
<line x1="10.5" y1="6.5" x2="14" y2="6.5" />
|
||||||
|
<line x1="10.5" y1="9" x2="14" y2="9" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
case "text":
|
case "text":
|
||||||
// Serifen-„A" als Text-Symbol.
|
// Serifen-„A" als Text-Symbol.
|
||||||
return (
|
return (
|
||||||
@@ -225,6 +234,7 @@ const TOOLS_2D: ToolId[] = [
|
|||||||
"spline",
|
"spline",
|
||||||
"bezier",
|
"bezier",
|
||||||
"dimension",
|
"dimension",
|
||||||
|
"kote",
|
||||||
"text",
|
"text",
|
||||||
"textbox",
|
"textbox",
|
||||||
"image",
|
"image",
|
||||||
|
|||||||
@@ -732,7 +732,8 @@ function computeBounds(
|
|||||||
} else if (g.shape === "dimension") {
|
} else if (g.shape === "dimension") {
|
||||||
acc(g.a);
|
acc(g.a);
|
||||||
acc(g.b);
|
acc(g.b);
|
||||||
} else if (g.shape === "text") acc(g.at);
|
} else if (g.shape === "kote") 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); }
|
||||||
}
|
}
|
||||||
// GESAMTE Kontext-Schicht (Konturen, importierte Gebäude/Terrain, Luftbild)
|
// GESAMTE Kontext-Schicht (Konturen, importierte Gebäude/Terrain, Luftbild)
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ describe("addDrawing2D — dimension (SIA 400 B.5.3)", () => {
|
|||||||
expect(thick).toBeCloseTo(thin * 2, 6);
|
expect(thick).toBeCloseTo(thin * 2, 6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Masszahl entspricht dist(a,b) mit Komma-Dezimaltrennzeichen (SIA 400 B.5.2)", () => {
|
it("Masszahl entspricht dist(a,b), 2 Dezimalstellen (SIA 400 Figur 13/14: Punkt-Notation)", () => {
|
||||||
const d: Drawing2D = {
|
const d: Drawing2D = {
|
||||||
id: "dim2",
|
id: "dim2",
|
||||||
type: "drawing2d",
|
type: "drawing2d",
|
||||||
@@ -158,7 +158,7 @@ describe("addDrawing2D — dimension (SIA 400 B.5.3)", () => {
|
|||||||
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
||||||
const text = out.find((p) => p.kind === "drawingText");
|
const text = out.find((p) => p.kind === "drawingText");
|
||||||
expect(text).toBeDefined();
|
expect(text).toBeDefined();
|
||||||
if (text?.kind === "drawingText") expect(text.text).toBe("5,00");
|
if (text?.kind === "drawingText") expect(text.text).toBe("5.00");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("entartete Strecke (a===b) erzeugt keine Primitive", () => {
|
it("entartete Strecke (a===b) erzeugt keine Primitive", () => {
|
||||||
@@ -174,3 +174,60 @@ describe("addDrawing2D — dimension (SIA 400 B.5.3)", () => {
|
|||||||
expect(out.length).toBe(0);
|
expect(out.length).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("addDrawing2D — kote (SIA 400 B.5.4)", () => {
|
||||||
|
it("OK fertig: hohles Dreieck (Spitze nach unten) + formatierte Zahl mit +", () => {
|
||||||
|
const d: Drawing2D = {
|
||||||
|
id: "k1",
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: "eg",
|
||||||
|
categoryCode: "20",
|
||||||
|
geom: { shape: "kote", at: { x: 0, y: 0 }, value: 3.25, kind: "okFertig", rotation: 0 },
|
||||||
|
};
|
||||||
|
const out: Primitive[] = [];
|
||||||
|
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
||||||
|
const poly = out.find((p) => p.kind === "polygon");
|
||||||
|
const text = out.find((p) => p.kind === "drawingText");
|
||||||
|
expect(poly).toBeDefined();
|
||||||
|
expect(text).toBeDefined();
|
||||||
|
if (poly?.kind === "polygon") {
|
||||||
|
expect(poly.fill).toBe("none"); // fertig = hohl
|
||||||
|
expect(poly.pts[0]).toEqual({ x: 0, y: 0 }); // Spitze = at
|
||||||
|
expect(poly.pts[1].y).toBeGreaterThan(0); // OK: Basis oberhalb der Spitze
|
||||||
|
}
|
||||||
|
if (text?.kind === "drawingText") expect(text.text).toBe("+3.25");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("UK roh: gefülltes Dreieck (Spitze nach oben), Basis unterhalb", () => {
|
||||||
|
const d: Drawing2D = {
|
||||||
|
id: "k2",
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: "eg",
|
||||||
|
categoryCode: "20",
|
||||||
|
geom: { shape: "kote", at: { x: 0, y: 0 }, value: -0.1, kind: "ukRoh", rotation: 0 },
|
||||||
|
};
|
||||||
|
const out: Primitive[] = [];
|
||||||
|
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
||||||
|
const poly = out.find((p) => p.kind === "polygon");
|
||||||
|
const text = out.find((p) => p.kind === "drawingText");
|
||||||
|
if (poly?.kind === "polygon") {
|
||||||
|
expect(poly.fill).not.toBe("none"); // roh = gefüllt
|
||||||
|
expect(poly.pts[1].y).toBeLessThan(0); // UK: Basis unterhalb der Spitze
|
||||||
|
}
|
||||||
|
if (text?.kind === "drawingText") expect(text.text).toBe("-0.10");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Wert 0 wird als ±0.00 formatiert", () => {
|
||||||
|
const d: Drawing2D = {
|
||||||
|
id: "k3",
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: "eg",
|
||||||
|
categoryCode: "20",
|
||||||
|
geom: { shape: "kote", at: { x: 0, y: 0 }, value: 0, kind: "okFertig", rotation: 0 },
|
||||||
|
};
|
||||||
|
const out: Primitive[] = [];
|
||||||
|
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
||||||
|
const text = out.find((p) => p.kind === "drawingText");
|
||||||
|
if (text?.kind === "drawingText") expect(text.text).toBe("±0.00");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -25,9 +25,21 @@ const DIM_OVERSHOOT_M = 0.05; // Überstand der Masshilfslinie über die Masslin
|
|||||||
const DIM_TICK_LEN_M = 0.15; // Länge des Massstrichs (Diagonale an den Masslinien-Enden)
|
const DIM_TICK_LEN_M = 0.15; // Länge des Massstrichs (Diagonale an den Masslinien-Enden)
|
||||||
const DIM_TEXT_HEIGHT_M = 0.2; // Schrifthöhe der Masszahl
|
const DIM_TEXT_HEIGHT_M = 0.2; // Schrifthöhe der Masszahl
|
||||||
|
|
||||||
/** Formatiert eine Länge (Meter) als Masszahl: 2 Dezimalstellen, Komma (SIA 400 B.5.2, „2,75"). */
|
/** Formatiert eine Länge (Meter) als Masszahl: 2 Dezimalstellen (SIA 400 B.5.2/B.5.4, „3.25"/„518.60"). */
|
||||||
function formatDimLabel(m: number): string {
|
function formatDimLabel(m: number): string {
|
||||||
return m.toFixed(2).replace(".", ",");
|
return m.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Höhenkote (SIA 400 B.5.4) ────────────────────────────────────────────────
|
||||||
|
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_TEXT_HEIGHT_M = 0.2;
|
||||||
|
const KOTE_TEXT_GAP_M = 0.08; // Abstand Dreieck-Basis → Zahl
|
||||||
|
|
||||||
|
/** Formatiert einen Höhenwert (SIA 400 B.5.4, Figur 18): „+3.25" · „±0.00" · „-0.10". */
|
||||||
|
function formatKoteLabel(value: number): string {
|
||||||
|
if (Math.abs(value) < 1e-6) return "±0.00";
|
||||||
|
return (value > 0 ? "+" : "") + value.toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addDrawing2D(
|
export function addDrawing2D(
|
||||||
@@ -231,6 +243,38 @@ export function addDrawing2D(
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "kote": {
|
||||||
|
// SIA 400 B.5.4/Figur 18: OK = Spitze nach unten, UK = Spitze nach oben;
|
||||||
|
// fertig = Kontur (hohl), roh = voll gefüllt. `at` ist die Spitze.
|
||||||
|
const up = g.kind === "okFertig" || g.kind === "okRoh" ? 1 : -1;
|
||||||
|
const filled = g.kind === "okRoh" || g.kind === "ukRoh";
|
||||||
|
const pts: Vec2[] = [
|
||||||
|
g.at,
|
||||||
|
{ x: g.at.x - KOTE_TRI_HALF_W_M, y: g.at.y + up * KOTE_TRI_SIZE_M },
|
||||||
|
{ x: g.at.x + KOTE_TRI_HALF_W_M, y: g.at.y + up * KOTE_TRI_SIZE_M },
|
||||||
|
];
|
||||||
|
out.push({
|
||||||
|
kind: "polygon",
|
||||||
|
pts,
|
||||||
|
fill: filled ? color : "none",
|
||||||
|
stroke: color,
|
||||||
|
strokeWidthMm: weightMm,
|
||||||
|
hatch: NO_HATCH,
|
||||||
|
greyed,
|
||||||
|
drawingId: d.id,
|
||||||
|
});
|
||||||
|
out.push({
|
||||||
|
kind: "drawingText",
|
||||||
|
at: { x: g.at.x + KOTE_TRI_HALF_W_M + KOTE_TEXT_GAP_M, y: g.at.y + up * KOTE_TRI_SIZE_M * 0.5 },
|
||||||
|
text: formatKoteLabel(g.value),
|
||||||
|
heightM: KOTE_TEXT_HEIGHT_M,
|
||||||
|
angle: 0,
|
||||||
|
color,
|
||||||
|
greyed,
|
||||||
|
drawingId: d.id,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "text":
|
case "text":
|
||||||
out.push({
|
out.push({
|
||||||
kind: "drawingText",
|
kind: "drawingText",
|
||||||
|
|||||||
@@ -1529,6 +1529,8 @@ export function drawingVertices(d: import("../model/types").Drawing2D): Vec2[] {
|
|||||||
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: (g.a.x + g.b.x) / 2 + nx, y: (g.a.y + g.b.y) / 2 + ny };
|
||||||
return [g.a, g.b, mid];
|
return [g.a, g.b, mid];
|
||||||
}
|
}
|
||||||
|
// Kote: EIN Griff an der Dreieck-Spitze (zum Verschieben; siehe moveGrip).
|
||||||
|
if (g.shape === "kote") return [g.at];
|
||||||
// Text: EIN Griff am Ankerpunkt (zum Verschieben; siehe moveGrip).
|
// Text: EIN Griff am Ankerpunkt (zum Verschieben; siehe moveGrip).
|
||||||
if (g.shape === "text") return [g.at];
|
if (g.shape === "text") return [g.at];
|
||||||
return [];
|
return [];
|
||||||
@@ -1616,6 +1618,8 @@ function moveGrip(
|
|||||||
}
|
}
|
||||||
// Text: der einzige Griff (Index 0) sitzt am Ankerpunkt → verschiebt ihn.
|
// Text: der einzige Griff (Index 0) sitzt am Ankerpunkt → verschiebt ihn.
|
||||||
if (g.shape === "text") return { ...d, geom: { ...g, at: pt } };
|
if (g.shape === "text") return { ...d, geom: { ...g, at: pt } };
|
||||||
|
// Kote: der einzige Griff (Index 0) sitzt an der Dreieck-Spitze.
|
||||||
|
if (g.shape === "kote") return { ...d, geom: { ...g, at: pt } };
|
||||||
// Kreis: Griff 0 (Ost) bzw. 1 (Nord) — zieht man die beiden Achsen
|
// Kreis: Griff 0 (Ost) bzw. 1 (Nord) — zieht man die beiden Achsen
|
||||||
// unabhängig auseinander, wird der Kreis zur Ellipse (rotation 0);
|
// unabhängig auseinander, wird der Kreis zur Ellipse (rotation 0);
|
||||||
// bleiben rx/ry (fast) gleich, bleibt es ein Kreis (s. `ellipseOrCircle`).
|
// bleiben rx/ry (fast) gleich, bleibt es ein Kreis (s. `ellipseOrCircle`).
|
||||||
@@ -1697,6 +1701,8 @@ function moveElementBy(
|
|||||||
return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
|
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, a: mv(g.a), b: mv(g.b) } };
|
||||||
|
case "kote":
|
||||||
|
return { ...d, geom: { ...g, at: mv(g.at) } };
|
||||||
case "text":
|
case "text":
|
||||||
return { ...d, geom: { ...g, at: mv(g.at) } };
|
return { ...d, geom: { ...g, at: mv(g.at) } };
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -554,6 +554,7 @@ function geomBBox(g: Drawing2DGeom): SelectionBBox {
|
|||||||
acc(g.b.x + nx, g.b.y + ny);
|
acc(g.b.x + nx, g.b.y + ny);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "kote":
|
||||||
case "text":
|
case "text":
|
||||||
acc(g.at.x, g.at.y);
|
acc(g.at.x, g.at.y);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ function drawingsBBox(
|
|||||||
} else if (g.shape === "dimension") {
|
} else if (g.shape === "dimension") {
|
||||||
acc(g.a.x, g.a.y);
|
acc(g.a.x, g.a.y);
|
||||||
acc(g.b.x, g.b.y);
|
acc(g.b.x, g.b.y);
|
||||||
} else if (g.shape === "text") 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);
|
||||||
}
|
}
|
||||||
return Number.isFinite(minX) ? { minX, minY, maxX, maxY } : null;
|
return Number.isFinite(minX) ? { minX, minY, maxX, maxY } : null;
|
||||||
}
|
}
|
||||||
@@ -76,6 +77,7 @@ function translateDrawing(d: Drawing2D, dx: number, dy: number): Drawing2D {
|
|||||||
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, a: mv(g.a), b: mv(g.b) } };
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -606,6 +606,19 @@ const dimensionTool: Tool = {
|
|||||||
onCancel: (s) => [s, { draft: null, done: true }],
|
onCancel: (s) => [s, { draft: null, done: true }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Höhenkote: Platzhalter-Werkzeug, gekoppelt an den `kote`-Befehl (Punkt →
|
||||||
|
// getippter Höhenwert). Nicht floorOnly.
|
||||||
|
const koteTool: Tool = {
|
||||||
|
id: "kote",
|
||||||
|
labelKey: "tool.kote",
|
||||||
|
hintKey: () => "tool.kote.hint",
|
||||||
|
init: () => ({ phase: "idle" }),
|
||||||
|
onClick: (s) => [s, { draft: null }],
|
||||||
|
onMove: (s) => [s, { draft: null }],
|
||||||
|
onCommitGesture: (s) => [s, { draft: null, done: true }],
|
||||||
|
onCancel: (s) => [s, { draft: null, done: true }],
|
||||||
|
};
|
||||||
|
|
||||||
// Text: Platzhalter-Werkzeug, gekoppelt an den `text`-Befehl (Ankerpunkt →
|
// Text: Platzhalter-Werkzeug, gekoppelt an den `text`-Befehl (Ankerpunkt →
|
||||||
// getippte Zeile). Wie Kreis/Bogen nicht floorOnly (freies 2D-Element).
|
// getippte Zeile). Wie Kreis/Bogen nicht floorOnly (freies 2D-Element).
|
||||||
const textTool: Tool = {
|
const textTool: Tool = {
|
||||||
@@ -667,6 +680,7 @@ const TOOLS: Record<ToolId, Tool> = {
|
|||||||
spline: splineTool,
|
spline: splineTool,
|
||||||
bezier: bezierTool,
|
bezier: bezierTool,
|
||||||
dimension: dimensionTool,
|
dimension: dimensionTool,
|
||||||
|
kote: koteTool,
|
||||||
text: textTool,
|
text: textTool,
|
||||||
textbox: textboxTool,
|
textbox: textboxTool,
|
||||||
image: imageTool,
|
image: imageTool,
|
||||||
|
|||||||
@@ -187,6 +187,8 @@ const mvGeom = (geom: Drawing2DGeom, fn: (p: Vec2) => Vec2): Drawing2DGeom => {
|
|||||||
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, a: fn(geom.a), b: fn(geom.b) };
|
||||||
|
case "kote":
|
||||||
|
return { ...geom, at: fn(geom.at) };
|
||||||
case "text":
|
case "text":
|
||||||
return { ...geom, at: fn(geom.at) };
|
return { ...geom, at: fn(geom.at) };
|
||||||
case "image": {
|
case "image": {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export type ToolId =
|
|||||||
| "spline"
|
| "spline"
|
||||||
| "bezier"
|
| "bezier"
|
||||||
| "dimension"
|
| "dimension"
|
||||||
|
| "kote"
|
||||||
| "text"
|
| "text"
|
||||||
| "textbox"
|
| "textbox"
|
||||||
| "image";
|
| "image";
|
||||||
|
|||||||
@@ -47,6 +47,6 @@ export function drawingMoveAnchor(d: Drawing2D, verts: Vec2[]): Vec2 | null {
|
|||||||
}
|
}
|
||||||
const g = d.geom;
|
const g = d.geom;
|
||||||
if (g.shape === "circle" || g.shape === "arc" || g.shape === "ellipse") return g.center;
|
if (g.shape === "circle" || g.shape === "arc" || g.shape === "ellipse") return g.center;
|
||||||
if (g.shape === "text") return g.at;
|
if (g.shape === "text" || g.shape === "kote") return g.at;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user