Raumstempel: Ausrichtung pro Zeile + Anker als Snappunkt
Jede Stempelzeile (Name, Name-2, Bodenflaeche, Nutzung) hat jetzt eine eigene Ausrichtung links/mitte/rechts, im Stempel-Editor pro Zeile waehlbar und in SVG- wie nativer Darstellung honoriert (text-anchor bzw. align). Bodenflaeche/Nutzung fallen auf mitte zurueck (bisheriges Bild). Der Stempel-Anker (stampAnchor bzw. Schwerpunkt) meldet sich zusaetzlich als Endpunkt-Snapkandidat an — gefiltert nach Geschoss und sichtbarer Kategorie; der ziehbare Griff war bereits verdrahtet.
This commit is contained in:
@@ -71,8 +71,8 @@ describe("roomStampExtraLines", () => {
|
|||||||
{ name: "Bad", showFloorArea: true, showUsage: true, floorAreaPrefix: "BF " },
|
{ name: "Bad", showFloorArea: true, showUsage: true, floorAreaPrefix: "BF " },
|
||||||
{ netArea: 12.345, siaCategory: "HNF", siaLabel: "Hauptnutzfläche" },
|
{ netArea: 12.345, siaCategory: "HNF", siaLabel: "Hauptnutzfläche" },
|
||||||
);
|
);
|
||||||
expect(lines[0]).toBe("BF 12.35 m²");
|
expect(lines[0].text).toBe("BF 12.35 m²");
|
||||||
expect(lines[1]).toBe("HNF · Hauptnutzfläche");
|
expect(lines[1].text).toBe("HNF · Hauptnutzfläche");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("blendet Zeilen aus, wenn Flags falsch", () => {
|
it("blendet Zeilen aus, wenn Flags falsch", () => {
|
||||||
|
|||||||
+24
-8
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import type { Room, RoomStamp } from "./types";
|
import type { Room, RoomStamp } from "./types";
|
||||||
import type { SiaCategory } from "../geometry/roomArea";
|
import type { SiaCategory } from "../geometry/roomArea";
|
||||||
import type { RichTextDoc, Paragraph, TextRun } from "../text/richText";
|
import type { RichTextDoc, Paragraph, TextRun, Align } from "../text/richText";
|
||||||
import { makeRun } from "../text/richText";
|
import { makeRun } from "../text/richText";
|
||||||
|
|
||||||
/** Grösse (pt) des Raumnamens (Zeile 1) relativ zur Stempel-Basis. */
|
/** Grösse (pt) des Raumnamens (Zeile 1) relativ zur Stempel-Basis. */
|
||||||
@@ -43,10 +43,13 @@ export function roomStampToDoc(stamp: RoomStamp): RichTextDoc {
|
|||||||
}
|
}
|
||||||
line1.push(makeRun(stamp.name, { bold: true, sizePt: NAME_PT }));
|
line1.push(makeRun(stamp.name, { bold: true, sizePt: NAME_PT }));
|
||||||
|
|
||||||
const paragraphs: Paragraph[] = [{ runs: line1 }];
|
const paragraphs: Paragraph[] = [{ runs: line1, align: stamp.nameAlign }];
|
||||||
const line2 = (stamp.nameLine2 ?? "").trim();
|
const line2 = (stamp.nameLine2 ?? "").trim();
|
||||||
if (line2) {
|
if (line2) {
|
||||||
paragraphs.push({ runs: [makeRun(line2, { italic: true, sizePt: LINE2_PT })] });
|
paragraphs.push({
|
||||||
|
runs: [makeRun(line2, { italic: true, sizePt: LINE2_PT })],
|
||||||
|
align: stamp.line2Align,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return { version: 1, paragraphs };
|
return { version: 1, paragraphs };
|
||||||
}
|
}
|
||||||
@@ -58,18 +61,31 @@ export interface RoomStampContext {
|
|||||||
siaLabel: string;
|
siaLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Eine Live-Zeile des Stempels mit Text und Ausrichtung. */
|
||||||
|
export interface RoomStampLine {
|
||||||
|
text: string;
|
||||||
|
align: Align;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Die Live-Zeilen unter dem Stempel: Bodenfläche (mit Präfix) wenn showFloorArea,
|
* Die Live-Zeilen unter dem Stempel: Bodenfläche (mit Präfix) wenn showFloorArea,
|
||||||
* Nutzung (SIA-Kürzel · Bezeichnung) wenn showUsage.
|
* Nutzung (SIA-Kürzel · Bezeichnung) wenn showUsage. Die Ausrichtung folgt den
|
||||||
|
* Stempel-Feldern; fehlt sie, gilt „zentriert" (Alt-Verhalten).
|
||||||
*/
|
*/
|
||||||
export function roomStampExtraLines(stamp: RoomStamp, ctx: RoomStampContext): string[] {
|
export function roomStampExtraLines(stamp: RoomStamp, ctx: RoomStampContext): RoomStampLine[] {
|
||||||
const lines: string[] = [];
|
const lines: RoomStampLine[] = [];
|
||||||
if (stamp.showFloorArea) {
|
if (stamp.showFloorArea) {
|
||||||
const prefix = stamp.floorAreaPrefix ?? "";
|
const prefix = stamp.floorAreaPrefix ?? "";
|
||||||
lines.push(`${prefix}${ctx.netArea.toFixed(2)} m²`);
|
lines.push({
|
||||||
|
text: `${prefix}${ctx.netArea.toFixed(2)} m²`,
|
||||||
|
align: stamp.floorAreaAlign ?? "center",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (stamp.showUsage) {
|
if (stamp.showUsage) {
|
||||||
lines.push(`${ctx.siaCategory} · ${ctx.siaLabel}`);
|
lines.push({
|
||||||
|
text: `${ctx.siaCategory} · ${ctx.siaLabel}`,
|
||||||
|
align: stamp.usageAlign ?? "center",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -16,7 +16,7 @@ export type Vec2 = { x: number; y: number };
|
|||||||
import type { SiaCategory } from "../geometry/roomArea";
|
import type { SiaCategory } from "../geometry/roomArea";
|
||||||
export type { SiaCategory } from "../geometry/roomArea";
|
export type { SiaCategory } from "../geometry/roomArea";
|
||||||
// Rich-Text-Dokument für den Raum-Stempel (frei editierbarer Teil).
|
// Rich-Text-Dokument für den Raum-Stempel (frei editierbarer Teil).
|
||||||
import type { RichTextDoc } from "../text/richText";
|
import type { RichTextDoc, Align } from "../text/richText";
|
||||||
export type { RichTextDoc } from "../text/richText";
|
export type { RichTextDoc } from "../text/richText";
|
||||||
|
|
||||||
// ── Ressourcen-Bibliotheken (Vectorworks-/DOSSIER-Stil) ────────────────────
|
// ── Ressourcen-Bibliotheken (Vectorworks-/DOSSIER-Stil) ────────────────────
|
||||||
@@ -679,6 +679,14 @@ export interface RoomStamp {
|
|||||||
floorAreaPrefix?: string;
|
floorAreaPrefix?: string;
|
||||||
/** Nutzung (HNF/… · Bezeichnung) anzeigen (Live-Zeile). */
|
/** Nutzung (HNF/… · Bezeichnung) anzeigen (Live-Zeile). */
|
||||||
showUsage: boolean;
|
showUsage: boolean;
|
||||||
|
/** Ausrichtung der Namenszeile (Zeile 1). Fehlt sie, gilt „links". */
|
||||||
|
nameAlign?: Align;
|
||||||
|
/** Ausrichtung der zweiten Namenszeile. Fehlt sie, gilt „links". */
|
||||||
|
line2Align?: Align;
|
||||||
|
/** Ausrichtung der Bodenflächen-Zeile. Fehlt sie, gilt „zentriert" (Alt-Verhalten). */
|
||||||
|
floorAreaAlign?: Align;
|
||||||
|
/** Ausrichtung der Nutzungs-Zeile. Fehlt sie, gilt „zentriert" (Alt-Verhalten). */
|
||||||
|
usageAlign?: Align;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Eine Tür, gehostet in einer Wand. Ihr Geschoss ergibt sich aus der Wand. */
|
/** Eine Tür, gehostet in einer Wand. Ihr Geschoss ergibt sich aus der Wand. */
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Schwebender Dialog zum Bearbeiten des strukturierten Raum-Stempels (Feldmodell):
|
// Schwebender Dialog zum Bearbeiten des strukturierten Raum-Stempels (Feldmodell):
|
||||||
// Raumnummer, Raumname (+ Zeile 2), Bodenfläche (mit Präfix) und Nutzung. Ersetzt
|
// Raumnummer, Raumname (+ Zeile 2), Bodenfläche (mit Präfix) und Nutzung. Jede
|
||||||
|
// Zeile trägt zusätzlich eine eigene Ausrichtung (links/mitte/rechts). Ersetzt
|
||||||
// für neue Räume den freien Text-Editor. Kontrolliert (value/onApply/onCancel);
|
// für neue Räume den freien Text-Editor. Kontrolliert (value/onApply/onCancel);
|
||||||
// arbeitet auf einem lokalen Entwurf, erst „Übernehmen" schreibt zurück.
|
// arbeitet auf einem lokalen Entwurf, erst „Übernehmen" schreibt zurück.
|
||||||
// Bezeichner englisch, UI-Text über t().
|
// Bezeichner englisch, UI-Text über t().
|
||||||
@@ -10,6 +11,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import type { RoomStamp } from "../model/types";
|
import type { RoomStamp } from "../model/types";
|
||||||
|
import type { Align } from "../text/richText";
|
||||||
|
|
||||||
export interface RoomStampEditorProps {
|
export interface RoomStampEditorProps {
|
||||||
/** Ausgangsstempel (wird beim Öffnen kopiert; Editor arbeitet lokal). */
|
/** Ausgangsstempel (wird beim Öffnen kopiert; Editor arbeitet lokal). */
|
||||||
@@ -20,11 +22,60 @@ export interface RoomStampEditorProps {
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Die drei Ausrichtungen mit Icon und Beschriftung (für die Umschalter). */
|
||||||
|
const ALIGN_CELLS: { align: Align; icon: string; titleKey: string }[] = [
|
||||||
|
{ align: "left", icon: "format_align_left", titleKey: "text.align.left" },
|
||||||
|
{ align: "center", icon: "format_align_center", titleKey: "text.align.center" },
|
||||||
|
{ align: "right", icon: "format_align_right", titleKey: "text.align.right" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drei-Wege-Umschalter für die Zeilen-Ausrichtung. `value` ist der aktuelle Wert
|
||||||
|
* (fehlt er, gilt `fallback`); `onChange` liefert die gewählte Ausrichtung.
|
||||||
|
*/
|
||||||
|
function AlignToggle({
|
||||||
|
value,
|
||||||
|
fallback,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
value: Align | undefined;
|
||||||
|
fallback: Align;
|
||||||
|
onChange: (align: Align) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
label: string;
|
||||||
|
}) {
|
||||||
|
const current = value ?? fallback;
|
||||||
|
return (
|
||||||
|
<div className="roomstamp-align" role="group" aria-label={label}>
|
||||||
|
{ALIGN_CELLS.map((cell) => (
|
||||||
|
<button
|
||||||
|
key={cell.align}
|
||||||
|
type="button"
|
||||||
|
className={`roomstamp-align-btn${current === cell.align ? " active" : ""}`}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-pressed={current === cell.align}
|
||||||
|
title={t(cell.titleKey)}
|
||||||
|
aria-label={t(cell.titleKey)}
|
||||||
|
onClick={() => onChange(cell.align)}
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined" aria-hidden="true">
|
||||||
|
{cell.icon}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function RoomStampEditor({ value, onApply, onCancel }: RoomStampEditorProps) {
|
export function RoomStampEditor({ value, onApply, onCancel }: RoomStampEditorProps) {
|
||||||
const [draft, setDraft] = useState<RoomStamp>(value);
|
const [draft, setDraft] = useState<RoomStamp>(value);
|
||||||
|
|
||||||
const patch = (p: Partial<RoomStamp>) => setDraft((d) => ({ ...d, ...p }));
|
const patch = (p: Partial<RoomStamp>) => setDraft((d) => ({ ...d, ...p }));
|
||||||
|
|
||||||
|
const hasLine2 = (draft.nameLine2 ?? "").trim().length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="texteditor-overlay"
|
className="texteditor-overlay"
|
||||||
@@ -66,6 +117,7 @@ export function RoomStampEditor({ value, onApply, onCancel }: RoomStampEditorPro
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<div className="roomstamp-row">
|
||||||
<label className="roomstamp-field">
|
<label className="roomstamp-field">
|
||||||
<span className="roomstamp-label">{t("roomStamp.name")}</span>
|
<span className="roomstamp-label">{t("roomStamp.name")}</span>
|
||||||
<input
|
<input
|
||||||
@@ -75,7 +127,15 @@ export function RoomStampEditor({ value, onApply, onCancel }: RoomStampEditorPro
|
|||||||
onChange={(e) => patch({ name: e.target.value })}
|
onChange={(e) => patch({ name: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<AlignToggle
|
||||||
|
value={draft.nameAlign}
|
||||||
|
fallback="left"
|
||||||
|
label={`${t("roomStamp.name")} — ${t("text.group")}`}
|
||||||
|
onChange={(nameAlign) => patch({ nameAlign })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="roomstamp-row">
|
||||||
<label className="roomstamp-field">
|
<label className="roomstamp-field">
|
||||||
<span className="roomstamp-label">{t("roomStamp.nameLine2")}</span>
|
<span className="roomstamp-label">{t("roomStamp.nameLine2")}</span>
|
||||||
<input
|
<input
|
||||||
@@ -85,7 +145,16 @@ export function RoomStampEditor({ value, onApply, onCancel }: RoomStampEditorPro
|
|||||||
onChange={(e) => patch({ nameLine2: e.target.value })}
|
onChange={(e) => patch({ nameLine2: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<AlignToggle
|
||||||
|
value={draft.line2Align}
|
||||||
|
fallback="left"
|
||||||
|
disabled={!hasLine2}
|
||||||
|
label={`${t("roomStamp.nameLine2")} — ${t("text.group")}`}
|
||||||
|
onChange={(line2Align) => patch({ line2Align })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="roomstamp-row">
|
||||||
<label className="roomstamp-check">
|
<label className="roomstamp-check">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -94,6 +163,14 @@ export function RoomStampEditor({ value, onApply, onCancel }: RoomStampEditorPro
|
|||||||
/>
|
/>
|
||||||
<span>{t("roomStamp.showFloorArea")}</span>
|
<span>{t("roomStamp.showFloorArea")}</span>
|
||||||
</label>
|
</label>
|
||||||
|
<AlignToggle
|
||||||
|
value={draft.floorAreaAlign}
|
||||||
|
fallback="center"
|
||||||
|
disabled={!draft.showFloorArea}
|
||||||
|
label={`${t("roomStamp.showFloorArea")} — ${t("text.group")}`}
|
||||||
|
onChange={(floorAreaAlign) => patch({ floorAreaAlign })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label className="roomstamp-field">
|
<label className="roomstamp-field">
|
||||||
<span className="roomstamp-label">{t("roomStamp.floorAreaPrefix")}</span>
|
<span className="roomstamp-label">{t("roomStamp.floorAreaPrefix")}</span>
|
||||||
@@ -106,6 +183,7 @@ export function RoomStampEditor({ value, onApply, onCancel }: RoomStampEditorPro
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<div className="roomstamp-row">
|
||||||
<label className="roomstamp-check">
|
<label className="roomstamp-check">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -114,6 +192,14 @@ export function RoomStampEditor({ value, onApply, onCancel }: RoomStampEditorPro
|
|||||||
/>
|
/>
|
||||||
<span>{t("roomStamp.showUsage")}</span>
|
<span>{t("roomStamp.showUsage")}</span>
|
||||||
</label>
|
</label>
|
||||||
|
<AlignToggle
|
||||||
|
value={draft.usageAlign}
|
||||||
|
fallback="center"
|
||||||
|
disabled={!draft.showUsage}
|
||||||
|
label={`${t("roomStamp.showUsage")} — ${t("text.group")}`}
|
||||||
|
onChange={(usageAlign) => patch({ usageAlign })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="texteditor-foot">
|
<div className="texteditor-foot">
|
||||||
|
|||||||
@@ -2814,8 +2814,8 @@ function renderPrimitive(
|
|||||||
color: p.color,
|
color: p.color,
|
||||||
});
|
});
|
||||||
const extra: SvgLine[] = p.extraLines.map((line) => ({
|
const extra: SvgLine[] = p.extraLines.map((line) => ({
|
||||||
align: "center" as const,
|
align: line.align,
|
||||||
tspans: [{ text: line, fontSize: baseFs * 0.8, fill: p.color }],
|
tspans: [{ text: line.text, fontSize: baseFs * 0.8, fill: p.color }],
|
||||||
}));
|
}));
|
||||||
const lines = [...docLines, ...extra];
|
const lines = [...docLines, ...extra];
|
||||||
if (lines.length === 0) return null;
|
if (lines.length === 0) return null;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { evaluateRoom, siaLabel } from "../geometry/roomArea";
|
|||||||
import type { RichTextDoc } from "../text/richText";
|
import type { RichTextDoc } from "../text/richText";
|
||||||
import { docFromText } from "../text/richText";
|
import { docFromText } from "../text/richText";
|
||||||
import { roomStampToDoc, roomStampExtraLines } from "../model/roomStamp";
|
import { roomStampToDoc, roomStampExtraLines } from "../model/roomStamp";
|
||||||
|
import type { RoomStampLine } from "../model/roomStamp";
|
||||||
import { stairVerticalExtent, wallReferenceOffset } from "../model/wall";
|
import { stairVerticalExtent, wallReferenceOffset } from "../model/wall";
|
||||||
import { stairGeometry, stairCut } from "../geometry/stair";
|
import { stairGeometry, stairCut } from "../geometry/stair";
|
||||||
import { doorSymbol, openingInterval, windowSymbol } from "../geometry/opening";
|
import { doorSymbol, openingInterval, windowSymbol } from "../geometry/opening";
|
||||||
@@ -229,9 +230,10 @@ export type Primitive =
|
|||||||
doc: RichTextDoc;
|
doc: RichTextDoc;
|
||||||
/**
|
/**
|
||||||
* LIVE-Zusatzzeilen (z. B. Fläche „24.30 m²" + SIA-Tag), UNTER dem Doc
|
* LIVE-Zusatzzeilen (z. B. Fläche „24.30 m²" + SIA-Tag), UNTER dem Doc
|
||||||
* gezeichnet — unabhängig vom editierten Text, stets aktuell.
|
* gezeichnet — unabhängig vom editierten Text, stets aktuell. Jede Zeile
|
||||||
|
* trägt ihre eigene Ausrichtung (`align`).
|
||||||
*/
|
*/
|
||||||
extraLines: string[];
|
extraLines: RoomStampLine[];
|
||||||
/** Basis-Schriftgrösse in PUNKT (für Runs ohne eigene Grösse + Zusatzzeilen). */
|
/** Basis-Schriftgrösse in PUNKT (für Runs ohne eigene Grösse + Zusatzzeilen). */
|
||||||
basePt: number;
|
basePt: number;
|
||||||
/** Textfarbe (hex) als Default. */
|
/** Textfarbe (hex) als Default. */
|
||||||
@@ -1309,8 +1311,11 @@ function addRoomArea(
|
|||||||
siaLabel: siaLabel(room.siaCategory),
|
siaLabel: siaLabel(room.siaCategory),
|
||||||
})
|
})
|
||||||
: [
|
: [
|
||||||
`${res.netArea.toFixed(2)} m²`,
|
{ text: `${res.netArea.toFixed(2)} m²`, align: "center" as const },
|
||||||
`${room.siaCategory} · ${siaLabel(room.siaCategory)}`,
|
{
|
||||||
|
text: `${room.siaCategory} · ${siaLabel(room.siaCategory)}`,
|
||||||
|
align: "center" as const,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
out.push({
|
out.push({
|
||||||
kind: "text",
|
kind: "text",
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ export function planToRenderScene(plan: Plan, paperScaleN: number = STAMP_DEFAUL
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const line of p.extraLines) {
|
for (const line of p.extraLines) {
|
||||||
flat.push({ content: line, sizeMm: baseMm * 0.8, color: defCol, align: "center" });
|
flat.push({ content: line.text, sizeMm: baseMm * 0.8, color: defCol, align: line.align });
|
||||||
}
|
}
|
||||||
if (flat.length > 0) {
|
if (flat.length > 0) {
|
||||||
const baseH = baseMm * mmToM; // Basisgröße in Modell-Metern, beim gewählten Massstab
|
const baseH = baseMm * mmToM; // Basisgröße in Modell-Metern, beim gewählten Massstab
|
||||||
|
|||||||
@@ -4518,6 +4518,57 @@ body {
|
|||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
/* Zeile mit Feld/Häkchen links und Ausrichtungs-Umschalter rechts. */
|
||||||
|
.roomstamp-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.roomstamp-row .roomstamp-field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.roomstamp-row .roomstamp-check {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
/* Drei-Wege-Umschalter (links/mitte/rechts) je Stempelzeile. */
|
||||||
|
.roomstamp-align {
|
||||||
|
display: inline-flex;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.roomstamp-align-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink-2);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.14s, color 0.14s;
|
||||||
|
}
|
||||||
|
.roomstamp-align-btn:first-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
.roomstamp-align-btn:hover:not(:disabled) {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.roomstamp-align-btn.active {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.roomstamp-align-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.roomstamp-align .material-symbols-outlined {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Flächenbilanz-Panel (SIA 416): Kopf, Raumliste, Summentabelle, Export. */
|
/* Flächenbilanz-Panel (SIA 416): Kopf, Raumliste, Summentabelle, Export. */
|
||||||
.balance-panel {
|
.balance-panel {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import type { Project, Vec2 } from "../model/types";
|
import type { Project, Vec2 } from "../model/types";
|
||||||
import { lineIntersect } from "../model/geometry";
|
import { lineIntersect } from "../model/geometry";
|
||||||
|
import { centroid as roomCentroid } from "../geometry/roomArea";
|
||||||
import type { SnapKind, SnapResult, SnapSettings } from "./types";
|
import type { SnapKind, SnapResult, SnapSettings } from "./types";
|
||||||
|
|
||||||
const dist = (a: Vec2, b: Vec2): number => Math.hypot(a.x - b.x, a.y - b.y);
|
const dist = (a: Vec2, b: Vec2): number => Math.hypot(a.x - b.x, a.y - b.y);
|
||||||
@@ -109,13 +110,20 @@ export function computeSnap(input: SnapInput): SnapResult | null {
|
|||||||
? collectSegments(input)
|
? collectSegments(input)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
// endpoint (Strecken-Endpunkte + Draft-Knoten).
|
// endpoint (Strecken-Endpunkte + Draft-Knoten + Raum-Stempel-Anker).
|
||||||
if (settings.endpoint) {
|
if (settings.endpoint) {
|
||||||
for (const [a, b] of segs) {
|
for (const [a, b] of segs) {
|
||||||
consider(a, "endpoint");
|
consider(a, "endpoint");
|
||||||
consider(b, "endpoint");
|
consider(b, "endpoint");
|
||||||
}
|
}
|
||||||
for (const p of input.draftPoints) consider(p, "endpoint");
|
for (const p of input.draftPoints) consider(p, "endpoint");
|
||||||
|
// Raum-Stempel-Anker (fixer Anker, sonst Flächenschwerpunkt) des Geschosses.
|
||||||
|
for (const room of input.project.rooms ?? []) {
|
||||||
|
if (room.floorId !== input.levelId) continue;
|
||||||
|
if (!input.visibleCodes.has(room.categoryCode)) continue;
|
||||||
|
if (room.boundary.length < 3) continue;
|
||||||
|
consider(room.stampAnchor ?? roomCentroid(room.boundary), "endpoint");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// midpoint.
|
// midpoint.
|
||||||
if (settings.midpoint) {
|
if (settings.midpoint) {
|
||||||
|
|||||||
Reference in New Issue
Block a user