Fenster/Tür 3D: tiefer, Detailgrad im 3D, kein Fenster→Tür-Umschalten
Nutzer-Feedback:
- Fenster wirkten im 3D flach: die Rahmen-Einbautiefe wurde fälschlich aus
frameThickness (Profil-Breite, ~6 cm) statt aus der Wanddicke bestimmt.
Jetzt füllt der Rahmen die VOLLE Wanddicke (tiefe Laibung), die Scheibe
ist eine dünne Tafel mittig darin → plastische Fenster mit sichtbarer
Laibung. frameDepth übersteuert weiterhin.
- Detailgrad grob/mittel/fein wirkt jetzt auch im 3D: grob = nur Loch +
Scheibe, mittel = Rahmen (+Oberlicht-Kämpfer), fein = zusätzlich
Flügel-Mittelpfosten/Kämpfer-Sprossen. Durchgereicht App → Viewport3D →
Wasm3DViewport → useWasm3dRenderer → projectToModel3d({detail}).
- Fenster↔Tür-Umschalter im Objekt-Info entfernt (nur noch Anzeige): die
Gattung wird beim Platzieren festgelegt, nicht nachträglich getauscht.
+1 Test (Detailgrad-Gating grob/mittel/fein). tsc + vitest grün.
This commit is contained in:
@@ -6124,6 +6124,7 @@ function Content({
|
|||||||
categoryDisplay={categoryDisplay}
|
categoryDisplay={categoryDisplay}
|
||||||
displayKey={displayKey}
|
displayKey={displayKey}
|
||||||
renderMode={renderMode}
|
renderMode={renderMode}
|
||||||
|
detail={detail}
|
||||||
view3d={view3d}
|
view3d={view3d}
|
||||||
fov={fov}
|
fov={fov}
|
||||||
gridElevation={level.baseElevation ?? 0}
|
gridElevation={level.baseElevation ?? 0}
|
||||||
|
|||||||
@@ -606,25 +606,14 @@ export function OpeningSection({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Art-Segment Fenster/Tür. */}
|
{/* Art (Fenster/Tür) — read-only: die Gattung wird beim Platzieren mit dem
|
||||||
|
jeweiligen Werkzeug festgelegt und NICHT nachträglich umgeschaltet
|
||||||
|
(ein Fenster in eine Tür zu verwandeln ergibt keinen sinnvollen Bauteil). */}
|
||||||
<div className="objinfo-field">
|
<div className="objinfo-field">
|
||||||
<span className="objinfo-flabel">{t("objinfo.opening.kind")}</span>
|
<span className="objinfo-flabel">{t("objinfo.opening.kind")}</span>
|
||||||
<div className="objinfo-segment">
|
<span className="objinfo-fval">
|
||||||
<button
|
{t(isDoor ? "objinfo.opening.kind.door" : "objinfo.opening.kind.window")}
|
||||||
type="button"
|
</span>
|
||||||
className={"objinfo-seg-btn" + (!isDoor ? " active" : "")}
|
|
||||||
onClick={() => host.onSetOpeningKind("window")}
|
|
||||||
>
|
|
||||||
{t("objinfo.opening.kind.window")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={"objinfo-seg-btn" + (isDoor ? " active" : "")}
|
|
||||||
onClick={() => host.onSetOpeningKind("door")}
|
|
||||||
>
|
|
||||||
{t("objinfo.opening.kind.door")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Wirts-Wand (read-only Anzeige des Geschosses/Wand-Bezugs). */}
|
{/* Wirts-Wand (read-only Anzeige des Geschosses/Wand-Bezugs). */}
|
||||||
|
|||||||
@@ -332,6 +332,23 @@ describe("Rahmen-/Sprossen-Riegel typisierter Öffnungen (emitOpeningFrames)", (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Detailgrad steuert Rahmen/Sprossen im 3D (grob=0, mittel=Rahmen, fein=+Mittelpfosten)", () => {
|
||||||
|
// O1 mit erzwungenem wingCount 2 -> im FEIN-Fall ein Flügel-Mittelpfosten extra.
|
||||||
|
const o1 = (sampleProject.openings ?? []).find((o) => o.id === "O1");
|
||||||
|
if (!o1) throw new Error("O1 fehlt im sampleProject");
|
||||||
|
const p: Project = {
|
||||||
|
...sampleProject,
|
||||||
|
openings: [{ ...o1, wingCount: 2 }],
|
||||||
|
doors: [],
|
||||||
|
context: [],
|
||||||
|
};
|
||||||
|
const frameCount = (detail: "grob" | "mittel" | "fein"): number =>
|
||||||
|
projectToModel3d(p, { detail }).meshes.filter(isFrame).length;
|
||||||
|
expect(frameCount("grob")).toBe(0); // keine Rahmen/Sprossen
|
||||||
|
expect(frameCount("mittel")).toBe(4); // 2 Pfosten + Sturz + Schwelle, KEIN Mittelpfosten
|
||||||
|
expect(frameCount("fein")).toBe(5); // + 1 Flügel-Mittelpfosten (wingCount 2)
|
||||||
|
});
|
||||||
|
|
||||||
it("transomHeight > 0 -> zusätzlicher Kämpfer-Riegel + zweite Glasscheibe im Oberlicht-Band", () => {
|
it("transomHeight > 0 -> zusätzlicher Kämpfer-Riegel + zweite Glasscheibe im Oberlicht-Band", () => {
|
||||||
const p: Project = {
|
const p: Project = {
|
||||||
...sampleProject,
|
...sampleProject,
|
||||||
|
|||||||
+48
-21
@@ -49,6 +49,7 @@ import { wallVerticalExtent, ceilingVerticalExtent, columnVerticalExtent, stairV
|
|||||||
import { columnFootprint } from "../geometry/column";
|
import { columnFootprint } from "../geometry/column";
|
||||||
import { pointInOutline } from "../geometry/ceiling";
|
import { pointInOutline } from "../geometry/ceiling";
|
||||||
import { openingInterval, openingVerticalExtent, openingJambs } from "../geometry/opening";
|
import { openingInterval, openingVerticalExtent, openingJambs } from "../geometry/opening";
|
||||||
|
import type { DetailLevel } from "../ui/TopBar";
|
||||||
import { stairGeometry, stairBBox } from "../geometry/stair";
|
import { stairGeometry, stairBBox } from "../geometry/stair";
|
||||||
import { extrudePolygon, extrudeCircle } from "../engine/truckSolid";
|
import { extrudePolygon, extrudeCircle } from "../engine/truckSolid";
|
||||||
import { computeJoins } from "../model/joins";
|
import { computeJoins } from "../model/joins";
|
||||||
@@ -1729,11 +1730,13 @@ function clampRange(v: number, lo: number, hi: number): number {
|
|||||||
* (übersprungen, nächster Fallback greift).
|
* (übersprungen, nächster Fallback greift).
|
||||||
*/
|
*/
|
||||||
function resolveAcrossWallDepth(
|
function resolveAcrossWallDepth(
|
||||||
frameThickness: number | undefined,
|
|
||||||
frameDepth: number | undefined,
|
frameDepth: number | undefined,
|
||||||
wallThickness: number,
|
wallThickness: number,
|
||||||
): number {
|
): number {
|
||||||
if (frameThickness !== undefined && frameThickness > EPS) return frameThickness;
|
// Rahmen-Tiefe quer zur Wand: explizite `frameDepth` gewinnt, sonst füllt der
|
||||||
|
// Rahmen die VOLLE Wanddicke (tiefe Laibung/Reveal). Das `frameThickness` ist
|
||||||
|
// die Profil-Breite des Rahmenstabs (2D/Ansicht), NICHT die Einbautiefe — es
|
||||||
|
// hier zu verwenden liess Fenster im 3D flach wirken (nur wenige cm tief).
|
||||||
if (frameDepth !== undefined && frameDepth > EPS) return frameDepth;
|
if (frameDepth !== undefined && frameDepth > EPS) return frameDepth;
|
||||||
return wallThickness;
|
return wallThickness;
|
||||||
}
|
}
|
||||||
@@ -1798,7 +1801,7 @@ function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): Opening
|
|||||||
if (op.kind === "door") {
|
if (op.kind === "door") {
|
||||||
const dt = getDoorType(project, op);
|
const dt = getDoorType(project, op);
|
||||||
if (!dt) return null;
|
if (!dt) return null;
|
||||||
const acrossWallDepth = resolveAcrossWallDepth(dt.frameThickness, dt.frameDepth, wallThickness);
|
const acrossWallDepth = resolveAcrossWallDepth(dt.frameDepth, wallThickness);
|
||||||
const { nMin, nMax } = resolveFrameNormalRange(
|
const { nMin, nMax } = resolveFrameNormalRange(
|
||||||
halfThickness,
|
halfThickness,
|
||||||
acrossWallDepth,
|
acrossWallDepth,
|
||||||
@@ -1819,7 +1822,7 @@ function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): Opening
|
|||||||
}
|
}
|
||||||
const wt = getWindowType(project, op);
|
const wt = getWindowType(project, op);
|
||||||
if (!wt) return null;
|
if (!wt) return null;
|
||||||
const acrossWallDepth = resolveAcrossWallDepth(wt.frameThickness, wt.frameDepth, wallThickness);
|
const acrossWallDepth = resolveAcrossWallDepth(wt.frameDepth, wallThickness);
|
||||||
const { nMin, nMax } = resolveFrameNormalRange(
|
const { nMin, nMax } = resolveFrameNormalRange(
|
||||||
halfThickness,
|
halfThickness,
|
||||||
acrossWallDepth,
|
acrossWallDepth,
|
||||||
@@ -1854,6 +1857,7 @@ function frameMeshesForOpening(
|
|||||||
iv: { from: number; to: number },
|
iv: { from: number; to: number },
|
||||||
vert: { zBottom: number; zTop: number },
|
vert: { zBottom: number; zTop: number },
|
||||||
params: OpeningFrameParams,
|
params: OpeningFrameParams,
|
||||||
|
fine = true,
|
||||||
): RMesh[] {
|
): RMesh[] {
|
||||||
const out: RMesh[] = [];
|
const out: RMesh[] = [];
|
||||||
const fw = params.frameWidth;
|
const fw = params.frameWidth;
|
||||||
@@ -1880,20 +1884,24 @@ function frameMeshesForOpening(
|
|||||||
push(sashFrom, sashTo, splitZ - fw / 2, splitZ + fw / 2); // Kämpfer-Riegel
|
push(sashFrom, sashTo, splitZ - fw / 2, splitZ + fw / 2); // Kämpfer-Riegel
|
||||||
sashTop = splitZ - fw / 2;
|
sashTop = splitZ - fw / 2;
|
||||||
}
|
}
|
||||||
// Flügel-Mittelpfosten (vertikal, wingCount−1) im Sash-Bereich.
|
// Flügel-Mittelpfosten + Kämpfer-Zeilen (Sprossen) nur bei FEINEM Detailgrad —
|
||||||
if (params.wingCount > 1 && sashTo - sashFrom > EPS) {
|
// bei „mittel" bleibt es beim reinen Rahmen (+ Oberlicht-Kämpfer oben).
|
||||||
const cell = (sashTo - sashFrom) / params.wingCount;
|
if (fine) {
|
||||||
for (let i = 1; i < params.wingCount; i++) {
|
// Flügel-Mittelpfosten (vertikal, wingCount−1) im Sash-Bereich.
|
||||||
const center = sashFrom + i * cell;
|
if (params.wingCount > 1 && sashTo - sashFrom > EPS) {
|
||||||
push(center - fw / 2, center + fw / 2, sashBottom, sashTop);
|
const cell = (sashTo - sashFrom) / params.wingCount;
|
||||||
|
for (let i = 1; i < params.wingCount; i++) {
|
||||||
|
const center = sashFrom + i * cell;
|
||||||
|
push(center - fw / 2, center + fw / 2, sashBottom, sashTop);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
// Kämpfer-Zeilen (horizontal, mullionRows−1) im Sash-Bereich.
|
||||||
// Kämpfer-Zeilen (horizontal, mullionRows−1) im Sash-Bereich.
|
if (params.mullionRows > 1 && sashTop - sashBottom > EPS) {
|
||||||
if (params.mullionRows > 1 && sashTop - sashBottom > EPS) {
|
const cell = (sashTop - sashBottom) / params.mullionRows;
|
||||||
const cell = (sashTop - sashBottom) / params.mullionRows;
|
for (let i = 1; i < params.mullionRows; i++) {
|
||||||
for (let i = 1; i < params.mullionRows; i++) {
|
const center = sashBottom + i * cell;
|
||||||
const center = sashBottom + i * cell;
|
push(sashFrom, sashTo, center - fw / 2, center + fw / 2);
|
||||||
push(sashFrom, sashTo, center - fw / 2, center + fw / 2);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -1919,8 +1927,15 @@ function glassPanesForOpening(
|
|||||||
const sashTo = iv.to - fw;
|
const sashTo = iv.to - fw;
|
||||||
const sashBottom = vert.zBottom + (params.hasSill ? fw : 0);
|
const sashBottom = vert.zBottom + (params.hasSill ? fw : 0);
|
||||||
const sashTopFull = vert.zTop - fw;
|
const sashTopFull = vert.zTop - fw;
|
||||||
|
// Die Scheibe ist eine DÜNNE Tafel mittig in der (nun tiefen) Rahmen-Laibung —
|
||||||
|
// nicht der volle Rahmen-Querschnitt, sonst wäre das „Glas" ein dicker Block
|
||||||
|
// und die Laibung/Reveal-Tiefe unsichtbar. So liest sich das Fenster plastisch:
|
||||||
|
// Rahmen zieht in die Wand, die Scheibe sitzt zurückgesetzt in der Mitte.
|
||||||
|
const glassMid = (params.nMin + params.nMax) / 2;
|
||||||
|
const gN0 = glassMid - GLASS_HALF_THICK;
|
||||||
|
const gN1 = glassMid + GLASS_HALF_THICK;
|
||||||
const push = (zBottom: number, zTop: number): void => {
|
const push = (zBottom: number, zTop: number): void => {
|
||||||
const mesh = openingAxisBox(wall, sashFrom, sashTo, zBottom, zTop, params.nMin, params.nMax, GLASS_RGB);
|
const mesh = openingAxisBox(wall, sashFrom, sashTo, zBottom, zTop, gN0, gN1, GLASS_RGB);
|
||||||
if (mesh) out.push(mesh);
|
if (mesh) out.push(mesh);
|
||||||
};
|
};
|
||||||
if (params.transomHeight > EPS) {
|
if (params.transomHeight > EPS) {
|
||||||
@@ -2002,7 +2017,10 @@ function emitOpeningGlass(project: Project): RMesh[] {
|
|||||||
* fehlt/verwaist) bekommen KEINEN Rahmen — exakt das Alt-Verhalten vor der
|
* fehlt/verwaist) bekommen KEINEN Rahmen — exakt das Alt-Verhalten vor der
|
||||||
* Typ-Vertiefung (Rückwärtskompatibilität).
|
* Typ-Vertiefung (Rückwärtskompatibilität).
|
||||||
*/
|
*/
|
||||||
function emitOpeningFrames(project: Project): RMesh[] {
|
function emitOpeningFrames(project: Project, detail: DetailLevel = "fein"): RMesh[] {
|
||||||
|
// Grob: keine Rahmen/Sprossen — die Öffnung bleibt Loch + Scheibe (schlank).
|
||||||
|
if (detail === "grob") return [];
|
||||||
|
const fine = detail === "fein";
|
||||||
const out: RMesh[] = [];
|
const out: RMesh[] = [];
|
||||||
for (const wall of project.walls) {
|
for (const wall of project.walls) {
|
||||||
for (const op of openingsOfWall(project, wall.id)) {
|
for (const op of openingsOfWall(project, wall.id)) {
|
||||||
@@ -2012,7 +2030,7 @@ function emitOpeningFrames(project: Project): RMesh[] {
|
|||||||
if (vert.zTop - vert.zBottom <= EPS) continue;
|
if (vert.zTop - vert.zBottom <= EPS) continue;
|
||||||
const params = resolveOpeningFrame(project, wall, op);
|
const params = resolveOpeningFrame(project, wall, op);
|
||||||
if (!params) continue;
|
if (!params) continue;
|
||||||
out.push(...frameMeshesForOpening(wall, iv, vert, params));
|
out.push(...frameMeshesForOpening(wall, iv, vert, params, fine));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -2070,11 +2088,20 @@ function computeJoinsByFloor(project: Project): Map<string, WallCuts> {
|
|||||||
*/
|
*/
|
||||||
export interface Model3dOptions {
|
export interface Model3dOptions {
|
||||||
layeredWalls?: boolean;
|
layeredWalls?: boolean;
|
||||||
|
/**
|
||||||
|
* Detailgrad der Öffnungs-Darstellung im 3D (wie im 2D-Grundriss):
|
||||||
|
* • "grob" — keine Rahmen/Sprossen (nur Loch + Scheibe).
|
||||||
|
* • "mittel"— Rahmen (Pfosten/Sturz/Schwelle) + ggf. Kämpfer, keine Sprossen.
|
||||||
|
* • "fein" — zusätzlich Flügel-Mittelpfosten + Kämpfer-Zeilen (Sprossen).
|
||||||
|
* Fehlt es, gilt "fein" (= bisheriges Verhalten, volle Details).
|
||||||
|
*/
|
||||||
|
detail?: DetailLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Das volle 3D-Modell: Wände (mit Öffnungen) + Deckenplatten + Kontext-Meshes. */
|
/** Das volle 3D-Modell: Wände (mit Öffnungen) + Deckenplatten + Kontext-Meshes. */
|
||||||
export function projectToModel3d(project: Project, opts: Model3dOptions = {}): RModel3d {
|
export function projectToModel3d(project: Project, opts: Model3dOptions = {}): RModel3d {
|
||||||
const layered = opts.layeredWalls ?? true;
|
const layered = opts.layeredWalls ?? true;
|
||||||
|
const detail = opts.detail ?? "fein";
|
||||||
return {
|
return {
|
||||||
walls: projectToWalls3d(project, layered),
|
walls: projectToWalls3d(project, layered),
|
||||||
// Decken + Treppen (Treppentritte als extrudierte Slabs) — so wird die Treppe
|
// Decken + Treppen (Treppentritte als extrudierte Slabs) — so wird die Treppe
|
||||||
@@ -2089,7 +2116,7 @@ export function projectToModel3d(project: Project, opts: Model3dOptions = {}): R
|
|||||||
meshes: [
|
meshes: [
|
||||||
...emitMeshes(project),
|
...emitMeshes(project),
|
||||||
...emitOpeningGlass(project),
|
...emitOpeningGlass(project),
|
||||||
...emitOpeningFrames(project),
|
...emitOpeningFrames(project, detail),
|
||||||
...emitStairMeshes(project),
|
...emitStairMeshes(project),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import {
|
|||||||
import { MaterialRuntime } from "../materials/runtime";
|
import { MaterialRuntime } from "../materials/runtime";
|
||||||
import { Wasm3DViewport } from "./Wasm3DViewport";
|
import { Wasm3DViewport } from "./Wasm3DViewport";
|
||||||
import type { Pick3dHit } from "./Wasm3DViewport";
|
import type { Pick3dHit } from "./Wasm3DViewport";
|
||||||
import type { RenderMode, View3d } from "../ui/TopBar";
|
import type { DetailLevel, RenderMode, View3d } from "../ui/TopBar";
|
||||||
import type { ToolDraft } from "../tools/types";
|
import type { ToolDraft } from "../tools/types";
|
||||||
import { applyAngleConstraint } from "../tools/snapping";
|
import { applyAngleConstraint } from "../tools/snapping";
|
||||||
import { drawingGripVertices, drawingMoveAnchor } from "./drawingGrips";
|
import { drawingGripVertices, drawingMoveAnchor } from "./drawingGrips";
|
||||||
@@ -143,6 +143,7 @@ export function Viewport3D(
|
|||||||
project={props.project}
|
project={props.project}
|
||||||
view3d={props.view3d}
|
view3d={props.view3d}
|
||||||
renderMode={props.renderMode}
|
renderMode={props.renderMode}
|
||||||
|
detail={props.detail}
|
||||||
gridVisible={gridVisible}
|
gridVisible={gridVisible}
|
||||||
groundElevation={props.gridElevation}
|
groundElevation={props.gridElevation}
|
||||||
onToggleGrid={onToggleGrid}
|
onToggleGrid={onToggleGrid}
|
||||||
@@ -210,6 +211,8 @@ function ThreeViewport3D({
|
|||||||
* schwarze Kantenlinien, tiefen-getestet → Hidden-Line-Look). Default shaded.
|
* schwarze Kantenlinien, tiefen-getestet → Hidden-Line-Look). Default shaded.
|
||||||
*/
|
*/
|
||||||
renderMode?: RenderMode;
|
renderMode?: RenderMode;
|
||||||
|
/** Detailgrad (grob/mittel/fein) — reicht die Öffnungs-Details ins wgpu-Modell. */
|
||||||
|
detail?: DetailLevel;
|
||||||
/**
|
/**
|
||||||
* Render-Entscheidung je Geschoss-ID (Darstellungsmodus „Zeichnungsebenen").
|
* Render-Entscheidung je Geschoss-ID (Darstellungsmodus „Zeichnungsebenen").
|
||||||
* Ohne Angabe werden alle sichtbaren Geschosse normal gezeigt.
|
* Ohne Angabe werden alle sichtbaren Geschosse normal gezeigt.
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import type { Project, Wall, Drawing2D, Ceiling, Vec2 } from "../model/types";
|
import type { Project, Wall, Drawing2D, Ceiling, Vec2 } from "../model/types";
|
||||||
import { ceilingVerticalExtent, wallVerticalExtent } from "../model/wall";
|
import { ceilingVerticalExtent, wallVerticalExtent } from "../model/wall";
|
||||||
import { projectToModel3d, pickGeometry, TRUCK_MESH_READY_EVENT } from "../plan/toWalls3d";
|
import { projectToModel3d, pickGeometry, TRUCK_MESH_READY_EVENT } from "../plan/toWalls3d";
|
||||||
import type { RenderMode, View3d } from "../ui/TopBar";
|
import type { DetailLevel, RenderMode, View3d } from "../ui/TopBar";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useWasm3dRenderer } from "./useWasm3dRenderer";
|
import { useWasm3dRenderer } from "./useWasm3dRenderer";
|
||||||
import type { Camera3d } from "./useWasm3dRenderer";
|
import type { Camera3d } from "./useWasm3dRenderer";
|
||||||
@@ -288,6 +288,7 @@ export function Wasm3DViewport({
|
|||||||
project,
|
project,
|
||||||
view3d = "perspective",
|
view3d = "perspective",
|
||||||
renderMode = "shaded",
|
renderMode = "shaded",
|
||||||
|
detail = "fein",
|
||||||
gridVisible = true,
|
gridVisible = true,
|
||||||
groundElevation = 0,
|
groundElevation = 0,
|
||||||
onToggleGrid,
|
onToggleGrid,
|
||||||
@@ -308,6 +309,8 @@ export function Wasm3DViewport({
|
|||||||
/** Darstellungsart (Oberleiste) — alle fünf Werte wirken hier, s.
|
/** Darstellungsart (Oberleiste) — alle fünf Werte wirken hier, s.
|
||||||
* Datei-Kommentar. */
|
* Datei-Kommentar. */
|
||||||
renderMode?: RenderMode;
|
renderMode?: RenderMode;
|
||||||
|
/** Detailgrad (grob/mittel/fein) — steuert die Öffnungs-Rahmen/Sprossen im 3D. */
|
||||||
|
detail?: DetailLevel;
|
||||||
/** Boden-Referenzraster sichtbar? Umgeschaltet über den Overlay-Button unten
|
/** Boden-Referenzraster sichtbar? Umgeschaltet über den Overlay-Button unten
|
||||||
* rechts im Viewport (`onToggleGrid`) — Single Source of Truth ist der
|
* rechts im Viewport (`onToggleGrid`) — Single Source of Truth ist der
|
||||||
* Aufrufer (App/viewSlice `grid3dVisible`), diese Komponente spiegelt nur. */
|
* Aufrufer (App/viewSlice `grid3dVisible`), diese Komponente spiegelt nur. */
|
||||||
@@ -382,6 +385,8 @@ export function Wasm3DViewport({
|
|||||||
onEditEndRef.current = onEditEnd;
|
onEditEndRef.current = onEditEnd;
|
||||||
const groundElevationRef = useRef(groundElevation);
|
const groundElevationRef = useRef(groundElevation);
|
||||||
groundElevationRef.current = groundElevation;
|
groundElevationRef.current = groundElevation;
|
||||||
|
const detailRef = useRef(detail);
|
||||||
|
detailRef.current = detail;
|
||||||
const {
|
const {
|
||||||
render,
|
render,
|
||||||
updateModel,
|
updateModel,
|
||||||
@@ -414,9 +419,9 @@ export function Wasm3DViewport({
|
|||||||
// Snapshot) — nur das `view3d`-Effekt unten springt aktiv auf ein Praeset.
|
// Snapshot) — nur das `view3d`-Effekt unten springt aktiv auf ein Praeset.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ready) return;
|
if (!ready) return;
|
||||||
updateModel(project);
|
updateModel(project, detail);
|
||||||
redrawRef.current();
|
redrawRef.current();
|
||||||
}, [ready, project, updateModel]);
|
}, [ready, project, detail, updateModel]);
|
||||||
|
|
||||||
// truck-Extrusionen (s. toWalls3d.ts emitExtrudedSolids): die WASM-Extrusion
|
// truck-Extrusionen (s. toWalls3d.ts emitExtrudedSolids): die WASM-Extrusion
|
||||||
// läuft asynchron und ist beim ersten `updateModel` oben typischerweise noch
|
// läuft asynchron und ist beim ersten `updateModel` oben typischerweise noch
|
||||||
@@ -425,7 +430,7 @@ export function Wasm3DViewport({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ready) return;
|
if (!ready) return;
|
||||||
const onMeshReady = () => {
|
const onMeshReady = () => {
|
||||||
updateModel(projectRef.current);
|
updateModel(projectRef.current, detailRef.current);
|
||||||
redrawRef.current();
|
redrawRef.current();
|
||||||
};
|
};
|
||||||
window.addEventListener(TRUCK_MESH_READY_EVENT, onMeshReady);
|
window.addEventListener(TRUCK_MESH_READY_EVENT, onMeshReady);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import type { Project } from "../model/types";
|
import type { Project } from "../model/types";
|
||||||
import { projectToModel3d } from "../plan/toWalls3d";
|
import { projectToModel3d } from "../plan/toWalls3d";
|
||||||
|
import type { DetailLevel } from "../ui/TopBar";
|
||||||
import { loadEngine3d } from "../engine/engine3d";
|
import { loadEngine3d } from "../engine/engine3d";
|
||||||
import { decodeMaterialTextures } from "./materialTextures";
|
import { decodeMaterialTextures } from "./materialTextures";
|
||||||
|
|
||||||
@@ -166,12 +167,13 @@ export function useWasm3dRenderer(
|
|||||||
// geändert haben (updateModel läuft bei JEDER Projektänderung).
|
// geändert haben (updateModel läuft bei JEDER Projektänderung).
|
||||||
const materialSigRef = useRef<string | null>(null);
|
const materialSigRef = useRef<string | null>(null);
|
||||||
|
|
||||||
/** Modell bei Projektänderung neu setzen (re-extrudiert in Rust). */
|
/** Modell bei Projektänderung neu setzen (re-extrudiert in Rust). `detail`
|
||||||
const updateModel = useCallback((project: Project) => {
|
* steuert den Öffnungs-Detailgrad (grob/mittel/fein) im 3D wie im Grundriss. */
|
||||||
|
const updateModel = useCallback((project: Project, detail?: DetailLevel) => {
|
||||||
const r = rendererRef.current;
|
const r = rendererRef.current;
|
||||||
if (!r || !project) return;
|
if (!r || !project) return;
|
||||||
try {
|
try {
|
||||||
r.set_model(JSON.stringify(projectToModel3d(project)));
|
r.set_model(JSON.stringify(projectToModel3d(project, { detail })));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("render3d set_model fehlgeschlagen:", e);
|
console.warn("render3d set_model fehlgeschlagen:", e);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user