Text-Styling-Leiste wirkt auf selektierten Freitext/Textspalte
Die Text-Formatier-Gruppe der Oberleiste formatierte bisher nur den Raum-Stempel. Jetzt ist auch ein selektierter Freitext/Textspalten-Element (Drawing2D shape "text") ein Formatier-Ziel: - Neues optionales Feld Drawing2D-Text.marks (Marks) für einheitliche Formatierung (Schriftfamilie, fett/kursiv/unterstrichen, Farbe). - App.textTarget adaptiert den Text über docFromText/plainText an die Rich-Text-Leiste; die GRÖSSE bleibt bewusst über die Modell-Höhe (Meter), nicht über pt (massstabslos wäre falsch) — sizePt wird verworfen. - generatePlan/PlanView rendern die Marks (fontFamily/-weight/-style/ textDecoration); marks.color hat Vorrang vor der Kategorie-Farbe. +3 Tests. tsc + vitest grün.
This commit is contained in:
+31
-12
@@ -189,7 +189,7 @@ import type {
|
|||||||
import type { PanelHostValue } from "./panels/host";
|
import type { PanelHostValue } from "./panels/host";
|
||||||
import { deriveSelection } from "./state/selectionInfo";
|
import { deriveSelection } from "./state/selectionInfo";
|
||||||
import { centroid as roomCentroid } from "./geometry/roomArea";
|
import { centroid as roomCentroid } from "./geometry/roomArea";
|
||||||
import { docFromText } from "./text/richText";
|
import { docFromText, plainText } from "./text/richText";
|
||||||
import type { RichTextDoc } from "./text/richText";
|
import type { RichTextDoc } from "./text/richText";
|
||||||
import {
|
import {
|
||||||
deleteNamedLayout,
|
deleteNamedLayout,
|
||||||
@@ -2399,17 +2399,36 @@ export default function App() {
|
|||||||
// gewählten Raums. Ohne aktive Editor-Auswahl wirkt Formatierung auf das ganze
|
// gewählten Raums. Ohne aktive Editor-Auswahl wirkt Formatierung auf das ganze
|
||||||
// Doc (range: null). Ist kein Raum gewählt → null (Controls setzen nur
|
// Doc (range: null). Ist kein Raum gewählt → null (Controls setzen nur
|
||||||
// Defaults). Der Setter schreibt live in den Store.
|
// Defaults). Der Setter schreibt live in den Store.
|
||||||
const textTarget = useMemo(
|
const textTarget = useMemo(() => {
|
||||||
() =>
|
if (selectedRoom) {
|
||||||
selectedRoom
|
return {
|
||||||
? {
|
doc: selectedRoom.stampDoc ?? docFromText(selectedRoom.name),
|
||||||
doc: selectedRoom.stampDoc ?? docFromText(selectedRoom.name),
|
range: null,
|
||||||
range: null,
|
apply: (doc: RichTextDoc) => setRoomStampDoc(selectedRoom.id, doc),
|
||||||
apply: (doc: RichTextDoc) => setRoomStampDoc(selectedRoom.id, doc),
|
};
|
||||||
}
|
}
|
||||||
: null,
|
// Selektierter freier Text/Textspalte: die Oberleisten-Formatier-Gruppe wirkt
|
||||||
[selectedRoom, setRoomStampDoc],
|
// auf Schriftfamilie/fett/kursiv/Farbe des ganzen Texts. Die GRÖSSE bleibt
|
||||||
);
|
// bewusst über die Modell-Höhe (Meter, height) geführt — nicht über sizePt,
|
||||||
|
// damit der Text im Massstab bleibt (Punkt-Grössen wären massstabslos).
|
||||||
|
if (selectedDrawingId) {
|
||||||
|
const d = project.drawings2d.find((x) => x.id === selectedDrawingId);
|
||||||
|
if (d && d.geom.shape === "text") {
|
||||||
|
const g = d.geom;
|
||||||
|
return {
|
||||||
|
doc: docFromText(g.text, g.marks ?? {}),
|
||||||
|
range: null,
|
||||||
|
apply: (doc: RichTextDoc) => {
|
||||||
|
const runMarks = doc.paragraphs.flatMap((p) => p.runs)[0]?.marks ?? {};
|
||||||
|
const marks = { ...runMarks };
|
||||||
|
delete marks.sizePt; // Grösse folgt der Modell-Höhe, nicht dem pt-Wert.
|
||||||
|
updateDrawing2D(d.id, { geom: { ...g, text: plainText(doc), marks } });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [selectedRoom, setRoomStampDoc, selectedDrawingId, project.drawings2d, updateDrawing2D]);
|
||||||
|
|
||||||
// ── 3D-Editier-Griffe (direkt im Viewport3D) ──────────────────────────────
|
// ── 3D-Editier-Griffe (direkt im Viewport3D) ──────────────────────────────
|
||||||
// Genau EINE gewählte Wand (kein 2D-Element) bzw. genau EIN 2D-Element liefert
|
// Genau EINE gewählte Wand (kein 2D-Element) bzw. genau EIN 2D-Element liefert
|
||||||
|
|||||||
+8
-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, Align } from "../text/richText";
|
import type { RichTextDoc, Align, Marks } from "../text/richText";
|
||||||
export type { RichTextDoc } from "../text/richText";
|
export type { RichTextDoc } from "../text/richText";
|
||||||
// Ansichts-Enums für Ausschnitte/View-Snapshots (Ansichtstyp, Kamera-Preset,
|
// Ansichts-Enums für Ausschnitte/View-Snapshots (Ansichtstyp, Kamera-Preset,
|
||||||
// Detailgrad). Rein type-only importiert — zur Laufzeit erased, kein Zyklus
|
// Detailgrad). Rein type-only importiert — zur Laufzeit erased, kein Zyklus
|
||||||
@@ -1279,6 +1279,13 @@ export type Drawing2DGeom =
|
|||||||
* sie, bleibt es einzeiliger Text (heutiges Verhalten).
|
* sie, bleibt es einzeiliger Text (heutiges Verhalten).
|
||||||
*/
|
*/
|
||||||
width?: number;
|
width?: number;
|
||||||
|
/**
|
||||||
|
* Optionale einheitliche Formatierung des ganzen Texts (Schriftfamilie, fett/
|
||||||
|
* kursiv, Farbe …) — gesetzt über die Text-Formatier-Gruppe der Oberleiste,
|
||||||
|
* wenn dieser Text selektiert ist. Die Grösse bleibt bewusst über `height`
|
||||||
|
* (Modell-Meter) geführt, nicht über `marks.sizePt`.
|
||||||
|
*/
|
||||||
|
marks?: Marks;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Ein freies 2D-Zeichenelement auf einer Zeichnungsebene. */
|
/** Ein freies 2D-Zeichenelement auf einer Zeichnungsebene. */
|
||||||
|
|||||||
+13
-1
@@ -3283,12 +3283,24 @@ function renderPrimitive(
|
|||||||
} else {
|
} else {
|
||||||
lines.push(p.text);
|
lines.push(p.text);
|
||||||
}
|
}
|
||||||
|
// Einheitliche Formatierung (Text-Werkzeug + Oberleisten-Formatier-Gruppe):
|
||||||
|
// Schriftfamilie/fett/kursiv/Unterstreichung aus `marks`; Grösse bleibt über
|
||||||
|
// heightM (Modell-Meter). Farbe steckt bereits in p.color (marks.color-Vorrang
|
||||||
|
// in generatePlan aufgelöst).
|
||||||
|
const mk = p.marks;
|
||||||
|
const deco =
|
||||||
|
mk && (mk.underline || mk.strike)
|
||||||
|
? [mk.underline ? "underline" : "", mk.strike ? "line-through" : ""].join(" ").trim()
|
||||||
|
: undefined;
|
||||||
return (
|
return (
|
||||||
<text
|
<text
|
||||||
x={c.x}
|
x={c.x}
|
||||||
y={c.y}
|
y={c.y}
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
fontFamily="var(--font-ui, sans-serif)"
|
fontFamily={mk?.font ?? "var(--font-ui, sans-serif)"}
|
||||||
|
fontWeight={mk?.bold ? 700 : undefined}
|
||||||
|
fontStyle={mk?.italic ? "italic" : undefined}
|
||||||
|
textDecoration={deco}
|
||||||
fill={p.color}
|
fill={p.color}
|
||||||
opacity={p.greyed ? 0.45 : undefined}
|
opacity={p.greyed ? 0.45 : undefined}
|
||||||
textAnchor="start"
|
textAnchor="start"
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// Freitext-Formatierung (Text-Werkzeug + Oberleisten-Formatier-Gruppe): die
|
||||||
|
// einheitlichen Marks (Schriftfamilie/fett/kursiv/Farbe) eines Drawing2D-Texts
|
||||||
|
// müssen ins `drawingText`-Primitiv wandern; marks.color hat Vorrang vor der
|
||||||
|
// Kategorie-/Elementfarbe.
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { generatePlan } from "./generatePlan";
|
||||||
|
import type { Drawing2D, Project } from "../model/types";
|
||||||
|
|
||||||
|
function projectWithText(drawing: Drawing2D): Project {
|
||||||
|
return {
|
||||||
|
id: "p",
|
||||||
|
name: "T",
|
||||||
|
lineStyles: [{ id: "thin", name: "d", weight: 0.13, color: "#111", dash: null }],
|
||||||
|
hatches: [{ id: "none", name: "Ohne", pattern: "none", scale: 1, angle: 0, color: "#111" }],
|
||||||
|
components: [],
|
||||||
|
wallTypes: [],
|
||||||
|
drawingLevels: [
|
||||||
|
{
|
||||||
|
id: "eg",
|
||||||
|
name: "EG",
|
||||||
|
kind: "floor",
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
floorHeight: 2.6,
|
||||||
|
cutHeight: 1.0,
|
||||||
|
baseElevation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layers: [
|
||||||
|
{ code: "70", name: "Text", color: "#0a0a0a", lw: 0.3, visible: true, locked: false },
|
||||||
|
],
|
||||||
|
walls: [],
|
||||||
|
doors: [],
|
||||||
|
openings: [],
|
||||||
|
ceilings: [],
|
||||||
|
stairs: [],
|
||||||
|
rooms: [],
|
||||||
|
drawings2d: [drawing],
|
||||||
|
context: [],
|
||||||
|
} as unknown as Project;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visible = new Set(["70"]);
|
||||||
|
|
||||||
|
function textPrim(drawing: Drawing2D) {
|
||||||
|
return generatePlan(projectWithText(drawing), "eg", visible, undefined, "mittel")
|
||||||
|
.primitives.find((p) => p.kind === "drawingText");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Drawing2D-Text: Marks im drawingText-Primitiv", () => {
|
||||||
|
const base: Drawing2D = {
|
||||||
|
id: "TX1",
|
||||||
|
type: "drawing2d",
|
||||||
|
levelId: "eg",
|
||||||
|
categoryCode: "70",
|
||||||
|
geom: { shape: "text", at: { x: 1, y: 1 }, text: "Hallo", height: 0.2, angle: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
it("trägt marks (font/bold/italic) ins Primitiv", () => {
|
||||||
|
const p = textPrim({
|
||||||
|
...base,
|
||||||
|
geom: { ...base.geom, marks: { font: "Merriweather", bold: true, italic: true } },
|
||||||
|
}) as { marks?: { font?: string; bold?: boolean; italic?: boolean } } | undefined;
|
||||||
|
expect(p?.marks?.font).toBe("Merriweather");
|
||||||
|
expect(p?.marks?.bold).toBe(true);
|
||||||
|
expect(p?.marks?.italic).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks.color übersteuert die Kategorie-Farbe", () => {
|
||||||
|
const p = textPrim({
|
||||||
|
...base,
|
||||||
|
geom: { ...base.geom, marks: { color: "#ff0000" } },
|
||||||
|
}) as { color: string } | undefined;
|
||||||
|
expect(p?.color).toBe("#ff0000");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ohne marks bleibt das Primitiv unverändert (kein marks-Feld)", () => {
|
||||||
|
const p = textPrim(base) as { marks?: unknown } | undefined;
|
||||||
|
expect(p).toBeTruthy();
|
||||||
|
expect(p?.marks).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -44,7 +44,7 @@ import {
|
|||||||
import { columnFootprint } from "../geometry/column";
|
import { columnFootprint } from "../geometry/column";
|
||||||
import { effectiveOverrides, hasOverrides } from "../overrides/engine";
|
import { effectiveOverrides, hasOverrides } from "../overrides/engine";
|
||||||
import { evaluateRoom, siaLabel } from "../geometry/roomArea";
|
import { evaluateRoom, siaLabel } from "../geometry/roomArea";
|
||||||
import type { RichTextDoc } from "../text/richText";
|
import type { Marks, 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 type { RoomStampLine } from "../model/roomStamp";
|
||||||
@@ -335,6 +335,8 @@ export type Primitive =
|
|||||||
/** Drehung in RADIANT (CCW im Modell). */
|
/** Drehung in RADIANT (CCW im Modell). */
|
||||||
angle: number;
|
angle: number;
|
||||||
color: string;
|
color: string;
|
||||||
|
/** Optionale einheitliche Formatierung (Schriftfamilie/fett/kursiv/…). */
|
||||||
|
marks?: Marks;
|
||||||
/**
|
/**
|
||||||
* Optionale Spaltenbreite in Modell-Metern (Textspalte): ist sie gesetzt,
|
* Optionale Spaltenbreite in Modell-Metern (Textspalte): ist sie gesetzt,
|
||||||
* bricht der Renderer den Text wortweise auf diese Breite um. Fehlt sie,
|
* bricht der Renderer den Text wortweise auf diese Breite um. Fehlt sie,
|
||||||
@@ -1289,8 +1291,10 @@ function addDrawing2D(
|
|||||||
text: g.text,
|
text: g.text,
|
||||||
heightM: g.height,
|
heightM: g.height,
|
||||||
angle: g.angle,
|
angle: g.angle,
|
||||||
color,
|
// marks.color (falls gesetzt) übersteuert die Kategorie-/Elementfarbe.
|
||||||
|
color: g.marks?.color ?? color,
|
||||||
...(g.width && g.width > 0 ? { wrapWidth: g.width } : {}),
|
...(g.width && g.width > 0 ? { wrapWidth: g.width } : {}),
|
||||||
|
...(g.marks ? { marks: g.marks } : {}),
|
||||||
greyed,
|
greyed,
|
||||||
drawingId: d.id,
|
drawingId: d.id,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user