Schnitt/Ansicht-Feinschliff: Kantenfilter, Print-Toggle, echte Fenster in der Ansicht

Vier Nutzer-Punkte:
1. Editieren im Schnitt funktioniert (Klick auf die Poché wählt das Bauteil,
   headless verifiziert) — die Störung waren die Kanten GESCHNITTENER
   Bauteile: der Extraktor projizierte auch deren Restkörper (Deckel-/
   Bodenkanten quer durch die eigene Poché). computeSection filtert sie jetzt;
   Ansichtskanten ungeschnittener Bauteile (Rückwand-Fenster etc.) bleiben.
2. Verdeckte Kanten (gestrichelt) sind Standard AUS — zuschaltbar je Ebene
   (DrawingLevel.hiddenLines, Checkbox in der Schnittlinien-Sektion).
3. Display/Print-Umschalter wirkt jetzt in ALLEN 2D-Darstellungen (Grundriss,
   Schnitt, Ansicht, Zeichnung) — war fälschlich auf den Grundriss begrenzt.
4. Fenster/Türen in der Ansicht als echtes Bauteil: Blendrahmen-Fläche →
   Flügelfelder (aus der Flügeltabelle, ungleiche Breiten/Pfosten) → Glas,
   Kämpfer/Oberlicht-Teilung, DIN-Öffnungssymbol (dezent gestrichelt,
   Dreh/Kipp/Drehkipp je Anschlag); Türen mit Rahmen + Blatt-Fläche.
737/737 grün; headless verifiziert (Schnitt A + Ansicht Süd).
This commit is contained in:
2026-07-11 02:18:09 +02:00
parent b96714641a
commit 4c4c9907af
8 changed files with 229 additions and 16 deletions
+9 -3
View File
@@ -5212,7 +5212,10 @@ export default function App() {
activeLayer={activeLayerName} activeLayer={activeLayerName}
lineMode={lineMode} lineMode={lineMode}
onLineModeChange={setLineMode} onLineModeChange={setLineMode}
lineModeEnabled={planActive} // Display/Print wirkt in ALLEN 2D-Darstellungen (Grundriss, Schnitt,
// Ansicht, Zeichnung — hairline fliesst überall hin); nur die
// 3D-Perspektive kennt keine Papier-Linienstärken.
lineModeEnabled={activeLevel.kind !== "floor" || viewType === "grundriss"}
snap={snap} snap={snap}
onSnapChange={setSnap} onSnapChange={setSnap}
/> />
@@ -6841,8 +6844,11 @@ function SectionPlanView({
); );
const sectionPlan = useMemo( const sectionPlan = useMemo(
() => (!isElevation && output ? generateSectionPlan(output, mono) : null), () =>
[isElevation, output, mono], !isElevation && output
? generateSectionPlan(output, mono, { hiddenLines: level.hiddenLines })
: null,
[isElevation, output, mono, level.hiddenLines],
); );
const plan = isElevation ? elevationPlan : sectionPlan; const plan = isElevation ? elevationPlan : sectionPlan;
const effStatus = isElevation ? (elevationPlan ? "ready" : "empty") : status; const effStatus = isElevation ? (elevationPlan ? "ready" : "empty") : status;
+1
View File
@@ -515,6 +515,7 @@ export const de = {
"objinfo.sectionLine.endX": "Ende X", "objinfo.sectionLine.endX": "Ende X",
"objinfo.sectionLine.endY": "Ende Y", "objinfo.sectionLine.endY": "Ende Y",
"objinfo.sectionLine.depth": "Schnitt-Tiefe (m)", "objinfo.sectionLine.depth": "Schnitt-Tiefe (m)",
"objinfo.sectionLine.hiddenLines": "Verdeckte Kanten",
"objinfo.sectionLine.depthHint": "leer = unbegrenzt", "objinfo.sectionLine.depthHint": "leer = unbegrenzt",
"objinfo.sectionLine.delete": "Schnittlinie löschen", "objinfo.sectionLine.delete": "Schnittlinie löschen",
"objinfo.extrudedSolid.section": "Extrusion", "objinfo.extrudedSolid.section": "Extrusion",
+1
View File
@@ -512,6 +512,7 @@ export const en: Record<TranslationKey, string> = {
"objinfo.sectionLine.endX": "End X", "objinfo.sectionLine.endX": "End X",
"objinfo.sectionLine.endY": "End Y", "objinfo.sectionLine.endY": "End Y",
"objinfo.sectionLine.depth": "Section depth (m)", "objinfo.sectionLine.depth": "Section depth (m)",
"objinfo.sectionLine.hiddenLines": "Hidden lines",
"objinfo.sectionLine.depthHint": "empty = unlimited", "objinfo.sectionLine.depthHint": "empty = unlimited",
"objinfo.sectionLine.delete": "Delete section line", "objinfo.sectionLine.delete": "Delete section line",
"objinfo.extrudedSolid.section": "Extrusion", "objinfo.extrudedSolid.section": "Extrusion",
+5
View File
@@ -777,6 +777,11 @@ export interface DrawingLevel {
* Verhalten). * Verhalten).
*/ */
depth?: number; depth?: number;
/**
* Verdeckte Kanten im Schnitt zeichnen (gestrichelt). Default AUS — sie
* legen sich sonst als blasses Liniengewirr über Poché und Ansichtskanten.
*/
hiddenLines?: boolean;
} }
/** /**
+11
View File
@@ -420,6 +420,17 @@ export function SectionLineSection({
/> />
</div> </div>
{/* Verdeckte Kanten (gestrichelt) — Default aus (blasses Liniengewirr). */}
<div className="objinfo-field">
<span className="objinfo-flabel">{t("objinfo.sectionLine.hiddenLines")}</span>
<input
type="checkbox"
checked={level.hiddenLines ?? false}
onChange={(e) => host.onSetSectionLinePatch({ hiddenLines: e.target.checked })}
title={t("objinfo.sectionLine.hiddenLines")}
/>
</div>
{/* Löschen: linePoints zurücksetzen (die Ebene bleibt bestehen). */} {/* Löschen: linePoints zurücksetzen (die Ebene bleibt bestehen). */}
<button type="button" className="objinfo-btn" onClick={() => host.onDeleteSectionLine()}> <button type="button" className="objinfo-btn" onClick={() => host.onDeleteSectionLine()}>
{t("objinfo.sectionLine.delete")} {t("objinfo.sectionLine.delete")}
+9 -3
View File
@@ -959,7 +959,11 @@ const SECTION_HATCH: HatchRender = {
* zur Plan-Y — die PlanView zeigt Y nach oben, also erscheint der Schnitt * zur Plan-Y — die PlanView zeigt Y nach oben, also erscheint der Schnitt
* aufrecht. Rein darstellend; kennt das Projekt nicht. * aufrecht. Rein darstellend; kennt das Projekt nicht.
*/ */
export function generateSectionPlan(output: SectionOutput, mono = false): Plan { export function generateSectionPlan(
output: SectionOutput,
mono = false,
opts: { hiddenLines?: boolean } = {},
): Plan {
const primitives: Primitive[] = []; const primitives: Primitive[] = [];
let minX = Infinity, let minX = Infinity,
minY = Infinity, minY = Infinity,
@@ -1014,8 +1018,10 @@ export function generateSectionPlan(output: SectionOutput, mono = false): Plan {
}); });
} }
// Verdeckte Kanten zuerst (liegen unter den sichtbaren), gestrichelt. // Verdeckte Kanten zuerst (liegen unter den sichtbaren), gestrichelt
for (const e of output.hiddenEdges) { // Standard AUS (VW-üblich): sie legen sich sonst als blasses Liniengewirr
// über Poché und Ansichtskanten. Zuschaltbar je Schnitt-/Ansichts-Ebene.
for (const e of opts.hiddenLines ? output.hiddenEdges : []) {
acc(e.a[0], e.a[1]); acc(e.a[0], e.a[1]);
acc(e.b[0], e.b[1]); acc(e.b[0], e.b[1]);
primitives.push({ primitives.push({
+177 -7
View File
@@ -34,7 +34,9 @@
import type { DrawingLevel, Project, Vec2, Wall } from "../model/types"; import type { DrawingLevel, Project, Vec2, Wall } from "../model/types";
import { import {
getWallType, getWallType,
getWindowType,
openingsOfWall, openingsOfWall,
sashesOfWindowType,
wallTypeThickness, wallTypeThickness,
} from "../model/types"; } from "../model/types";
import { wallVerticalExtent } from "../model/wall"; import { wallVerticalExtent } from "../model/wall";
@@ -82,6 +84,12 @@ const GROUND_MM = 0.5;
* verschwinden optisch in der Fassade), Dach eine Stufe dunkler. */ * verschwinden optisch in der Fassade), Dach eine Stufe dunkler. */
const WALL_FILL = "#ecebe8"; const WALL_FILL = "#ecebe8";
const ROOF_FILL = "#d9d6d1"; const ROOF_FILL = "#d9d6d1";
/** Blendrahmen-Fläche der Öffnungen (heller als die Fassade). */
const FRAME_FILL = "#f8f7f4";
/** Türblatt-Fläche (zwischen Fassade und Glas). */
const DOOR_LEAF_FILL = "#d4d2cd";
/** Öffnungssymbol (DIN-Andeutung): dezente Annotation, nicht Bauteilkante. */
const SYMBOL_INK = "#7d858f";
/** Sonnenrichtung als (u, v)-Schattenversatz je Tiefeneinheit: 45° von links /** Sonnenrichtung als (u, v)-Schattenversatz je Tiefeneinheit: 45° von links
* oben ⇒ Schatten fällt nach rechts unten (+u, v). */ * oben ⇒ Schatten fällt nach rechts unten (+u, v). */
@@ -407,6 +415,14 @@ export function roofWorldFacesAll(project: Project): WorldFace[] {
interface ProjectedOpening { interface ProjectedOpening {
pts: Array<[number, number]>; pts: Array<[number, number]>;
depth: number; depth: number;
/** Glasfelder (je Flügel/Feld) innerhalb des Blendrahmens. */
glass: Array<Array<[number, number]>>;
/** Teilungslinien (Pfosten/Kämpfer) über den Rahmen. */
divisions: Array<[[number, number], [number, number]]>;
/** Öffnungssymbol (DIN-Ansicht): gestrichelte Linien zum Bandseiten-Mittel. */
symbol: Array<[[number, number], [number, number]]>;
/** Tür (Blatt-Fläche statt Glas, ausser Glas-Blatt). */
isDoor: boolean;
} }
/** /**
@@ -454,7 +470,9 @@ function projectOpeningRect(
} }
const depth = depthSum / world.length; const depth = depthSum / world.length;
if (depth <= EPS) continue; if (depth <= EPS) continue;
return { pts, depth }; // pts-Reihenfolge: [unten-von, unten-bis, oben-bis, oben-von] — Basis für
// die (fa, fz)-Interpolation der Flügel-/Glasfelder in collectOpenings.
return { pts, depth, glass: [], divisions: [], symbol: [], isDoor: false };
} }
return null; return null;
} }
@@ -467,6 +485,30 @@ function projectOpeningRect(
*/ */
function collectOpenings(project: Project, frame: ElevationFrame): ProjectedOpening[] { function collectOpenings(project: Project, frame: ElevationFrame): ProjectedOpening[] {
const out: ProjectedOpening[] = []; const out: ProjectedOpening[] = [];
const lerp = (
a: [number, number],
b: [number, number],
t: number,
): [number, number] => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
// Punkt im Öffnungs-Quad: fa = Anteil entlang der Wandachse, fz = Höhenanteil.
const quadAt = (
q: Array<[number, number]>,
fa: number,
fz: number,
): [number, number] => lerp(lerp(q[0], q[1], fa), lerp(q[3], q[2], fa), fz);
const rect = (
q: Array<[number, number]>,
fa0: number,
fa1: number,
fz0: number,
fz1: number,
): Array<[number, number]> => [
quadAt(q, fa0, fz0),
quadAt(q, fa1, fz0),
quadAt(q, fa1, fz1),
quadAt(q, fa0, fz1),
];
for (const wall of project.walls) { for (const wall of project.walls) {
let thickness = 0.2; let thickness = 0.2;
try { try {
@@ -481,7 +523,83 @@ function collectOpenings(project: Project, frame: ElevationFrame): ProjectedOpen
if (!iv) continue; if (!iv) continue;
const v = openingVerticalExtent(project, wall, op); const v = openingVerticalExtent(project, wall, op);
const pr = projectOpeningRect(frame, wall, iv.from, iv.to, v.zBottom, v.zTop, thickness); const pr = projectOpeningRect(frame, wall, iv.from, iv.to, v.zBottom, v.zTop, thickness);
if (pr) out.push(pr); if (!pr) continue;
const w = iv.to - iv.from;
const h = v.zTop - v.zBottom;
if (op.kind === "door") {
// Türen im Projekt-`openings`-Array: Blatt-Fläche (kein Glasraster).
pr.isDoor = true;
pr.glass.push(rect(pr.pts, 0.04, 0.96, 0, 0.97));
out.push(pr);
continue;
}
// Fenster: Blendrahmen → Flügelfelder → Glas (+ Kämpfer/Oberlicht) aus dem
// Fenstertyp; ohne Typ ein einzelnes Glasfeld im Default-Rahmen.
const wt = getWindowType(project, op);
const fw = wt?.frameWidth ?? 0.06;
const fa = Math.min(0.45, fw / Math.max(w, 1e-6));
const fz = Math.min(0.45, fw / Math.max(h, 1e-6));
const transom = Math.max(0, wt?.transomHeight ?? 0);
// Oberlicht-Trennung als Höhenanteil (0 = keins).
const fTr = transom > EPS && transom < h - fw ? 1 - transom / h : 0;
const fieldTop = fTr > 0 ? fTr - fz / 2 : 1 - fz;
// Flügel-Spannen über die innere Breite (autoWidth gleichmässig).
const sashes = wt ? sashesOfWindowType(wt) : [{ kind: "fluegel" as const }];
const innerW = Math.max(1e-6, w * (1 - 2 * fa));
let fixedSum = 0;
let autoCount = 0;
for (const sd of sashes) {
if (sd.kind === "pfosten") fixedSum += Math.max(0, sd.postWidth ?? 0.06);
else if (sd.autoWidth === false) fixedSum += Math.max(0, sd.width ?? 0);
else autoCount++;
}
const autoW = autoCount > 0 ? Math.max(0.02, (innerW - fixedSum) / autoCount) : 0;
let cursor = fa;
for (const sd of sashes) {
const sw =
sd.kind === "pfosten"
? Math.max(0, sd.postWidth ?? 0.06)
: sd.autoWidth === false
? Math.max(0, sd.width ?? autoW)
: autoW;
const fa0 = cursor;
const fa1 = cursor + sw / w;
cursor = fa1;
if (sd.kind === "pfosten") {
// Pfosten als Teilungslinie (Mitte).
const fm = (fa0 + fa1) / 2;
pr.divisions.push([quadAt(pr.pts, fm, fz), quadAt(pr.pts, fm, fieldTop)]);
continue;
}
pr.glass.push(rect(pr.pts, fa0 + fa * 0.3, fa1 - fa * 0.3, fz, fieldTop));
// Trennlinie zum nächsten Feld (ausser am Rand).
if (fa1 < 1 - fa - 1e-6) {
pr.divisions.push([quadAt(pr.pts, fa1, fz), quadAt(pr.pts, fa1, fieldTop)]);
}
// Öffnungssymbol (DIN-Ansicht, gestrichelt): Spitze am Bandseiten-Mittel.
const kind = sd.opening ?? wt?.kind ?? "fest";
if (kind === "dreh" || kind === "drehkipp") {
const hingeLeft = (sd.hingeSide ?? "left") === "left";
const apex = quadAt(pr.pts, hingeLeft ? fa0 : fa1, (fz + fieldTop) / 2);
pr.symbol.push(
[quadAt(pr.pts, hingeLeft ? fa1 : fa0, fz), apex],
[quadAt(pr.pts, hingeLeft ? fa1 : fa0, fieldTop), apex],
);
}
if (kind === "kipp" || kind === "drehkipp") {
const apex = quadAt(pr.pts, (fa0 + fa1) / 2, fz);
pr.symbol.push(
[quadAt(pr.pts, fa0, fieldTop), apex],
[quadAt(pr.pts, fa1, fieldTop), apex],
);
}
}
// Oberlicht: eigenes Glasfeld + Kämpferlinie.
if (fTr > 0) {
pr.glass.push(rect(pr.pts, fa, 1 - fa, fTr + fz / 2, 1 - fz));
pr.divisions.push([quadAt(pr.pts, 0, fTr), quadAt(pr.pts, 1, fTr)]);
}
out.push(pr);
} }
for (const d of project.doors ?? []) { for (const d of project.doors ?? []) {
if (d.hostWallId !== wall.id) continue; if (d.hostWallId !== wall.id) continue;
@@ -489,7 +607,11 @@ function collectOpenings(project: Project, frame: ElevationFrame): ProjectedOpen
const to = Math.max(from, Math.min(d.position + d.width, axisLen)); const to = Math.max(from, Math.min(d.position + d.width, axisLen));
const oTop = Math.min(zTop, zBottom + d.height); const oTop = Math.min(zTop, zBottom + d.height);
const pr = projectOpeningRect(frame, wall, from, to, zBottom, oTop, thickness); const pr = projectOpeningRect(frame, wall, from, to, zBottom, oTop, thickness);
if (pr) out.push(pr); if (pr) {
pr.isDoor = true;
pr.glass.push(rect(pr.pts, 0.04, 0.96, 0, 0.97));
out.push(pr);
}
} }
} }
return out; return out;
@@ -732,22 +854,70 @@ export function generateElevationPlan(
// Fenster/Türen: Rahmen+Glas, knapp VOR der Wirtsfläche (auf der Fassade). // Fenster/Türen: Rahmen+Glas, knapp VOR der Wirtsfläche (auf der Fassade).
for (const op of collectOpenings(project, frame)) { for (const op of collectOpenings(project, frame)) {
const pts: Vec2[] = op.pts.map(([u, v]) => { const toVec = (q: Array<[number, number]>): Vec2[] =>
q.map(([u, v]) => {
acc(u, v); acc(u, v);
return { x: u, y: v }; return { x: u, y: v };
}); });
const d = op.depth - EPS * 100;
// Blendrahmen: helle Rahmenfläche mit kräftiger Kontur — Glas/Blatt/
// Teilungen liegen darüber (gleiche Tiefe, seq-Reihenfolge).
items.push({ items.push({
depth: op.depth - EPS * 100, depth: d,
seq: seq++, seq: seq++,
prim: { prim: {
kind: "polygon", kind: "polygon",
pts, pts: toVec(op.pts),
fill: mono ? GLASS_FILL_MONO : GLASS_FILL, fill: mono ? "#ffffff" : FRAME_FILL,
stroke: EDGE_INK, stroke: EDGE_INK,
strokeWidthMm: FRAME_MM, strokeWidthMm: FRAME_MM,
hatch: NO_HATCH, hatch: NO_HATCH,
}, },
}); });
for (const g of op.glass) {
items.push({
depth: d,
seq: seq++,
prim: {
kind: "polygon",
pts: toVec(g),
fill: mono ? GLASS_FILL_MONO : op.isDoor ? DOOR_LEAF_FILL : GLASS_FILL,
stroke: EDGE_INK,
strokeWidthMm: EDGE_MM,
hatch: NO_HATCH,
},
});
}
for (const [a, bLn] of op.divisions) {
items.push({
depth: d,
seq: seq++,
prim: {
kind: "line",
a: { x: a[0], y: a[1] },
b: { x: bLn[0], y: bLn[1] },
cls: "elevation-division",
weightMm: EDGE_MM,
color: EDGE_INK,
},
});
}
// Öffnungssymbol (DIN): dünn gestrichelt über dem Glas.
for (const [a, bLn] of op.symbol) {
items.push({
depth: d,
seq: seq++,
prim: {
kind: "line",
a: { x: a[0], y: a[1] },
b: { x: bLn[0], y: bLn[1] },
cls: "elevation-opening-symbol",
weightMm: 0.1,
dash: [2.2, 1.8],
color: SYMBOL_INK,
},
});
}
} }
// Painter: fern (grosse Tiefe) zuerst, nah zuletzt. Stabiler Tiebreak über seq. // Painter: fern (grosse Tiefe) zuerst, nah zuletzt. Stabiler Tiebreak über seq.
+13
View File
@@ -1141,6 +1141,19 @@ export async function computeSection(
plane.normal[2], plane.normal[2],
); );
const output = JSON.parse(json) as SectionOutput; const output = JSON.parse(json) as SectionOutput;
// Kanten GESCHNITTENER Bauteile verwerfen: der Extraktor projiziert auch die
// Restkörper der geschnittenen Wände/Decken (Deckel-/Bodenkanten laufen quer
// durch die eigene Schnitt-Poché — die „komischen Linien über den
// Schnittlinien"). Die Schnittgeometrie trägt die Poché-Kontur selbst; die
// Ansichtskanten UNGESCHNITTENER Bauteile (z. B. Fenster der Rückwand)
// bleiben erhalten.
const cutOwners = new Set(
output.cutPolygons.map((cp) => `${cp.component.kind}:${cp.component.index}`),
);
const notCut = (e: SectionEdge): boolean =>
!cutOwners.has(`${e.component.kind}:${e.component.index}`);
output.visibleEdges = output.visibleEdges.filter(notCut);
output.hiddenEdges = output.hiddenEdges.filter(notCut);
// Dächer TS-seitig ergänzen (der Rust-Extraktor kennt nur Wände + Decken). // Dächer TS-seitig ergänzen (der Rust-Extraktor kennt nur Wände + Decken).
appendRoofSections(output, project, plane); appendRoofSections(output, project, plane);
attachCutStyles(output, project, plane); attachCutStyles(output, project, plane);