Ansicht als Linienzeichnung (VW-Look): weisse Flächen + XOR-Silhouette
Nutzer-Referenz ('so sieht es ordentlich aus'): Architektur-Ansichten sind
Linienzeichnungen, keine Grauflächen. Umsetzung:
- Alle Flächen weiss (Fassade/Dach/Rahmen/Glas/Türblatt); Kanten tragen.
- Frontale Wand-/Deckenflächen je Tiefen-Gruppe: Flächen ohne Kontur, dann
der Vereinigungs-UMRISS der Gruppe über Kanten-XOR (xorOutlineEdges) —
Innenkanten der Pfeiler-/Sturz-/Geschoss-Teilboxen heben sich paarweise
auf, übrig bleibt exakt die Silhouette inklusive der Öffnungslöcher.
- Dach + schräg angeschnittene Flächen behalten ihre eigene Kontur.
Painter-Verdeckung bleibt (weisse Füllung nah überdeckt fern). 737/737 grün;
headless verifiziert — deckungsgleich mit der VW-Referenz.
This commit is contained in:
+107
-12
@@ -69,7 +69,7 @@ const EDGE_MM = 0.13;
|
|||||||
/** Fenster/Tür-Rahmen: etwas kräftigere Umrisslinie. */
|
/** Fenster/Tür-Rahmen: etwas kräftigere Umrisslinie. */
|
||||||
const FRAME_MM = 0.18;
|
const FRAME_MM = 0.18;
|
||||||
/** Blaugraues Fenster-Glas (dunkler als die Fassade → als Öffnung lesbar). */
|
/** Blaugraues Fenster-Glas (dunkler als die Fassade → als Öffnung lesbar). */
|
||||||
const GLASS_FILL = "#7f8b99";
|
const GLASS_FILL = "#ffffff";
|
||||||
/** Glas im Mono-Modus (neutrales Grau statt Blaugrau). */
|
/** Glas im Mono-Modus (neutrales Grau statt Blaugrau). */
|
||||||
const GLASS_FILL_MONO = "#c4c8cd";
|
const GLASS_FILL_MONO = "#c4c8cd";
|
||||||
/** Festes Schattengrau (über der Fassade, unter den Kanten). */
|
/** Festes Schattengrau (über der Fassade, unter den Kanten). */
|
||||||
@@ -82,12 +82,12 @@ const GROUND_MM = 0.5;
|
|||||||
* klassische Architektur-Ansicht (Flächen tragen, Kanten nur wo bedeutsam:
|
* klassische Architektur-Ansicht (Flächen tragen, Kanten nur wo bedeutsam:
|
||||||
* Dachrand, Öffnungen, Bodenlinie). Wand/Decke identisch (Decken-Stirnflächen
|
* Dachrand, Öffnungen, Bodenlinie). Wand/Decke identisch (Decken-Stirnflächen
|
||||||
* 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 = "#ffffff";
|
||||||
const ROOF_FILL = "#d9d6d1";
|
const ROOF_FILL = "#ffffff";
|
||||||
/** Blendrahmen-Fläche der Öffnungen (heller als die Fassade). */
|
/** Blendrahmen-Fläche der Öffnungen. */
|
||||||
const FRAME_FILL = "#f8f7f4";
|
const FRAME_FILL = "#ffffff";
|
||||||
/** Türblatt-Fläche (zwischen Fassade und Glas). */
|
/** Türblatt-Fläche. */
|
||||||
const DOOR_LEAF_FILL = "#d4d2cd";
|
const DOOR_LEAF_FILL = "#ffffff";
|
||||||
/** Öffnungssymbol (DIN-Andeutung): dezente Annotation, nicht Bauteilkante. */
|
/** Öffnungssymbol (DIN-Andeutung): dezente Annotation, nicht Bauteilkante. */
|
||||||
const SYMBOL_INK = "#7d858f";
|
const SYMBOL_INK = "#7d858f";
|
||||||
|
|
||||||
@@ -292,6 +292,8 @@ export interface ProjectedFace {
|
|||||||
depth: number;
|
depth: number;
|
||||||
/** Kleinste Punkt-Tiefe (nächste Kante — z. B. die auskragende Traufe). */
|
/** Kleinste Punkt-Tiefe (nächste Kante — z. B. die auskragende Traufe). */
|
||||||
depthMin: number;
|
depthMin: number;
|
||||||
|
/** Grösste Punkt-Tiefe — depthMax≈depthMin ⇒ frontale (ebene) Fläche. */
|
||||||
|
depthMax: number;
|
||||||
role: FaceRole;
|
role: FaceRole;
|
||||||
wallId?: string;
|
wallId?: string;
|
||||||
ceilingId?: string;
|
ceilingId?: string;
|
||||||
@@ -310,15 +312,17 @@ export function projectFace(frame: ElevationFrame, face: WorldFace): ProjectedFa
|
|||||||
const pts: Array<[number, number]> = [];
|
const pts: Array<[number, number]> = [];
|
||||||
let depthSum = 0;
|
let depthSum = 0;
|
||||||
let depthMin = Infinity;
|
let depthMin = Infinity;
|
||||||
|
let depthMax = -Infinity;
|
||||||
for (const w of face.poly) {
|
for (const w of face.poly) {
|
||||||
const p = projectPoint(frame, w);
|
const p = projectPoint(frame, w);
|
||||||
pts.push([p.u, p.v]);
|
pts.push([p.u, p.v]);
|
||||||
depthSum += p.depth;
|
depthSum += p.depth;
|
||||||
depthMin = Math.min(depthMin, p.depth);
|
depthMin = Math.min(depthMin, p.depth);
|
||||||
|
depthMax = Math.max(depthMax, p.depth);
|
||||||
}
|
}
|
||||||
const depth = depthSum / face.poly.length;
|
const depth = depthSum / face.poly.length;
|
||||||
if (depth <= EPS) return null; // vor der Ebene → ignorieren
|
if (depth <= EPS) return null; // vor der Ebene → ignorieren
|
||||||
return { pts, depth, depthMin, role: face.role, wallId: face.wallId, ceilingId: face.ceilingId, roofId: face.roofId };
|
return { pts, depth, depthMin, depthMax, role: face.role, wallId: face.wallId, ceilingId: face.ceilingId, roofId: face.roofId };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -732,6 +736,69 @@ export function buildShadows(walls: ProjectedFace[], slabs: ProjectedFace[]): Sh
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Umriss-Vereinigung frontaler Flächengruppen ─────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vereinigungs-UMRISS einer Gruppe achsparalleler (u,v)-Rechteckflächen über
|
||||||
|
* Kanten-XOR: jede Kante wird als Intervall auf ihrer Trägerlinie gesammelt;
|
||||||
|
* Bereiche mit GERADER Überdeckung (geteilte Innenkanten zweier Teilboxen)
|
||||||
|
* heben sich auf, ungerade Bereiche bilden die Silhouette — inklusive der
|
||||||
|
* Fensterlöcher (die Teilboxen um eine Öffnung lassen genau deren Rand übrig).
|
||||||
|
* Nicht-achsparallele Kanten werden unverändert durchgereicht.
|
||||||
|
*/
|
||||||
|
export function xorOutlineEdges(
|
||||||
|
polys: Array<Array<[number, number]>>,
|
||||||
|
): Array<[[number, number], [number, number]]> {
|
||||||
|
const EPS_AX = 1e-6;
|
||||||
|
const keyOf = (x: number): number => Math.round(x * 1e5);
|
||||||
|
const horiz = new Map<number, Array<[number, number]>>();
|
||||||
|
const vert = new Map<number, Array<[number, number]>>();
|
||||||
|
const out: Array<[[number, number], [number, number]]> = [];
|
||||||
|
for (const poly of polys) {
|
||||||
|
for (let i = 0; i < poly.length; i++) {
|
||||||
|
const a = poly[i];
|
||||||
|
const b = poly[(i + 1) % poly.length];
|
||||||
|
if (Math.abs(a[1] - b[1]) < EPS_AX) {
|
||||||
|
const k = keyOf(a[1]);
|
||||||
|
if (!horiz.has(k)) horiz.set(k, []);
|
||||||
|
horiz.get(k)!.push([Math.min(a[0], b[0]), Math.max(a[0], b[0])]);
|
||||||
|
} else if (Math.abs(a[0] - b[0]) < EPS_AX) {
|
||||||
|
const k = keyOf(a[0]);
|
||||||
|
if (!vert.has(k)) vert.set(k, []);
|
||||||
|
vert.get(k)!.push([Math.min(a[1], b[1]), Math.max(a[1], b[1])]);
|
||||||
|
} else {
|
||||||
|
out.push([a, b]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sweep = (
|
||||||
|
m: Map<number, Array<[number, number]>>,
|
||||||
|
emit: (coord: number, s: number, e: number) => void,
|
||||||
|
): void => {
|
||||||
|
for (const [k, ivs] of m) {
|
||||||
|
const evs: Array<[number, number]> = [];
|
||||||
|
for (const [s, e] of ivs) {
|
||||||
|
if (e - s < 1e-9) continue;
|
||||||
|
evs.push([s, 1], [e, -1]);
|
||||||
|
}
|
||||||
|
evs.sort((p, q) => p[0] - q[0] || q[1] - p[1]);
|
||||||
|
let count = 0;
|
||||||
|
let start = 0;
|
||||||
|
const coord = k / 1e5;
|
||||||
|
for (const [x, d] of evs) {
|
||||||
|
const wasOdd = count % 2 === 1;
|
||||||
|
count += d;
|
||||||
|
const isOdd = count % 2 === 1;
|
||||||
|
if (!wasOdd && isOdd) start = x;
|
||||||
|
else if (wasOdd && !isOdd && x - start > 1e-9) emit(coord, start, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sweep(horiz, (v, s, e) => out.push([[s, v], [e, v]]));
|
||||||
|
sweep(vert, (u, s, e) => out.push([[u, s], [u, e]]));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Plan-Zusammenbau ────────────────────────────────────────────────────────
|
// ── Plan-Zusammenbau ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Optionen des Ansichts-Generators. */
|
/** Optionen des Ansichts-Generators. */
|
||||||
@@ -799,12 +866,24 @@ export function generateElevationPlan(
|
|||||||
const items: Item[] = [];
|
const items: Item[] = [];
|
||||||
let seq = 0;
|
let seq = 0;
|
||||||
|
|
||||||
|
// Frontale Wand-/Deckenflächen (alle Ecken auf gleicher Tiefe) werden je
|
||||||
|
// Tiefen-Gruppe gesammelt: die Flächen selbst weiss OHNE Kontur, dann der
|
||||||
|
// XOR-Vereinigungs-Umriss der Gruppe als Linienzug — Innenkanten der
|
||||||
|
// Teilboxen (Pfeiler/Sturz/Geschossfugen) heben sich auf, übrig bleibt die
|
||||||
|
// Silhouette samt Öffnungslöchern (VW-Linienzeichnungs-Look).
|
||||||
|
const frontalGroups = new Map<number, { faces: ProjectedFace[]; depth: number }>();
|
||||||
for (const f of faces) {
|
for (const f of faces) {
|
||||||
const pts: Vec2[] = f.pts.map(([u, v]) => {
|
const pts: Vec2[] = f.pts.map(([u, v]) => {
|
||||||
acc(u, v);
|
acc(u, v);
|
||||||
return { x: u, y: v };
|
return { x: u, y: v };
|
||||||
});
|
});
|
||||||
const isRoof = f.role === "roof";
|
const isRoof = f.role === "roof";
|
||||||
|
const frontal = !isRoof && f.depthMax - f.depthMin < 1e-4;
|
||||||
|
if (frontal) {
|
||||||
|
const gk = Math.round(f.depth * 1e4);
|
||||||
|
if (!frontalGroups.has(gk)) frontalGroups.set(gk, { faces: [], depth: f.depth });
|
||||||
|
frontalGroups.get(gk)!.faces.push(f);
|
||||||
|
}
|
||||||
items.push({
|
items.push({
|
||||||
depth: f.depth,
|
depth: f.depth,
|
||||||
seq: seq++,
|
seq: seq++,
|
||||||
@@ -816,10 +895,10 @@ export function generateElevationPlan(
|
|||||||
kind: "polygon",
|
kind: "polygon",
|
||||||
pts,
|
pts,
|
||||||
fill: fillFor(f.role),
|
fill: fillFor(f.role),
|
||||||
// Nur das Dach trägt eine Kontur — die Fassade bleibt EINE ruhige
|
// Dach + schräg angeschnittene Flächen tragen ihre eigene Kontur;
|
||||||
// Fläche (ihr Rand liest sich über den Kontrast zum Papier).
|
// frontale Flächen bekommen den Gruppen-Umriss (unten).
|
||||||
stroke: isRoof ? EDGE_INK : "none",
|
stroke: isRoof || !frontal ? EDGE_INK : "none",
|
||||||
strokeWidthMm: isRoof ? EDGE_MM : 0,
|
strokeWidthMm: isRoof || !frontal ? EDGE_MM : 0,
|
||||||
hatch: NO_HATCH,
|
hatch: NO_HATCH,
|
||||||
...(f.wallId ? { wallId: f.wallId } : {}),
|
...(f.wallId ? { wallId: f.wallId } : {}),
|
||||||
...(f.ceilingId ? { ceilingId: f.ceilingId } : {}),
|
...(f.ceilingId ? { ceilingId: f.ceilingId } : {}),
|
||||||
@@ -827,6 +906,22 @@ export function generateElevationPlan(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
for (const g of frontalGroups.values()) {
|
||||||
|
for (const [a, b] of xorOutlineEdges(g.faces.map((f) => f.pts))) {
|
||||||
|
items.push({
|
||||||
|
depth: g.depth - EPS * 5,
|
||||||
|
seq: seq++,
|
||||||
|
prim: {
|
||||||
|
kind: "line",
|
||||||
|
a: { x: a[0], y: a[1] },
|
||||||
|
b: { x: b[0], y: b[1] },
|
||||||
|
cls: "elevation-outline",
|
||||||
|
weightMm: EDGE_MM,
|
||||||
|
color: EDGE_INK,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Schatten (leicht VOR der Empfängertiefe, damit sie direkt nach der Fassade,
|
// Schatten (leicht VOR der Empfängertiefe, damit sie direkt nach der Fassade,
|
||||||
// aber unter den Öffnungen/Kanten näherer Flächen liegen).
|
// aber unter den Öffnungen/Kanten näherer Flächen liegen).
|
||||||
|
|||||||
Reference in New Issue
Block a user