2D: Bemassungs-Werkzeug nach SIA 400 B.5.3
Neue Masslinie (shape:"dimension"): zwei Punkte + Lage der Masslinie. Folgt der Norm statt DIN-Gewohnheiten: Massbegrenzungslinien als Massstriche (doppelte Strichstärke, keine Pfeilspitzen), Masszahl mit Komma- Dezimaltrennzeichen, nie kopfüber lesbar (dreht sich automatisch). Die Masszahl wird nicht gespeichert, sondern immer aus dem aktuellen Abstand der beiden Punkte abgeleitet — bleibt beim Griff-Ziehen automatisch korrekt. Drei Griffe (beide Endpunkte + ein Griff auf der Masslinie zum Verschieben des Abstands), Verschieben/Rotieren, Georef-Rebase und Plan-Einpassen entsprechend erweitert.
This commit is contained in:
@@ -222,6 +222,7 @@ const TOOL_COMMAND: Partial<Record<ToolId, string>> = {
|
|||||||
ellipse: "ellipse",
|
ellipse: "ellipse",
|
||||||
spline: "spline",
|
spline: "spline",
|
||||||
bezier: "bezier",
|
bezier: "bezier",
|
||||||
|
dimension: "dimension",
|
||||||
text: "text",
|
text: "text",
|
||||||
textbox: "textbox",
|
textbox: "textbox",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* `dimensionCommand` — zwei Punkte + Lage der Masslinie → `shape:"dimension"`.
|
||||||
|
* 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 + Offset-Klick (links positiv) → 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);
|
||||||
|
// 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("getippte Offset-Zahl committet direkt (kein Punkt-Klick nötig)", () => {
|
||||||
|
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: 0, y: 5 } }, 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("Null-Strecke (a===b) committet nichts", () => {
|
||||||
|
const p = project();
|
||||||
|
const ctx = makeCtx(p);
|
||||||
|
let [state] = dimensionCommand.onInput(
|
||||||
|
dimensionCommand.init(),
|
||||||
|
{ kind: "point", point: { x: 3, y: 3 } },
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
const [, result] = dimensionCommand.onInput(state, { kind: "point", point: { x: 3, y: 3 } }, ctx);
|
||||||
|
expect(result.commit).toBeUndefined();
|
||||||
|
expect(result.done).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// Bemassung (lineare Masslinie, SIA 400 B.5.3) — misst den Abstand zweier
|
||||||
|
// Punkte. Schritte:
|
||||||
|
// 1) „Erster Punkt:" → Punkt (a)
|
||||||
|
// 2) „Zweiter Punkt:" → Punkt (b) — zeigt die Länge live
|
||||||
|
// 3) „Lage der Masslinie:" → Punkt/Zahl (Normalabstand zu a-b,
|
||||||
|
// links von a→b positiv) → commit (shape:"dimension")
|
||||||
|
//
|
||||||
|
// Die Masszahl wird beim Rendern aus dist(a,b) abgeleitet (nicht gespeichert)
|
||||||
|
// — bleibt beim späteren Griff-Ziehen automatisch korrekt.
|
||||||
|
|
||||||
|
import type { Drawing2D } from "../../model/types";
|
||||||
|
import { uniqueId } from "../../tools/types";
|
||||||
|
import type {
|
||||||
|
Command,
|
||||||
|
CommandContext,
|
||||||
|
CommandField,
|
||||||
|
CommandResult,
|
||||||
|
CommandState,
|
||||||
|
DraftShape,
|
||||||
|
Project,
|
||||||
|
ToolDraft,
|
||||||
|
Vec2,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
const EPS = 1e-6;
|
||||||
|
const dist = (a: Vec2, b: Vec2): number => Math.hypot(b.x - a.x, b.y - a.y);
|
||||||
|
|
||||||
|
/** Vorzeichenbehafteter Normalabstand von `pt` zur Geraden a→b (links positiv). */
|
||||||
|
function signedOffset(a: Vec2, b: Vec2, pt: Vec2): number {
|
||||||
|
const dx = b.x - a.x, dy = b.y - a.y;
|
||||||
|
const len = Math.hypot(dx, dy);
|
||||||
|
if (len < EPS) return 0;
|
||||||
|
const nx = -dy / len, ny = dx / len;
|
||||||
|
return (pt.x - a.x) * nx + (pt.y - a.y) * ny;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DimStart extends CommandState {
|
||||||
|
phase: "start";
|
||||||
|
}
|
||||||
|
interface DimEnd extends CommandState {
|
||||||
|
phase: "end";
|
||||||
|
a: Vec2;
|
||||||
|
cursor: Vec2 | null;
|
||||||
|
}
|
||||||
|
interface DimOffset extends CommandState {
|
||||||
|
phase: "offset";
|
||||||
|
a: Vec2;
|
||||||
|
b: Vec2;
|
||||||
|
cursor: Vec2 | null;
|
||||||
|
}
|
||||||
|
type DimState = DimStart | DimEnd | DimOffset;
|
||||||
|
|
||||||
|
const OFFSET_FIELDS: CommandField[] = [{ id: "offset", labelKey: "cmd.field.offset" }];
|
||||||
|
|
||||||
|
/** Vorschau: Masshilfslinien + Masslinie (grob, ohne Massstriche/Text — Feinheiten rendert generatePlan). */
|
||||||
|
function dimDraft(a: Vec2, b: Vec2, offset: number, at: Vec2 | null): ToolDraft {
|
||||||
|
const preview: DraftShape[] = [];
|
||||||
|
if (Math.abs(offset) > EPS) {
|
||||||
|
const off = offsetPoints(a, b, offset);
|
||||||
|
preview.push({ kind: "line", a, b: off.a });
|
||||||
|
preview.push({ kind: "line", a: b, b: off.b });
|
||||||
|
preview.push({ kind: "line", a: off.a, b: off.b });
|
||||||
|
} else {
|
||||||
|
preview.push({ kind: "line", a, b });
|
||||||
|
}
|
||||||
|
const draft: ToolDraft = { preview, vertices: [a, b] };
|
||||||
|
if (at) draft.hud = { at, text: `${dist(a, b).toFixed(3)}m · Δ${offset.toFixed(3)}m` };
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Endpunkte der um `offset` parallel verschobenen Masslinie. */
|
||||||
|
function offsetPoints(a: Vec2, b: Vec2, offset: number): { a: Vec2; b: Vec2 } {
|
||||||
|
const dx = b.x - a.x, dy = b.y - a.y;
|
||||||
|
const len = Math.hypot(dx, dy) || 1;
|
||||||
|
const nx = (-dy / len) * offset, ny = (dx / len) * offset;
|
||||||
|
return { a: { x: a.x + nx, y: a.y + ny }, b: { x: b.x + nx, y: b.y + ny } };
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendDimension(p: Project, a: Vec2, b: Vec2, offset: number, ctx: CommandContext): Project {
|
||||||
|
if (dist(a, b) < EPS) return p;
|
||||||
|
const d: Drawing2D = {
|
||||||
|
id: uniqueId("dr2d"),
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: ctx.level.id,
|
||||||
|
categoryCode: ctx.defaultCategoryCode,
|
||||||
|
geom: { shape: "dimension", a, b, offset },
|
||||||
|
};
|
||||||
|
return { ...p, drawings2d: [...p.drawings2d, d] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const idle = (): [CommandState, CommandResult] => [
|
||||||
|
{ phase: "start", lastPoint: null },
|
||||||
|
{ draft: null, done: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const dimensionCommand: Command = {
|
||||||
|
name: "dimension",
|
||||||
|
labelKey: "cmd.dimension.label",
|
||||||
|
prompt: (s) => {
|
||||||
|
const phase = (s as DimState).phase;
|
||||||
|
return phase === "end" ? "cmd.dimension.end" : phase === "offset" ? "cmd.dimension.offset" : "cmd.dimension.start";
|
||||||
|
},
|
||||||
|
accepts: (s) => ((s as DimState).phase === "offset" ? ["point", "number"] : ["point"]),
|
||||||
|
options: () => [],
|
||||||
|
init: (): DimStart => ({ phase: "start", lastPoint: null }),
|
||||||
|
|
||||||
|
onInput: (state, input, ctx): [CommandState, CommandResult] => {
|
||||||
|
const s = state as DimState;
|
||||||
|
if (s.phase !== "offset") {
|
||||||
|
if (input.kind !== "point") return [s, { draft: null }];
|
||||||
|
const pt = input.point;
|
||||||
|
if (s.phase !== "end") {
|
||||||
|
const ns: DimEnd = { phase: "end", a: pt, cursor: pt, lastPoint: pt };
|
||||||
|
return [ns, { draft: { preview: [], vertices: [pt] } }];
|
||||||
|
}
|
||||||
|
if (dist(s.a, pt) < EPS) return idle();
|
||||||
|
const ns: DimOffset = { phase: "offset", a: s.a, b: pt, cursor: pt, lastPoint: pt };
|
||||||
|
return [ns, { draft: dimDraft(s.a, pt, 0, null) }];
|
||||||
|
}
|
||||||
|
// Offset-Schritt: Zahl = direkter Abstand, Punkt = Abstand zum Cursor.
|
||||||
|
let offset: number;
|
||||||
|
if (input.kind === "number") offset = input.value;
|
||||||
|
else if (input.kind === "point") offset = signedOffset(s.a, s.b, input.point);
|
||||||
|
else return [s, { draft: dimDraft(s.a, s.b, 0, null) }];
|
||||||
|
const { a, b } = s;
|
||||||
|
return [
|
||||||
|
{ phase: "start", lastPoint: null },
|
||||||
|
{ draft: null, done: true, commit: (p) => appendDimension(p, a, b, offset, ctx) },
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
onMove: (state, point): [CommandState, CommandResult] => {
|
||||||
|
const s = state as DimState;
|
||||||
|
if (s.phase === "end") {
|
||||||
|
const ns: DimEnd = { ...s, cursor: point };
|
||||||
|
if (dist(s.a, point) < EPS) return [ns, { draft: { preview: [], vertices: [s.a] } }];
|
||||||
|
const draft: ToolDraft = {
|
||||||
|
preview: [{ kind: "line", a: s.a, b: point }],
|
||||||
|
vertices: [s.a],
|
||||||
|
hud: { at: point, text: `${dist(s.a, point).toFixed(3)}m` },
|
||||||
|
};
|
||||||
|
return [ns, { draft }];
|
||||||
|
}
|
||||||
|
if (s.phase === "offset") {
|
||||||
|
const off = signedOffset(s.a, s.b, point);
|
||||||
|
const ns: DimOffset = { ...s, cursor: point };
|
||||||
|
return [ns, { draft: dimDraft(s.a, s.b, off, point) }];
|
||||||
|
}
|
||||||
|
return [s, { draft: null }];
|
||||||
|
},
|
||||||
|
|
||||||
|
onConfirm: (): [CommandState, CommandResult] => idle(),
|
||||||
|
onCancel: (): [CommandState, CommandResult] => idle(),
|
||||||
|
|
||||||
|
fields: (state) => ((state as DimState).phase === "offset" ? OFFSET_FIELDS : []),
|
||||||
|
fieldValues: (state, _locks, cursor): Record<string, number> => {
|
||||||
|
const s = state as DimState;
|
||||||
|
if (s.phase !== "offset" || !cursor) return {};
|
||||||
|
return { offset: signedOffset(s.a, s.b, cursor) };
|
||||||
|
},
|
||||||
|
pointFromFields: (state, locks, cursor) => {
|
||||||
|
const s = state as DimState;
|
||||||
|
if (s.phase !== "offset") return null;
|
||||||
|
const offset = "offset" in locks ? locks.offset : cursor ? signedOffset(s.a, s.b, cursor) : 0;
|
||||||
|
return offsetPoints(s.a, s.b, offset).a;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -20,6 +20,7 @@ import { arcCommand } from "./cmds/arc";
|
|||||||
import { ellipseCommand } from "./cmds/ellipse";
|
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 { 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";
|
||||||
@@ -59,6 +60,7 @@ export const COMMANDS: Record<string, Command> = {
|
|||||||
ellipse: ellipseCommand,
|
ellipse: ellipseCommand,
|
||||||
spline: splineCommand,
|
spline: splineCommand,
|
||||||
bezier: bezierCommand,
|
bezier: bezierCommand,
|
||||||
|
dimension: dimensionCommand,
|
||||||
text: textCommand,
|
text: textCommand,
|
||||||
textbox: textboxCommand,
|
textbox: textboxCommand,
|
||||||
move: moveCommand,
|
move: moveCommand,
|
||||||
@@ -112,6 +114,8 @@ export const ALIASES: Record<string, string> = {
|
|||||||
bogen: "arc",
|
bogen: "arc",
|
||||||
el: "ellipse",
|
el: "ellipse",
|
||||||
sp: "spline",
|
sp: "spline",
|
||||||
|
dim: "dimension",
|
||||||
|
bemassung: "dimension",
|
||||||
bz: "bezier",
|
bz: "bezier",
|
||||||
tx: "text",
|
tx: "text",
|
||||||
txt: "text",
|
txt: "text",
|
||||||
|
|||||||
@@ -184,6 +184,8 @@ export const de = {
|
|||||||
"tool.spline.hint": "Spline: Wegpunkte klicken, Enter beendet (glatte Kurve durch die Punkte)",
|
"tool.spline.hint": "Spline: Wegpunkte klicken, Enter beendet (glatte Kurve durch die Punkte)",
|
||||||
"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.hint": "Bemassung: Erster Punkt, zweiter Punkt, dann Lage der Masslinie",
|
||||||
"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",
|
||||||
@@ -1160,6 +1162,11 @@ export const de = {
|
|||||||
"cmd.bezier.c2": "Griff 2:",
|
"cmd.bezier.c2": "Griff 2:",
|
||||||
"cmd.bezier.end": "Endpunkt ( Fertig ):",
|
"cmd.bezier.end": "Endpunkt ( Fertig ):",
|
||||||
"cmd.bezier.finish": "Fertig",
|
"cmd.bezier.finish": "Fertig",
|
||||||
|
"cmd.dimension.label": "Bemassung",
|
||||||
|
"cmd.dimension.start": "Erster Punkt:",
|
||||||
|
"cmd.dimension.end": "Zweiter Punkt:",
|
||||||
|
"cmd.dimension.offset": "Lage der Masslinie:",
|
||||||
|
"cmd.field.offset": "Abstand",
|
||||||
"cmd.text.label": "Text",
|
"cmd.text.label": "Text",
|
||||||
"cmd.text.point": "Ankerpunkt:",
|
"cmd.text.point": "Ankerpunkt:",
|
||||||
"cmd.textbox.label": "Textspalte",
|
"cmd.textbox.label": "Textspalte",
|
||||||
|
|||||||
@@ -182,6 +182,8 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"tool.spline.hint": "Spline: click waypoints, Enter finishes (smooth curve through the points)",
|
"tool.spline.hint": "Spline: click waypoints, Enter finishes (smooth curve through the points)",
|
||||||
"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.hint": "Dimension: first point, second point, then dimension line position",
|
||||||
"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",
|
||||||
@@ -1149,6 +1151,11 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"cmd.bezier.c2": "Handle 2:",
|
"cmd.bezier.c2": "Handle 2:",
|
||||||
"cmd.bezier.end": "End point ( Finish ):",
|
"cmd.bezier.end": "End point ( Finish ):",
|
||||||
"cmd.bezier.finish": "Finish",
|
"cmd.bezier.finish": "Finish",
|
||||||
|
"cmd.dimension.label": "Dimension",
|
||||||
|
"cmd.dimension.start": "First point:",
|
||||||
|
"cmd.dimension.end": "Second point:",
|
||||||
|
"cmd.dimension.offset": "Dimension line position:",
|
||||||
|
"cmd.field.offset": "Offset",
|
||||||
"cmd.text.label": "Text",
|
"cmd.text.label": "Text",
|
||||||
"cmd.text.point": "Anchor point:",
|
"cmd.text.point": "Anchor point:",
|
||||||
"cmd.textbox.label": "Text column",
|
"cmd.textbox.label": "Text column",
|
||||||
|
|||||||
@@ -112,6 +112,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 "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":
|
||||||
|
return { ...d, geom: { ...g, a: shift(g.a, dx, dy), b: shift(g.b, 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":
|
||||||
|
|||||||
@@ -1524,6 +1524,16 @@ export type Drawing2DGeom =
|
|||||||
* schliesst den Ring (letzter Wegpunkt → erster).
|
* schliesst den Ring (letzter Wegpunkt → erster).
|
||||||
*/
|
*/
|
||||||
| { shape: "spline"; pts: Vec2[]; closed: boolean }
|
| { shape: "spline"; pts: Vec2[]; closed: boolean }
|
||||||
|
/**
|
||||||
|
* Masslinie (lineare Bemassung, SIA 400 B.5.3): misst den Abstand `a`↔`b`.
|
||||||
|
* `offset` ist der VORZEICHENBEHAFTETE Normalabstand (Modell-Meter, links
|
||||||
|
* von a→b positiv), in dem die eigentliche Masslinie parallel zu a-b liegt
|
||||||
|
* — Masshilfslinien laufen von a/b bis zu dieser Linie. Die Masszahl wird
|
||||||
|
* beim Rendern aus `dist(a,b)` abgeleitet (nicht gespeichert), bleibt also
|
||||||
|
* beim Verschieben der Griffe automatisch korrekt. Rein 2D (kein Bezug auf
|
||||||
|
* andere Elemente — Assoziativität nur über die eigenen Endpunkte).
|
||||||
|
*/
|
||||||
|
| { shape: "dimension"; a: Vec2; b: Vec2; offset: number }
|
||||||
| {
|
| {
|
||||||
shape: "text";
|
shape: "text";
|
||||||
at: Vec2;
|
at: Vec2;
|
||||||
|
|||||||
@@ -168,6 +168,17 @@ export function ToolIcon({ id }: { id: ToolId }) {
|
|||||||
<rect x="7.2" y="12.2" width="1.6" height="1.6" fill="currentColor" stroke="none" />
|
<rect x="7.2" y="12.2" width="1.6" height="1.6" fill="currentColor" stroke="none" />
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
case "dimension":
|
||||||
|
// Masslinie mit Massstrichen (Diagonalstriche) an beiden Enden.
|
||||||
|
return (
|
||||||
|
<svg {...common}>
|
||||||
|
<line x1="2.5" y1="4" x2="2.5" y2="11" strokeWidth="0.9" />
|
||||||
|
<line x1="13.5" y1="4" x2="13.5" y2="11" strokeWidth="0.9" />
|
||||||
|
<line x1="2.5" y1="8" x2="13.5" y2="8" />
|
||||||
|
<line x1="1.7" y1="6.6" x2="3.3" y2="9.4" strokeWidth="1.6" />
|
||||||
|
<line x1="12.7" y1="6.6" x2="14.3" y2="9.4" strokeWidth="1.6" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
case "text":
|
case "text":
|
||||||
// Serifen-„A" als Text-Symbol.
|
// Serifen-„A" als Text-Symbol.
|
||||||
return (
|
return (
|
||||||
@@ -213,6 +224,7 @@ const TOOLS_2D: ToolId[] = [
|
|||||||
"ellipse",
|
"ellipse",
|
||||||
"spline",
|
"spline",
|
||||||
"bezier",
|
"bezier",
|
||||||
|
"dimension",
|
||||||
"text",
|
"text",
|
||||||
"textbox",
|
"textbox",
|
||||||
"image",
|
"image",
|
||||||
|
|||||||
@@ -729,6 +729,9 @@ function computeBounds(
|
|||||||
acc({ x: g.center.x + g.rx, y: g.center.y + g.ry });
|
acc({ x: g.center.x + g.rx, y: g.center.y + g.ry });
|
||||||
} 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") {
|
||||||
|
acc(g.a);
|
||||||
|
acc(g.b);
|
||||||
} 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); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,3 +121,56 @@ describe("addDrawing2D — spline", () => {
|
|||||||
expect(last.b).toEqual(lines[0].a);
|
expect(last.b).toEqual(lines[0].a);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("addDrawing2D — dimension (SIA 400 B.5.3)", () => {
|
||||||
|
it("gibt 2 Masshilfslinien + 1 Masslinie (dünn) + 2 Massstriche (doppelte Stärke) + 1 Masszahl-Text aus", () => {
|
||||||
|
const d: Drawing2D = {
|
||||||
|
id: "dim1",
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: "eg",
|
||||||
|
categoryCode: "20",
|
||||||
|
geom: { shape: "dimension", a: { x: 0, y: 0 }, b: { x: 4, y: 0 }, offset: 1 },
|
||||||
|
};
|
||||||
|
const out: Primitive[] = [];
|
||||||
|
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
||||||
|
const lines = out.filter((p): p is Extract<Primitive, { kind: "line" }> => p.kind === "line");
|
||||||
|
const texts = out.filter((p) => p.kind === "drawingText");
|
||||||
|
expect(lines.length).toBe(5); // 2 Masshilfslinien + 1 Masslinie + 2 Massstriche
|
||||||
|
expect(texts.length).toBe(1);
|
||||||
|
const weights = lines.map((l) => l.weightMm).sort((a, b) => a - b);
|
||||||
|
const thin = weights[0];
|
||||||
|
const thick = weights[weights.length - 1];
|
||||||
|
// 3× dünn (Masshilfslinien + Masslinie), 2× doppelt so dick (Massstriche).
|
||||||
|
expect(weights.filter((w) => w === thin).length).toBe(3);
|
||||||
|
expect(weights.filter((w) => w === thick).length).toBe(2);
|
||||||
|
expect(thick).toBeCloseTo(thin * 2, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Masszahl entspricht dist(a,b) mit Komma-Dezimaltrennzeichen (SIA 400 B.5.2)", () => {
|
||||||
|
const d: Drawing2D = {
|
||||||
|
id: "dim2",
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: "eg",
|
||||||
|
categoryCode: "20",
|
||||||
|
geom: { shape: "dimension", a: { x: 0, y: 0 }, b: { x: 3, y: 4 }, offset: 0.5 }, // 3-4-5-Dreieck → 5m
|
||||||
|
};
|
||||||
|
const out: Primitive[] = [];
|
||||||
|
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
||||||
|
const text = out.find((p) => p.kind === "drawingText");
|
||||||
|
expect(text).toBeDefined();
|
||||||
|
if (text?.kind === "drawingText") expect(text.text).toBe("5,00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("entartete Strecke (a===b) erzeugt keine Primitive", () => {
|
||||||
|
const d: Drawing2D = {
|
||||||
|
id: "dim3",
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: "eg",
|
||||||
|
categoryCode: "20",
|
||||||
|
geom: { shape: "dimension", a: { x: 2, y: 2 }, b: { x: 2, y: 2 }, offset: 1 },
|
||||||
|
};
|
||||||
|
const out: Primitive[] = [];
|
||||||
|
addDrawing2D(out, project(), d, false, lwByCode, colorByCode, catByCode);
|
||||||
|
expect(out.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -15,6 +15,21 @@ import {
|
|||||||
} from "./shared";
|
} from "./shared";
|
||||||
import { ellipsePts, sampleBezierChain, sampleCatmullRom } from "../../tools/curves";
|
import { ellipsePts, sampleBezierChain, sampleCatmullRom } from "../../tools/curves";
|
||||||
|
|
||||||
|
// ── Bemassung (SIA 400 B.5.3) ────────────────────────────────────────────────
|
||||||
|
// Modell-Meter-Konstanten für rein grafische (nicht reale) Bemassungs-Details
|
||||||
|
// — analog zu DEFAULT_TEXT_HEIGHT_M (text.ts): fest gewählt, damit sie bei der
|
||||||
|
// üblichen Plan-Massstäben (1:50…1:100) lesbar wirken, nicht dynamisch an den
|
||||||
|
// Massstab gekoppelt (wie auch die übrige Textgrösse im Plan).
|
||||||
|
const DIM_GAP_M = 0.05; // Abstand Masshilfslinie → bemessener Punkt
|
||||||
|
const DIM_OVERSHOOT_M = 0.05; // Überstand der Masshilfslinie über die Masslinie hinaus
|
||||||
|
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
|
||||||
|
|
||||||
|
/** Formatiert eine Länge (Meter) als Masszahl: 2 Dezimalstellen, Komma (SIA 400 B.5.2, „2,75"). */
|
||||||
|
function formatDimLabel(m: number): string {
|
||||||
|
return m.toFixed(2).replace(".", ",");
|
||||||
|
}
|
||||||
|
|
||||||
export function addDrawing2D(
|
export function addDrawing2D(
|
||||||
out: Primitive[],
|
out: Primitive[],
|
||||||
project: Project,
|
project: Project,
|
||||||
@@ -157,6 +172,65 @@ export function addDrawing2D(
|
|||||||
if (g.closed && g.pts.length > 2) seg(pts[pts.length - 1], pts[0]);
|
if (g.closed && g.pts.length > 2) seg(pts[pts.length - 1], pts[0]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "dimension": {
|
||||||
|
// SIA 400 B.5.3: Masshilfslinien + Masslinie in der dünnsten Linie;
|
||||||
|
// Massbegrenzungslinien (Massstriche) doppelt so dick; Masszahl aus
|
||||||
|
// dist(a,b) — nicht gespeichert, folgt Griff-Änderungen automatisch.
|
||||||
|
const dx = g.b.x - g.a.x, dy = g.b.y - g.a.y;
|
||||||
|
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;
|
||||||
|
}
|
||||||
case "text":
|
case "text":
|
||||||
out.push({
|
out.push({
|
||||||
kind: "drawingText",
|
kind: "drawingText",
|
||||||
|
|||||||
@@ -1520,6 +1520,15 @@ 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
|
||||||
|
// AUF der Masslinie (Mitte) — ihn zu ziehen ändert nur `offset` (s. moveGrip).
|
||||||
|
if (g.shape === "dimension") {
|
||||||
|
const dx = g.b.x - g.a.x, dy = g.b.y - g.a.y;
|
||||||
|
const len = Math.hypot(dx, dy) || 1;
|
||||||
|
const nx = (-dy / len) * g.offset, ny = (dx / len) * g.offset;
|
||||||
|
const mid: Vec2 = { x: (g.a.x + g.b.x) / 2 + nx, y: (g.a.y + g.b.y) / 2 + ny };
|
||||||
|
return [g.a, g.b, mid];
|
||||||
|
}
|
||||||
// Text: EIN Griff am Ankerpunkt (zum Verschieben; siehe moveGrip).
|
// 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 [];
|
||||||
@@ -1581,6 +1590,18 @@ 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
|
||||||
|
// folgt automatisch, s. Rendering); Griff 2 (auf der Masslinie) ändert
|
||||||
|
// nur `offset` — Normalabstand von `pt` zur (unveränderten) Strecke a-b.
|
||||||
|
if (g.shape === "dimension") {
|
||||||
|
if (index === 0) return { ...d, geom: { ...g, a: pt } };
|
||||||
|
if (index === 1) return { ...d, geom: { ...g, b: pt } };
|
||||||
|
const dx = g.b.x - g.a.x, dy = g.b.y - g.a.y;
|
||||||
|
const len = Math.hypot(dx, dy) || 1;
|
||||||
|
const nx = -dy / len, ny = dx / len;
|
||||||
|
const offset = (pt.x - g.a.x) * nx + (pt.y - g.a.y) * ny;
|
||||||
|
return { ...d, geom: { ...g, offset } };
|
||||||
|
}
|
||||||
if (g.shape === "rect" || g.shape === "image") {
|
if (g.shape === "rect" || g.shape === "image") {
|
||||||
const corners = [
|
const corners = [
|
||||||
g.min,
|
g.min,
|
||||||
@@ -1674,6 +1695,8 @@ function moveElementBy(
|
|||||||
case "bezier":
|
case "bezier":
|
||||||
case "spline":
|
case "spline":
|
||||||
return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
|
return { ...d, geom: { ...g, pts: g.pts.map(mv) } };
|
||||||
|
case "dimension":
|
||||||
|
return { ...d, geom: { ...g, a: mv(g.a), b: mv(g.b) } };
|
||||||
case "text":
|
case "text":
|
||||||
return { ...d, geom: { ...g, at: mv(g.at) } };
|
return { ...d, geom: { ...g, at: mv(g.at) } };
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -544,6 +544,16 @@ function geomBBox(g: Drawing2DGeom): SelectionBBox {
|
|||||||
case "spline":
|
case "spline":
|
||||||
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": {
|
||||||
|
acc(g.a.x, g.a.y);
|
||||||
|
acc(g.b.x, g.b.y);
|
||||||
|
const dx = g.b.x - g.a.x, dy = g.b.y - g.a.y;
|
||||||
|
const len = Math.hypot(dx, dy) || 1;
|
||||||
|
const nx = (-dy / len) * g.offset, ny = (dx / len) * g.offset;
|
||||||
|
acc(g.a.x + nx, g.a.y + ny);
|
||||||
|
acc(g.b.x + nx, g.b.y + ny);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "text":
|
case "text":
|
||||||
acc(g.at.x, g.at.y);
|
acc(g.at.x, g.at.y);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ function drawingsBBox(
|
|||||||
acc(g.center.x + g.rx, g.center.y + g.ry);
|
acc(g.center.x + g.rx, g.center.y + g.ry);
|
||||||
} 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") {
|
||||||
|
acc(g.a.x, g.a.y);
|
||||||
|
acc(g.b.x, g.b.y);
|
||||||
} else if (g.shape === "text") acc(g.at.x, g.at.y);
|
} 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;
|
||||||
@@ -72,6 +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 "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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -593,6 +593,19 @@ const bezierTool: Tool = {
|
|||||||
onCancel: (s) => [s, { draft: null, done: true }],
|
onCancel: (s) => [s, { draft: null, done: true }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Bemassung: Platzhalter-Werkzeug, gekoppelt an den `dimension`-Befehl (zwei
|
||||||
|
// Punkte + Lage der Masslinie). Nicht floorOnly.
|
||||||
|
const dimensionTool: Tool = {
|
||||||
|
id: "dimension",
|
||||||
|
labelKey: "tool.dimension",
|
||||||
|
hintKey: () => "tool.dimension.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 = {
|
||||||
@@ -653,6 +666,7 @@ const TOOLS: Record<ToolId, Tool> = {
|
|||||||
ellipse: ellipseTool,
|
ellipse: ellipseTool,
|
||||||
spline: splineTool,
|
spline: splineTool,
|
||||||
bezier: bezierTool,
|
bezier: bezierTool,
|
||||||
|
dimension: dimensionTool,
|
||||||
text: textTool,
|
text: textTool,
|
||||||
textbox: textboxTool,
|
textbox: textboxTool,
|
||||||
image: imageTool,
|
image: imageTool,
|
||||||
|
|||||||
@@ -185,6 +185,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 "spline":
|
case "spline":
|
||||||
return { ...geom, pts: geom.pts.map(fn) };
|
return { ...geom, pts: geom.pts.map(fn) };
|
||||||
|
case "dimension":
|
||||||
|
return { ...geom, a: fn(geom.a), b: fn(geom.b) };
|
||||||
case "text":
|
case "text":
|
||||||
return { ...geom, at: fn(geom.at) };
|
return { ...geom, at: fn(geom.at) };
|
||||||
case "image": {
|
case "image": {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type ToolId =
|
|||||||
| "ellipse"
|
| "ellipse"
|
||||||
| "spline"
|
| "spline"
|
||||||
| "bezier"
|
| "bezier"
|
||||||
|
| "dimension"
|
||||||
| "text"
|
| "text"
|
||||||
| "textbox"
|
| "textbox"
|
||||||
| "image";
|
| "image";
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function drawingGripVertices(d: Drawing2D): Vec2[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (g.shape === "bezier" || g.shape === "spline") return [...g.pts];
|
if (g.shape === "bezier" || g.shape === "spline") return [...g.pts];
|
||||||
|
if (g.shape === "dimension") return [g.a, g.b];
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user