From 53144c8b060fb991c5fb3e138dbc899a3e38a15b Mon Sep 17 00:00:00 2001 From: Karim Date: Sat, 22 Aug 2026 11:28:59 +0200 Subject: [PATCH] Gruppen-Isolation: Rahmen im Viewport, Knopf dort platziert, Bbox-Auswahl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drei Verbesserungen nach Vectorworks-Vorbild (Screenshot-Referenz): - Sichtbarer Rahmen um die ganze Ansicht, solange eine Gruppe betreten ist (statt nur der gedimmten Elemente ohne Kontur). - "Gruppe verlassen" sitzt jetzt oben rechts IM Viewport (wie in Vectorworks), nicht mehr als lose schwebender Knopf über der ganzen App. - Ist eine GRUPPE (nicht einzelne Elemente) gewählt, zeigt die Auswahl nur noch eine gestrichelte Bounding-Box mit vier Eckpunkten statt jeden einzelnen Mitglieds-Umriss einzeln zu umranden. --- src/App.tsx | 15 +----- src/plan/PlanView.tsx | 87 +++++++++++++++++++++++++++++++++++ src/styles.css | 60 +++++++++++++++++------- src/views/viewportContent.tsx | 44 +++++++++++++++++- 4 files changed, 174 insertions(+), 32 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0672a5b..baf4b78 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2857,6 +2857,7 @@ export default function App() { selectedStairIds={selectedStairIds} selectedExtrudedSolidIds={selectedExtrudedSolidIds} selectedColumnIds={selectedColumnIds} + selectedGroupIds={selectedGroupIds} selectedRoomIds={selectedRoomIds} selectedRoofIds={selectedRoofIds} selectedContextObjectIds={selectedContextObjectIds} @@ -2866,6 +2867,7 @@ export default function App() { onStampEdit={onOpenStampEditor} onEditDrawingText={onEditDrawingText} onEnterGroup={onEnterGroup} + onLeaveGroup={onLeaveGroup} groupPath={groupPath} editTextId={editTextId} editTextMinHeightM={editTextMinHeightM} @@ -2906,19 +2908,6 @@ export default function App() { onEdit3dWallTop={onEdit3dWallTop} onEdit3dEnd={onEdit3dEnd} /> - {/* "Gruppe verlassen" (Vectorworks-artig): sichtbar, solange eine - Gruppe betreten ist (Isolation — alles andere gedimmt/gesperrt, - s. viewportContent.tsx). Verlässt genau EIN Level. */} - {groupPath.length > 0 && ( - - )} {/* Command-Line (Rhino-artig) UNTER dem Viewport — nur Viewport-breit. Prompt + klickbare Inline-Optionen + Texteingabe + Autocomplete. */} ( selectedSectionLineIds, selectedExtrudedSolidIds, selectedColumnIds, + selectedGroupIds, roomStamp, onStampMove, onStampEdit, @@ -2270,6 +2278,48 @@ export const PlanView = forwardRef( ); }, [plan, selectedColumnIds]); + // Ist eine GRUPPE gewählt (statt einzelner Elemente), zeigt die Auswahl NUR + // eine einfache Bounding-Box mit vier Eckpunkten statt jedes einzelnen + // Mitglieds-Umrisses (Nutzer-Wunsch, Vectorworks-Vorbild: „einfach vier + // Aussenpunkte zeigen") — vereint dieselben Hervorhebungs-Listen, die sonst + // einzeln gezeichnet würden. + const groupBBox = useMemo(() => { + if (!selectedGroupIds || selectedGroupIds.length === 0) return null; + const pts: Vec2[] = []; + for (const poly of highlightPolys) pts.push(...poly.pts); + for (const poly of highlightCeilingPolys) pts.push(...poly.pts); + for (const poly of highlightRoomPolys) pts.push(...poly.pts); + for (const poly of highlightRoofPolys) pts.push(...poly.pts); + for (const poly of highlightColumnPolys) pts.push(...poly.pts); + const fromLineOrPoly = (p: { kind: string; a?: Vec2; b?: Vec2; from?: Vec2; to?: Vec2; center?: Vec2; pts?: Vec2[] }) => { + if (p.kind === "line" && p.a && p.b) pts.push(p.a, p.b); + else if (p.kind === "arc" && p.from && p.to) pts.push(p.from, p.to); + else if (p.pts) pts.push(...p.pts); + }; + for (const p of highlightDrawPrims) fromLineOrPoly(p); + for (const p of highlightOpeningPrims) fromLineOrPoly(p); + for (const p of highlightStairPrims) fromLineOrPoly(p); + if (pts.length === 0) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const p of pts) { + minX = Math.min(minX, p.x); + minY = Math.min(minY, p.y); + maxX = Math.max(maxX, p.x); + maxY = Math.max(maxY, p.y); + } + return { minX, minY, maxX, maxY }; + }, [ + selectedGroupIds, + highlightPolys, + highlightCeilingPolys, + highlightRoomPolys, + highlightRoofPolys, + highlightColumnPolys, + highlightDrawPrims, + highlightOpeningPrims, + highlightStairPrims, + ]); + // Zusammenhängende 2D-Zeichen-Linien (gleiche drawingId + Stil) zu Zügen // bündeln, damit sie als EIN / gezeichnet werden können — // so gehren dicke Ecken sauber (kein Butt-Cap-Sprung). generatePlan schiebt die @@ -2461,6 +2511,11 @@ export const PlanView = forwardRef( CAD-typische Gehrung an den Ecken (stroke-linejoin), keine Stufe bei dicken Linien. Nach der Haupt-Ebene gezeichnet (2D liegt ohnehin oben). */} {drawingRunEls} + {/* Ist eine GRUPPE gewählt, ersetzt die Bounding-Box unten die einzelnen + Mitglieds-Hervorhebungen (s. `groupBBox`) — dieser ganze Block bleibt + dann aus. */} + {!groupBBox && ( + <> {/* Auswahl-Hervorhebung: alle Bänder der gewählten Wand (bzw. das lokal getroffene Polygon) mit Akzentkontur überzeichnen. */} {highlightPolys.map((poly, i) => ( @@ -2640,6 +2695,38 @@ export const PlanView = forwardRef( /> ), )} + + )} + {/* Gruppen-Auswahl: nur eine Bounding-Box mit vier Eckpunkten (kein + Umriss jedes einzelnen Mitglieds) — s. `groupBBox`. */} + {groupBBox && (() => { + const c1 = toScreen({ x: groupBBox.minX, y: groupBBox.minY }); + const c2 = toScreen({ x: groupBBox.maxX, y: groupBBox.minY }); + const c3 = toScreen({ x: groupBBox.maxX, y: groupBBox.maxY }); + const c4 = toScreen({ x: groupBBox.minX, y: groupBBox.maxY }); + const size = 5 / (meetScale(view, svgRef.current) ?? 1); + const corners = [c1, c2, c3, c4]; + return ( + <> + `${s.x},${s.y}`).join(" ")} + pointerEvents="none" + /> + {corners.map((s, i) => ( + + ))} + + ); + })()} {/* Auswahl-Rechteck (Marquee). Gestrichelt; die crossing-Variante (rechts→ links, berührend) hebt sich farblich/strichlich ab. Das Rechteck steht bereits in viewBox-Einheiten → direkt setzbar. */} diff --git a/src/styles.css b/src/styles.css index 0547469..6d88fbb 100644 --- a/src/styles.css +++ b/src/styles.css @@ -2110,6 +2110,23 @@ body { vector-effect: non-scaling-stroke; } +/* Gruppen-Auswahl (Vectorworks-artig): statt jedes Mitglieds-Umrisses nur eine + gestrichelte Bounding-Box mit vier Eckpunkt-Griffen — dieselbe Akzentfarbe + wie die normale Auswahl, aber gestrichelt (sofort als "Gruppe" erkennbar). */ +.plan-svg .plan-group-bbox { + fill: none; + stroke: var(--accent); + stroke-width: 1.5; + stroke-dasharray: 6 4; + vector-effect: non-scaling-stroke; +} +.plan-svg .plan-group-bbox-corner { + fill: var(--accent); + stroke: #fff; + stroke-width: 1.5; + vector-effect: non-scaling-stroke; +} + /* Hervorhebung selektierter 2D-Zeichenelemente (Mehrfach/Marquee): Akzentkontur über der Geometrie, keine Füllung. */ .plan-svg .plan-sel-draw { @@ -2119,25 +2136,34 @@ body { vector-effect: non-scaling-stroke; } -/* "Gruppe verlassen" (Vectorworks-artig): schwebender Knopf oben links über - dem Grundriss, sichtbar nur solange eine Gruppe betreten ist (Isolation). */ -.plan-leave-group-btn { - position: absolute; - top: 10px; - left: 10px; - z-index: 20; - padding: 6px 12px; - border-radius: 6px; - border: 1px solid var(--accent); - background: var(--field-bg); - color: var(--accent); - font-size: 12px; - font-weight: 600; - cursor: pointer; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); +/* Gruppen-Isolation (Vectorworks „Gruppe betreten"): sichtbarer Rahmen um + die ganze Ansicht, solange eine Gruppe betreten ist — dieselbe visuelle + Sprache wie Vectorworks' "Ansichtsbereich"-Rahmen. */ +.pane-isolated { + outline: 2px solid var(--accent); + outline-offset: -2px; } -.plan-leave-group-btn:hover { + +/* "Gruppe verlassen": Knopf oben RECHTS in der Ansicht (analog `.pane-title` + oben links), sichtbar nur solange eine Gruppe betreten ist. */ +.pane-leave-group-btn { + position: absolute; + top: 12px; + right: 12px; + z-index: 3; + padding: 5px 11px; + border-radius: 6px; + border: 1px solid var(--accent-border); background: var(--accent-dim); + color: var(--muted); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + cursor: pointer; +} +.pane-leave-group-btn:hover { + color: var(--accent); + border-color: var(--accent); } /* Auswahl-Rechteck (Marquee, Links-Drag): gestrichelte Akzentkontur, leichte diff --git a/src/views/viewportContent.tsx b/src/views/viewportContent.tsx index a7d2333..a9fe145 100644 --- a/src/views/viewportContent.tsx +++ b/src/views/viewportContent.tsx @@ -88,6 +88,7 @@ export function Content({ selectedStairIds, selectedExtrudedSolidIds, selectedColumnIds, + selectedGroupIds, selectedRoomIds, selectedRoofIds, selectedContextObjectIds, @@ -98,6 +99,7 @@ export function Content({ onEditDrawingText, onEnterGroup, groupPath, + onLeaveGroup, editTextId, editTextMinHeightM, onCommitTextEdit, @@ -183,6 +185,7 @@ export function Content({ selectedStairIds: string[]; selectedExtrudedSolidIds: string[]; selectedColumnIds: string[]; + selectedGroupIds: string[]; selectedRoomIds: string[]; selectedRoofIds: string[]; selectedContextObjectIds: string[]; @@ -193,6 +196,7 @@ export function Content({ onEditDrawingText?: (drawingId: string) => void; onEnterGroup?: (hit: GroupableHit) => boolean; groupPath: string[]; + onLeaveGroup?: () => void; editTextId?: string | null; editTextMinHeightM?: number | null; onCommitTextEdit?: (doc: RichTextDoc) => void; @@ -315,6 +319,7 @@ export function Content({ selectedStairIds={selectedStairIds} selectedExtrudedSolidIds={selectedExtrudedSolidIds} selectedColumnIds={selectedColumnIds} + selectedGroupIds={selectedGroupIds} selectedRoomIds={selectedRoomIds} selectedRoofIds={selectedRoofIds} selectedSectionLineId={selectedSectionLineId} @@ -324,6 +329,7 @@ export function Content({ onEditDrawingText={onEditDrawingText} onEnterGroup={onEnterGroup} groupPath={groupPath} + onLeaveGroup={onLeaveGroup} editTextId={editTextId} editTextMinHeightM={editTextMinHeightM} onCommitTextEdit={onCommitTextEdit} @@ -382,6 +388,7 @@ export function Content({ selectedStairIds={selectedStairIds} selectedExtrudedSolidIds={selectedExtrudedSolidIds} selectedColumnIds={selectedColumnIds} + selectedGroupIds={selectedGroupIds} selectedRoomIds={selectedRoomIds} selectedRoofIds={selectedRoofIds} selectedContextObjectIds={selectedContextObjectIds} @@ -392,6 +399,7 @@ export function Content({ onEditDrawingText={onEditDrawingText} onEnterGroup={onEnterGroup} groupPath={groupPath} + onLeaveGroup={onLeaveGroup} editTextId={editTextId} editTextMinHeightM={editTextMinHeightM} onCommitTextEdit={onCommitTextEdit} @@ -492,6 +500,7 @@ export function Content({ selectedStairIds={selectedStairIds} selectedExtrudedSolidIds={selectedExtrudedSolidIds} selectedColumnIds={selectedColumnIds} + selectedGroupIds={selectedGroupIds} selectedRoomIds={selectedRoomIds} selectedRoofIds={selectedRoofIds} selectedContextObjectIds={selectedContextObjectIds} @@ -502,6 +511,7 @@ export function Content({ onEditDrawingText={onEditDrawingText} onEnterGroup={onEnterGroup} groupPath={groupPath} + onLeaveGroup={onLeaveGroup} editTextId={editTextId} editTextMinHeightM={editTextMinHeightM} onCommitTextEdit={onCommitTextEdit} @@ -550,6 +560,7 @@ function LevelPlanView({ selectedStairIds, selectedExtrudedSolidIds, selectedColumnIds, + selectedGroupIds, selectedRoomIds, selectedRoofIds, selectedContextObjectIds, @@ -560,6 +571,7 @@ function LevelPlanView({ onEditDrawingText, onEnterGroup, groupPath, + onLeaveGroup, editTextId, editTextMinHeightM, onCommitTextEdit, @@ -601,6 +613,7 @@ function LevelPlanView({ selectedStairIds: string[]; selectedExtrudedSolidIds: string[]; selectedColumnIds: string[]; + selectedGroupIds: string[]; selectedRoomIds: string[]; selectedRoofIds: string[]; selectedContextObjectIds?: string[]; @@ -611,6 +624,7 @@ function LevelPlanView({ onEditDrawingText?: (drawingId: string) => void; onEnterGroup?: (hit: GroupableHit) => boolean; groupPath: string[]; + onLeaveGroup?: () => void; editTextId?: string | null; editTextMinHeightM?: number | null; onCommitTextEdit?: (doc: RichTextDoc) => void; @@ -683,12 +697,22 @@ function LevelPlanView({ }); return ( -
+
0 ? "pane pane-isolated" : "pane"}>
{isDrawing ? t("content.drawingTitle", { name: level.name }) : t("content.grundrissTitle", { name: level.name })}
+ {groupPath.length > 0 && ( + + )} void; onEnterGroup?: (hit: GroupableHit) => boolean; groupPath: string[]; + onLeaveGroup?: () => void; editTextId?: string | null; editTextMinHeightM?: number | null; onCommitTextEdit?: (doc: RichTextDoc) => void; @@ -921,10 +950,20 @@ function SectionPlanView({ } return ( -
+
0 ? "pane pane-isolated" : "pane"}>
{t("content.sectionTitle", { name: level.name, kind: kindLabel })}
+ {groupPath.length > 0 && ( + + )}