From 1945fecd11ba60773cb9b1ce22686f4c64ecf929 Mon Sep 17 00:00:00 2001 From: Karim Date: Sat, 11 Jul 2026 02:22:14 +0200 Subject: [PATCH] Lineale oben + links in allen 2D-Ansichten (Vectorworks-Stil) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screen-fixe Lineal-Leisten über dem sichtbaren Ausschnitt: Major-Ticks mit Meter-Beschriftung ('5.000m', Schrittweite 1/2/5·10^n nach Zoom), Minor-Ticks, gedrehte Beschriftung am linken Lineal, gelber Cursor-Marker je Achse, Ecke oben links. pointerEvents none — Klicks gehen durch. Gilt für Grundriss, Schnitt, Ansicht und Zeichnungs-Ebenen (PlanRulers in PlanView, Default an, per showRulers-Prop abschaltbar). 737/737 grün; headless verifiziert. --- src/plan/PlanView.tsx | 169 +++++++++++++++++++++++++++++++++++++++++- src/styles.css | 32 ++++++++ 2 files changed, 197 insertions(+), 4 deletions(-) diff --git a/src/plan/PlanView.tsx b/src/plan/PlanView.tsx index fe70c86..40f9b5d 100644 --- a/src/plan/PlanView.tsx +++ b/src/plan/PlanView.tsx @@ -359,6 +359,8 @@ export interface PlanViewProps { * und zeichnet den zurückgegebenen `draft` als Overlay (Vorschau + Snap-Marker). */ toolHandlers?: ToolHandlers; + /** Lineale (oben + links, VW-Stil) einblenden. Default an. */ + showRulers?: boolean; /** * Feld-Status der Befehlszeile fürs Cursor-HUD (Tab-Feld-Zyklus §2.7): * gelockte/live Werte je Feld, aktives Tab-Ziel und der gerade getippte @@ -455,6 +457,7 @@ export const PlanView = forwardRef( activeTool, toolInputActive, toolHandlers, + showRulers = true, hudFields, grips, edgeGrips, @@ -885,11 +888,16 @@ export const PlanView = forwardRef( alt: e.altKey, }); - // Cursor in Modell-Metern nach oben melden (für die Status-Leiste). + // Cursor in Modell-Metern nach oben melden (für die Status-Leiste) und + // lokal für die Lineal-Marker halten (App re-rendert bei jedem Move ohnehin + // über onCursor → X/Y-Anzeige; das lokale State-Update kostet nichts extra). + const [rulerCursor, setRulerCursor] = useState(null); const reportCursor = (clientX: number, clientY: number) => { - if (!onCursor) return; const v = clientToView(clientX, clientY, view); - onCursor(viewToModel(v.x, v.y)); + const m = viewToModel(v.x, v.y); + setRulerCursor(m); + if (!onCursor) return; + onCursor(m); }; // Trefferprüfung: liefert den Index des obersten Polygon-Primitivs unter dem @@ -2005,7 +2013,7 @@ export const PlanView = forwardRef( onPointerMove={onPointerMove} onPointerUp={endDrag} onPointerCancel={endDrag} - onPointerLeave={() => onCursor?.(null)} + onPointerLeave={() => { setRulerCursor(null); onCursor?.(null); }} onWheel={onWheel} onDoubleClick={onDoubleClick} onContextMenu={onContextMenuHandler} @@ -2324,6 +2332,27 @@ export const PlanView = forwardRef( hudFields={hudFields ?? null} /> )} + {/* Lineale (oben + links, VW-Stil): Modell-Meter mit Ticks, Beschriftung + und Cursor-Marker — screen-fix über den sichtbaren Ausschnitt gelegt. */} + {showRulers && (() => { + const m = measure(view); + if (!m.rect) return null; + const scale = m.scale || 1; + const vb = { + x0: view.x - m.offX / scale, + y0: view.y - m.offY / scale, + w: (m.rect.width || 1) / scale, + h: (m.rect.height || 1) / scale, + }; + return ( + + ); + })()} ); @@ -3745,3 +3774,135 @@ function renderPrimitive( } } } + +/** Schrittweite (Meter) fürs Lineal: 1/2/5·10^n, sodass Major-Ticks ≥ ~64 px auseinander liegen. */ +function rulerStep(pxPerMeter: number): number { + const minPx = 64; + let step = Math.pow(10, Math.ceil(Math.log10(minPx / pxPerMeter))); + for (const f of [0.2, 0.5, 1]) { + if (step * f * pxPerMeter >= minPx) return step * f; + } + return step; +} + +/** + * Lineale oben + links (Vectorworks-Stil): helle Leisten mit Major-Ticks + + * Meter-Beschriftung („5.000m"), Minor-Ticks und einem Cursor-Marker je Achse. + * Alles screen-fix (Grössen über vbPerPx), pointerEvents none — die Leisten + * liegen optisch über dem Plan, schlucken aber keine Klicks. + */ +function PlanRulers({ + vb, + vbPerPx, + pxPerMeter, + cursor, +}: { + /** Sichtbarer Ausschnitt in viewBox-Einheiten. */ + vb: { x0: number; y0: number; w: number; h: number }; + vbPerPx: number; + pxPerMeter: number; + cursor: Vec2 | null; +}) { + const TH = 20 * vbPerPx; // Leisten-Dicke (screen-px → viewBox) + const step = rulerStep(pxPerMeter); + const minor = step / 5; + const font = 9 * vbPerPx; + // Negative Null vermeiden („−0.000m" bei Startwerten knapp unter 0). + const label = (v: number): string => `${(Math.abs(v) < 1e-9 ? 0 : v).toFixed(3)}m`; + + // Sichtbarer Modellbereich (Plan-Y nach oben: vbY = −y·PX_PER_M). + const mx0 = vb.x0 / PX_PER_M; + const mx1 = (vb.x0 + vb.w) / PX_PER_M; + const myTop = -vb.y0 / PX_PER_M; + const myBottom = -(vb.y0 + vb.h) / PX_PER_M; + + const xTicks: { vbX: number; model: number; major: boolean }[] = []; + const startX = Math.floor(mx0 / minor) * minor; + for (let v = startX; v <= mx1 + 1e-9; v += minor) { + const major = Math.abs(v / step - Math.round(v / step)) < 1e-6; + xTicks.push({ vbX: v * PX_PER_M, model: v, major }); + } + const yTicks: { vbY: number; model: number; major: boolean }[] = []; + const startY = Math.floor(myBottom / minor) * minor; + for (let v = startY; v <= myTop + 1e-9; v += minor) { + const major = Math.abs(v / step - Math.round(v / step)) < 1e-6; + yTicks.push({ vbY: -v * PX_PER_M, model: v, major }); + } + + const x0 = vb.x0; + const y0 = vb.y0; + return ( + + {/* Leisten + Trennlinien. */} + + + + + {/* Oben: Ticks + Beschriftung. */} + {xTicks.map((t, i) => + t.vbX < x0 + TH ? null : ( + + + {t.major && ( + + {label(t.model)} + + )} + + ), + )} + {/* Links: Ticks + gedrehte Beschriftung. */} + {yTicks.map((t, i) => + t.vbY < y0 + TH ? null : ( + + + {t.major && ( + + {label(t.model)} + + )} + + ), + )} + {/* Cursor-Marker (VW-gelb) je Achse. */} + {cursor && ( + <> + + + + )} + {/* Ecke oben links. */} + + + ); +} diff --git a/src/styles.css b/src/styles.css index 9252ce8..eeaf88f 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2058,6 +2058,38 @@ body { font-weight: 700; } +/* Lineale (oben + links, VW-Stil): helle Papier-Leisten, Tinte-Ticks, + gelber Cursor-Marker. */ +.plan-svg .plan-ruler-strip { + fill: #f7f6f3; + fill-opacity: 0.96; +} +.plan-svg .plan-ruler-border { + stroke: #b9b5ae; + stroke-width: 1; + vector-effect: non-scaling-stroke; +} +.plan-svg .plan-ruler-tick { + stroke: #6d6a64; + stroke-width: 1; + vector-effect: non-scaling-stroke; +} +.plan-svg .plan-ruler-text { + fill: #3c3a36; + font-family: var(--mono, ui-monospace, monospace); + font-weight: 500; +} +.plan-svg .plan-ruler-cursor { + fill: #e8c33c; + fill-opacity: 0.9; +} +.plan-svg .plan-ruler-corner { + fill: #f0efec; + stroke: #b9b5ae; + stroke-width: 1; + vector-effect: non-scaling-stroke; +} + /* Weicher Winkel-Snap: gestrichelte Führungslinie (dezent) + Winkel-Badge in derselben dunklen Dropdown-Pille wie das Cursor-HUD. */ .plan-svg .plan-angle-guide-line {