Lineale oben + links in allen 2D-Ansichten (Vectorworks-Stil)
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.
This commit is contained in:
+165
-4
@@ -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<PlanViewHandle, PlanViewProps>(
|
||||
activeTool,
|
||||
toolInputActive,
|
||||
toolHandlers,
|
||||
showRulers = true,
|
||||
hudFields,
|
||||
grips,
|
||||
edgeGrips,
|
||||
@@ -885,11 +888,16 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
||||
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<Vec2 | null>(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<PlanViewHandle, PlanViewProps>(
|
||||
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<PlanViewHandle, PlanViewProps>(
|
||||
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 (
|
||||
<PlanRulers
|
||||
vb={vb}
|
||||
vbPerPx={1 / scale}
|
||||
pxPerMeter={scale * PX_PER_M}
|
||||
cursor={rulerCursor}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
@@ -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 (
|
||||
<g className="plan-ruler" pointerEvents="none">
|
||||
{/* Leisten + Trennlinien. */}
|
||||
<rect className="plan-ruler-strip" x={x0} y={y0} width={vb.w} height={TH} />
|
||||
<rect className="plan-ruler-strip" x={x0} y={y0} width={TH} height={vb.h} />
|
||||
<line className="plan-ruler-border" x1={x0} y1={y0 + TH} x2={x0 + vb.w} y2={y0 + TH} />
|
||||
<line className="plan-ruler-border" x1={x0 + TH} y1={y0} x2={x0 + TH} y2={y0 + vb.h} />
|
||||
{/* Oben: Ticks + Beschriftung. */}
|
||||
{xTicks.map((t, i) =>
|
||||
t.vbX < x0 + TH ? null : (
|
||||
<g key={`x${i}`}>
|
||||
<line
|
||||
className="plan-ruler-tick"
|
||||
x1={t.vbX}
|
||||
y1={y0 + (t.major ? TH * 0.45 : TH * 0.72)}
|
||||
x2={t.vbX}
|
||||
y2={y0 + TH}
|
||||
/>
|
||||
{t.major && (
|
||||
<text className="plan-ruler-text" x={t.vbX + 3 * vbPerPx} y={y0 + TH * 0.42} fontSize={font}>
|
||||
{label(t.model)}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
),
|
||||
)}
|
||||
{/* Links: Ticks + gedrehte Beschriftung. */}
|
||||
{yTicks.map((t, i) =>
|
||||
t.vbY < y0 + TH ? null : (
|
||||
<g key={`y${i}`}>
|
||||
<line
|
||||
className="plan-ruler-tick"
|
||||
x1={x0 + (t.major ? TH * 0.45 : TH * 0.72)}
|
||||
y1={t.vbY}
|
||||
x2={x0 + TH}
|
||||
y2={t.vbY}
|
||||
/>
|
||||
{t.major && (
|
||||
<text
|
||||
className="plan-ruler-text"
|
||||
x={x0 + TH * 0.42}
|
||||
y={t.vbY - 3 * vbPerPx}
|
||||
fontSize={font}
|
||||
transform={`rotate(-90 ${x0 + TH * 0.42} ${t.vbY - 3 * vbPerPx})`}
|
||||
>
|
||||
{label(t.model)}
|
||||
</text>
|
||||
)}
|
||||
</g>
|
||||
),
|
||||
)}
|
||||
{/* Cursor-Marker (VW-gelb) je Achse. */}
|
||||
{cursor && (
|
||||
<>
|
||||
<rect
|
||||
className="plan-ruler-cursor"
|
||||
x={cursor.x * PX_PER_M - 1.5 * vbPerPx}
|
||||
y={y0}
|
||||
width={3 * vbPerPx}
|
||||
height={TH}
|
||||
/>
|
||||
<rect
|
||||
className="plan-ruler-cursor"
|
||||
x={x0}
|
||||
y={-cursor.y * PX_PER_M - 1.5 * vbPerPx}
|
||||
width={TH}
|
||||
height={3 * vbPerPx}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{/* Ecke oben links. */}
|
||||
<rect className="plan-ruler-corner" x={x0} y={y0} width={TH} height={TH} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user