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:
+9
-3
@@ -5212,7 +5212,10 @@ export default function App() {
|
||||
activeLayer={activeLayerName}
|
||||
lineMode={lineMode}
|
||||
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}
|
||||
onSnapChange={setSnap}
|
||||
/>
|
||||
@@ -6841,8 +6844,11 @@ function SectionPlanView({
|
||||
);
|
||||
|
||||
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 effStatus = isElevation ? (elevationPlan ? "ready" : "empty") : status;
|
||||
|
||||
@@ -515,6 +515,7 @@ export const de = {
|
||||
"objinfo.sectionLine.endX": "Ende X",
|
||||
"objinfo.sectionLine.endY": "Ende Y",
|
||||
"objinfo.sectionLine.depth": "Schnitt-Tiefe (m)",
|
||||
"objinfo.sectionLine.hiddenLines": "Verdeckte Kanten",
|
||||
"objinfo.sectionLine.depthHint": "leer = unbegrenzt",
|
||||
"objinfo.sectionLine.delete": "Schnittlinie löschen",
|
||||
"objinfo.extrudedSolid.section": "Extrusion",
|
||||
|
||||
@@ -512,6 +512,7 @@ export const en: Record<TranslationKey, string> = {
|
||||
"objinfo.sectionLine.endX": "End X",
|
||||
"objinfo.sectionLine.endY": "End Y",
|
||||
"objinfo.sectionLine.depth": "Section depth (m)",
|
||||
"objinfo.sectionLine.hiddenLines": "Hidden lines",
|
||||
"objinfo.sectionLine.depthHint": "empty = unlimited",
|
||||
"objinfo.sectionLine.delete": "Delete section line",
|
||||
"objinfo.extrudedSolid.section": "Extrusion",
|
||||
|
||||
@@ -777,6 +777,11 @@ export interface DrawingLevel {
|
||||
* Verhalten).
|
||||
*/
|
||||
depth?: number;
|
||||
/**
|
||||
* Verdeckte Kanten im Schnitt zeichnen (gestrichelt). Default AUS — sie
|
||||
* legen sich sonst als blasses Liniengewirr über Poché und Ansichtskanten.
|
||||
*/
|
||||
hiddenLines?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -420,6 +420,17 @@ export function SectionLineSection({
|
||||
/>
|
||||
</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). */}
|
||||
<button type="button" className="objinfo-btn" onClick={() => host.onDeleteSectionLine()}>
|
||||
{t("objinfo.sectionLine.delete")}
|
||||
|
||||
@@ -959,7 +959,11 @@ const SECTION_HATCH: HatchRender = {
|
||||
* zur Plan-Y — die PlanView zeigt Y nach oben, also erscheint der Schnitt
|
||||
* 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[] = [];
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
@@ -1014,8 +1018,10 @@ export function generateSectionPlan(output: SectionOutput, mono = false): Plan {
|
||||
});
|
||||
}
|
||||
|
||||
// Verdeckte Kanten zuerst (liegen unter den sichtbaren), gestrichelt.
|
||||
for (const e of output.hiddenEdges) {
|
||||
// Verdeckte Kanten zuerst (liegen unter den sichtbaren), gestrichelt —
|
||||
// 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.b[0], e.b[1]);
|
||||
primitives.push({
|
||||
|
||||
+177
-7
@@ -34,7 +34,9 @@
|
||||
import type { DrawingLevel, Project, Vec2, Wall } from "../model/types";
|
||||
import {
|
||||
getWallType,
|
||||
getWindowType,
|
||||
openingsOfWall,
|
||||
sashesOfWindowType,
|
||||
wallTypeThickness,
|
||||
} from "../model/types";
|
||||
import { wallVerticalExtent } from "../model/wall";
|
||||
@@ -82,6 +84,12 @@ const GROUND_MM = 0.5;
|
||||
* verschwinden optisch in der Fassade), Dach eine Stufe dunkler. */
|
||||
const WALL_FILL = "#ecebe8";
|
||||
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
|
||||
* oben ⇒ Schatten fällt nach rechts unten (+u, −v). */
|
||||
@@ -407,6 +415,14 @@ export function roofWorldFacesAll(project: Project): WorldFace[] {
|
||||
interface ProjectedOpening {
|
||||
pts: Array<[number, 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;
|
||||
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;
|
||||
}
|
||||
@@ -467,6 +485,30 @@ function projectOpeningRect(
|
||||
*/
|
||||
function collectOpenings(project: Project, frame: ElevationFrame): 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) {
|
||||
let thickness = 0.2;
|
||||
try {
|
||||
@@ -481,7 +523,83 @@ function collectOpenings(project: Project, frame: ElevationFrame): ProjectedOpen
|
||||
if (!iv) continue;
|
||||
const v = openingVerticalExtent(project, wall, op);
|
||||
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 ?? []) {
|
||||
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 oTop = Math.min(zTop, zBottom + d.height);
|
||||
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;
|
||||
@@ -732,22 +854,70 @@ export function generateElevationPlan(
|
||||
|
||||
// Fenster/Türen: Rahmen+Glas, knapp VOR der Wirtsfläche (auf der Fassade).
|
||||
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);
|
||||
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({
|
||||
depth: op.depth - EPS * 100,
|
||||
depth: d,
|
||||
seq: seq++,
|
||||
prim: {
|
||||
kind: "polygon",
|
||||
pts,
|
||||
fill: mono ? GLASS_FILL_MONO : GLASS_FILL,
|
||||
pts: toVec(op.pts),
|
||||
fill: mono ? "#ffffff" : FRAME_FILL,
|
||||
stroke: EDGE_INK,
|
||||
strokeWidthMm: FRAME_MM,
|
||||
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.
|
||||
|
||||
@@ -1141,6 +1141,19 @@ export async function computeSection(
|
||||
plane.normal[2],
|
||||
);
|
||||
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).
|
||||
appendRoofSections(output, project, plane);
|
||||
attachCutStyles(output, project, plane);
|
||||
|
||||
Reference in New Issue
Block a user