2dc57d01a5
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.
178 lines
6.9 KiB
TypeScript
178 lines
6.9 KiB
TypeScript
/**
|
|
* `dimensionCommand` — Punktreihe (wie Polylinie) + EIN Offset-Klick →
|
|
* eine oder mehrere `shape:"dimension"`-Elemente (Reihenbemassung, SIA 400
|
|
* Figur 14). Die Masszahl wird NICHT gespeichert (aus dist(a,b) beim Rendern
|
|
* abgeleitet).
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { dimensionCommand } from "./dimension";
|
|
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("dimensionCommand", () => {
|
|
it("zwei Punkte + Enter + Offset-Klick (links positiv) → ein shape:dimension mit korrektem Vorzeichen", () => {
|
|
const p = project();
|
|
const ctx = makeCtx(p);
|
|
let [state] = dimensionCommand.onInput(
|
|
dimensionCommand.init(),
|
|
{ kind: "point", point: { x: 0, y: 0 } },
|
|
ctx,
|
|
);
|
|
[state] = dimensionCommand.onInput(state, { kind: "point", point: { x: 10, y: 0 } }, ctx);
|
|
[state] = dimensionCommand.onConfirm(state, ctx);
|
|
// Cursor bei y=2 (oberhalb von a→b, entlang +x) → Links-Normale zeigt nach +y.
|
|
const [, result] = dimensionCommand.onInput(state, { kind: "point", point: { x: 5, y: 2 } }, 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("dimension");
|
|
if (g.shape === "dimension") {
|
|
expect(g.a).toEqual({ x: 0, y: 0 });
|
|
expect(g.b).toEqual({ x: 10, y: 0 });
|
|
expect(g.offset).toBeCloseTo(2, 6);
|
|
}
|
|
});
|
|
|
|
it("Fertig-Option (statt Enter) beendet die Punktreihe identisch", () => {
|
|
const p = project();
|
|
const ctx = makeCtx(p);
|
|
let [state] = dimensionCommand.onInput(
|
|
dimensionCommand.init(),
|
|
{ kind: "point", point: { x: 0, y: 0 } },
|
|
ctx,
|
|
);
|
|
[state] = dimensionCommand.onInput(state, { kind: "point", point: { x: 4, y: 0 } }, ctx);
|
|
[state] = dimensionCommand.onInput(state, { kind: "option", id: "finish" }, ctx);
|
|
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 1.5 }, ctx);
|
|
const next = result.commit!(p);
|
|
const g = next.drawings2d[0].geom;
|
|
if (g.shape === "dimension") expect(g.offset).toBe(1.5);
|
|
});
|
|
|
|
it("Kette aus drei Punkten (Reihenbemassung) → zwei Segmente mit demselben Offset", () => {
|
|
const p = project();
|
|
const ctx = makeCtx(p);
|
|
let [state] = dimensionCommand.onInput(
|
|
dimensionCommand.init(),
|
|
{ kind: "point", point: { x: 0, y: 0 } },
|
|
ctx,
|
|
);
|
|
[state] = dimensionCommand.onInput(state, { kind: "point", point: { x: 4, y: 0 } }, ctx);
|
|
[state] = dimensionCommand.onInput(state, { kind: "point", point: { x: 10, y: 0 } }, ctx);
|
|
[state] = dimensionCommand.onConfirm(state, ctx);
|
|
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 2 }, ctx);
|
|
const next = result.commit!(p);
|
|
expect(next.drawings2d.length).toBe(2);
|
|
const segs = next.drawings2d.map((d) => d.geom).filter((g) => g.shape === "dimension");
|
|
expect(segs[0]).toMatchObject({ a: { x: 0, y: 0 }, b: { x: 4, 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", () => {
|
|
const p = project();
|
|
const ctx = makeCtx(p);
|
|
let [state] = dimensionCommand.onInput(
|
|
dimensionCommand.init(),
|
|
{ kind: "point", point: { x: 0, y: 0 } },
|
|
ctx,
|
|
);
|
|
[state] = dimensionCommand.onInput(state, { kind: "point", point: { x: 4, y: 0 } }, ctx);
|
|
[state] = dimensionCommand.onInput(state, { kind: "point", point: { x: 9, y: 0 } }, ctx);
|
|
[state] = dimensionCommand.onInput(state, { kind: "option", id: "undo" }, ctx);
|
|
[state] = dimensionCommand.onConfirm(state, ctx);
|
|
const [, result] = dimensionCommand.onInput(state, { kind: "number", value: 1 }, ctx);
|
|
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;
|
|
if (g.shape === "dimension") expect(g.b).toEqual({ x: 4, y: 0 });
|
|
});
|
|
|
|
it("entartetes Segment (identische Punkte) innerhalb der Kette wird übersprungen", () => {
|
|
const p = project();
|
|
const ctx = makeCtx(p);
|
|
let [state] = dimensionCommand.onInput(
|
|
dimensionCommand.init(),
|
|
{ kind: "point", point: { x: 3, y: 3 } },
|
|
ctx,
|
|
);
|
|
[state] = dimensionCommand.onInput(state, { kind: "point", point: { x: 3, 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(0);
|
|
});
|
|
|
|
it("Enter mit nur einem Punkt bricht ohne commit ab", () => {
|
|
const p = project();
|
|
const ctx = makeCtx(p);
|
|
const [state] = dimensionCommand.onInput(
|
|
dimensionCommand.init(),
|
|
{ kind: "point", point: { x: 0, y: 0 } },
|
|
ctx,
|
|
);
|
|
const [, result] = dimensionCommand.onConfirm(state, ctx);
|
|
expect(result.commit).toBeUndefined();
|
|
expect(result.done).toBe(true);
|
|
});
|
|
});
|