God-Files aufgeteilt (ResourceManager/generatePlan/toWalls3d/App.tsx-Handler), Totcode entfernt (geometry-Crate, OCCT/HLR-Spike), PDF/Bild-Import ergänzt.
ResourceManager.tsx, generatePlan.ts und toWalls3d.ts sind nach Bauteil- Domäne in src/ui/resourceManager/, src/plan/generatePlan/ und src/plan/toWalls3d/ aufgeteilt (reine Verschiebung, keine Verhaltens- änderung). App.tsx verliert weitere Handler-Gruppen an eigene Hooks (useExportHandlers, useImportHandling, useKeyboardShortcuts) sowie Hit-Testing an src/viewport/planHitTest.ts. Unbenutzte Rust-geometry-Crate (durch WASM-Joins abgelöst) sowie der alte OCCT/HLR-Schnitt-Spike (hlr.ts/occt.ts, vite occt-Aliases) entfernt. Neu: PDF-Import als Rasterbild (src/io/pdfImport.ts) + gemeinsamer Bild-Asset-Helfer (src/io/imageAsset.ts) für Plan- und Layout-Bilder. tsc -b sauber, vitest 891/891 grün.
This commit is contained in:
+113
-4
@@ -14,7 +14,7 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import type { HatchRender, Plan, Primitive } from "./generatePlan";
|
||||
import { docToLines, type SvgLine } from "../text/renderHtml";
|
||||
import { docToLines, wrapDocToLines, type SvgLine } from "../text/renderHtml";
|
||||
import type { EdgeGrip, Vec2 } from "../model/types";
|
||||
import type { SnapResult, ToolDraft, ToolHandlers, ToolId, ToolMods } from "../tools/types";
|
||||
import { useGlPlanRenderer } from "./useGlPlanRenderer";
|
||||
@@ -566,9 +566,14 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
||||
// vollen SVG-Pfad nutzen — dort liegt das Bild als eigene Ebene ZWISCHEN
|
||||
// Blatt-Hintergrund und Geometrie (sichtbar, ohne den GL-Renderer zu ändern).
|
||||
const hasBgImages = (plan.backgroundImages?.length ?? 0) > 0;
|
||||
// Eingebettete Bild-Zeichenelemente (Drawing2D `{shape:"image"}`, Foto/Logo/
|
||||
// PDF-Seite) kann das GPU-Backend ebenfalls nicht texturieren (kein Bild-
|
||||
// Primitiv in render2d, s. toRenderScene.ts `RImage`-Kommentar) — dieselbe
|
||||
// SVG-Fallback-Regel wie bei georeferenzierten Luftbildern.
|
||||
const hasDrawingImages = plan.primitives.some((p) => p.kind === "drawingImage");
|
||||
// Engine WIRKLICH aktiv? Steuert das Umschalten SVG↔GPU: bei false rendert der
|
||||
// volle SVG-Pfad (Fallback), bei true übernimmt die GPU-Ebene + Text-Overlay.
|
||||
const useGpuRenderer = wantGpu && glReady && !hasBgImages;
|
||||
const useGpuRenderer = wantGpu && glReady && !hasBgImages && !hasDrawingImages;
|
||||
|
||||
// Stabiler Papier-Massstab-Nenner N (1:N) als REFERENZ für die Print-Strich-
|
||||
// breiten. BEWUSST NICHT der live aus dem Zoom abgeleitete N (der sich beim
|
||||
@@ -959,8 +964,8 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
||||
}
|
||||
}
|
||||
if (bestId) return bestId;
|
||||
// Kein Linien-Treffer: Text-Elemente prüfen (eigene Bounding-Box-Logik).
|
||||
return pickDrawingText(clientX, clientY);
|
||||
// Kein Linien-Treffer: Text- dann Bild-Elemente prüfen (eigene Bbox-Logik).
|
||||
return pickDrawingText(clientX, clientY) ?? pickDrawingImage(clientX, clientY);
|
||||
};
|
||||
|
||||
// Trifft der Klick ein Text-2D-Element (kind "drawingText")? Grosszügige Modell-
|
||||
@@ -988,6 +993,21 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
||||
return null;
|
||||
};
|
||||
|
||||
// Trifft der Klick ein Bild-2D-Element (kind "drawingImage")? Reine
|
||||
// Bounding-Box-Prüfung (achsparallel, keine Rotation). Liefert die drawingId
|
||||
// oder null.
|
||||
const pickDrawingImage = (clientX: number, clientY: number): string | null => {
|
||||
const m = rawModelAt(clientX, clientY);
|
||||
for (let i = plan.primitives.length - 1; i >= 0; i--) {
|
||||
const p = plan.primitives[i];
|
||||
if (p.kind !== "drawingImage" || !p.drawingId) continue;
|
||||
if (m.x >= p.min.x && m.x <= p.max.x && m.y >= p.min.y && m.y <= p.max.y) {
|
||||
return p.drawingId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Nächstgelegene Öffnung (Fenster/Tür) zum Cursor: Linien-/Bogen-Primitive mit
|
||||
// `openingId` per Bildschirm-Nähe, plus getroffene Fenster-Rahmen (Polygon mit
|
||||
// openingId, per Punkt-in-Polygon). Liefert die openingId oder null.
|
||||
@@ -3815,6 +3835,25 @@ function renderPrimitive(
|
||||
</text>
|
||||
);
|
||||
}
|
||||
case "drawingImage": {
|
||||
// Eingebettetes Rasterbild (Foto/Logo/gerasterte PDF-Seite), achsparallel.
|
||||
// Screen-Y ist gespiegelt (toScreen) → oben-links = (min.x, max.y).
|
||||
const topLeft = toScreen(p.max);
|
||||
const w = (p.max.x - p.min.x) * PX_PER_M;
|
||||
const h = (p.max.y - p.min.y) * PX_PER_M;
|
||||
if (w <= 0 || h <= 0) return null;
|
||||
return (
|
||||
<image
|
||||
href={p.src}
|
||||
x={toScreen(p.min).x}
|
||||
y={topLeft.y}
|
||||
width={w}
|
||||
height={h}
|
||||
opacity={(p.greyed ? 0.45 : 1) * p.opacity}
|
||||
preserveAspectRatio="none"
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "drawingText": {
|
||||
// Modellverankerter Einzeltext (DXF-Import / Text-Werkzeug): Höhe in Metern →
|
||||
// viewBox-Einheiten (skaliert mit dem Zoom), Baseline am Ankerpunkt (links).
|
||||
@@ -3822,6 +3861,76 @@ function renderPrimitive(
|
||||
const fontSize = p.heightM * PX_PER_M;
|
||||
// Modell-Winkel CCW; Screen-Y ist gespiegelt und SVG-rotate CW-positiv → -deg.
|
||||
const deg = -(p.angle * 180) / Math.PI;
|
||||
|
||||
// Reiches Textfeld (InDesign-artig, mehrere Absätze/gemischte Formatierung):
|
||||
// hat Vorrang vor dem Alt-Pfad (einfacher String + einheitliche `marks`)
|
||||
// weiter unten. Bricht bei gesetzter Spaltenbreite wortweise um (Run-Grenzen
|
||||
// bleiben erhalten, s. `wrapDocToLines`).
|
||||
if (p.doc) {
|
||||
const unitPerPt = (0.0254 / 72) * PX_PER_M;
|
||||
const basePt = (p.heightM / 0.0254) * 72;
|
||||
const lineHeight = fontSize * 1.25;
|
||||
const wrapWidthUnits = p.wrapWidth && p.wrapWidth > 0 ? p.wrapWidth * PX_PER_M : undefined;
|
||||
const docLines: SvgLine[] = wrapDocToLines(p.doc, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
lineHeight,
|
||||
unitPerPt,
|
||||
basePt,
|
||||
color: p.color,
|
||||
...(wrapWidthUnits ? { wrapWidthUnits } : {}),
|
||||
});
|
||||
if (docLines.length === 0) return null;
|
||||
const lineYs: number[] = [];
|
||||
{
|
||||
let y = c.y;
|
||||
docLines.forEach((line, k) => {
|
||||
if (k > 0) y += line.lineHeight;
|
||||
lineYs.push(y);
|
||||
});
|
||||
}
|
||||
return (
|
||||
<text
|
||||
opacity={p.greyed ? 0.45 : undefined}
|
||||
transform={deg ? `rotate(${deg} ${c.x} ${c.y})` : undefined}
|
||||
style={{ pointerEvents: "none", userSelect: "none" }}
|
||||
>
|
||||
{docLines.map((line, k) => {
|
||||
const anchor =
|
||||
line.align === "center" ? "middle" : line.align === "right" ? "end" : "start";
|
||||
const x = !wrapWidthUnits
|
||||
? c.x
|
||||
: line.align === "center"
|
||||
? c.x + wrapWidthUnits / 2
|
||||
: line.align === "right"
|
||||
? c.x + wrapWidthUnits
|
||||
: c.x;
|
||||
return (
|
||||
<tspan key={k} x={x} y={lineYs[k]} textAnchor={anchor}>
|
||||
{line.tspans.length === 0
|
||||
? ""
|
||||
: line.tspans.map((ts, j) => (
|
||||
<tspan
|
||||
key={j}
|
||||
fontSize={ts.fontSize}
|
||||
fontWeight={ts.fontWeight}
|
||||
fontStyle={ts.fontStyle}
|
||||
fontFamily={ts.fontFamily ?? "var(--font-ui, sans-serif)"}
|
||||
fill={ts.fill}
|
||||
textDecoration={ts.textDecoration}
|
||||
>
|
||||
{ts.text}
|
||||
</tspan>
|
||||
))}
|
||||
</tspan>
|
||||
);
|
||||
})}
|
||||
</text>
|
||||
);
|
||||
}
|
||||
|
||||
// Alt-Pfad: einfacher String + einheitliche `marks` (Text-Werkzeug/DXF-Import
|
||||
// ohne Rich-Doc) — unverändert.
|
||||
// Spaltentext: bei gesetzter `wrapWidth` den Text wortweise auf die Breite
|
||||
// umbrechen (Zeichenbreite ≈ 0.55·fontSize geschätzt), sonst eine Zeile.
|
||||
const lineH = fontSize * 1.25;
|
||||
|
||||
Reference in New Issue
Block a user