Panel-Konsistenz: Gestaltung context-aware + Tabellen-Cleanup

Gestaltung:
- Backend detektiert geometryKind (curve / curveOpen / 3d / mixed),
  ignoriert DOSSIER-Source-Curves (wand_axis etc.) damit die
  Klassifikation einer Wand-Selektion (Axis + Volume) als reines 3D
  durchgeht.
- UI zeigt sektion-spezifische Section-Heads: 2D-Curve → Fill+Pen,
  3D-Solid → Section Style+Boundary. Section Style + Fill nutzen
  jetzt einen gemeinsamen HatchEditor (gleiche Controls).
- _set_section_style schreibt per-Object SectionHatchIndex/Scale/
  Rotation/Color via _try_set_attr-Multifallback.
- Selection-Summary liest dieselben Properties zurueck.
- ColorBar als Pill (borderRadius 999) statt eckig.
- "Attribute"-Header oben raus (war alt + redundant).

Ebenen / Zeichnungsebenen:
- Aktive Zeile wieder als Pill (borderRadius 999) — aber ohne
  Margin/Shift, Inhalt springt nicht. Kein bold-text-change mehr.
- Linker borderLeft komplett raus — vorher leere graue Spalte am
  Panel-Rand sichtbar.
- Inhalt mehr nach links (Padding 8/6 statt 12).
- Zeilen-minHeight 24, kompaktere Icons.
- EbenenSettings: Ebene-Picker auf BarCombo.
- GeschossManager: Gebaeudehoehe-Zeile raus, Stift→Settings-Icon.

ElementeApp:
- Alle btn-contained/btn-outlined → BarToggle (Wand-Aufbau, Raum-
  Align, Treppe-Lage/Modus, Oeffnung-Ref/Rahmen/Fluegel/Glas,
  AutoOverride).
- ReferenzSelector → BarToggle.
- "Neues Element"-Container ohne Box-Border, Header-Slot
  beherbergt Projektuebersicht-Pill statt label.
- Property-Labels (UK/OK/Dicke/...) von text-muted auf
  text-secondary (war zu blass).
- Gruener Accent-Border um Properties-Containern → normaler border.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 01:17:58 +02:00
parent 15fb0a6037
commit 9ae8574ab0
5 changed files with 307 additions and 177 deletions
+109 -135
View File
@@ -1,8 +1,10 @@
import { useState, useEffect, useRef } from 'react'
import Icon from './components/Icon'
import { BarCombo, BarToggle, BarButton } from './components/BarControls'
import {
onMessage, notifyReady,
requestSelection, setColorSource, setLwSource, setLinetypeSource, setLinetypeScale, setFill,
setColorSource, setLwSource, setLinetypeSource, setLinetypeScale, setFill,
setSectionStyle,
} from './lib/rhinoBridge'
const LW_PRESETS = [0.02, 0.10, 0.13, 0.18, 0.25, 0.35, 0.50, 0.70, 1.00]
@@ -20,36 +22,20 @@ function SectionHead({ title }) {
)
}
/** Source-Dropdown im Vectorworks-Stil: Link-Icon + Label + Caret */
/** Source-Dropdown: BarCombo mit Link-Icon (active wenn Nach Ebene) */
function SourceSelect({ source, onChange, overrideLabel = 'Eigene' }) {
return (
<div style={{ position: 'relative', display: 'flex' }}>
<Icon
name={source === 'layer' ? 'link' : 'link_off'}
size={14}
style={{
position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
color: source === 'layer' ? 'var(--text-primary)' : 'var(--text-muted)',
pointerEvents: 'none',
}}
/>
<select
<div style={{ display: 'flex' }}>
<BarCombo
icon={source === 'layer' ? 'link' : 'link_off'}
iconActive={source === 'layer'}
value={source}
onChange={(ev) => onChange(ev.target.value)}
style={{
paddingLeft: 30, flex: 1,
fontFamily: 'var(--font)',
fontWeight: 500,
fontSize: 11,
textTransform: 'none',
/* borderRadius/background uebernehmen wir von der globalen <select>-
Pille (index.css) damit's optisch konsistent mit dem Fill-Dropdown
wirkt. Nur paddingLeft ueberschreiben wegen Link-Icon. */
}}
onChange={onChange}
title="Farb-Quelle"
>
<option value="layer">Nach Ebene</option>
<option value="object">{overrideLabel}</option>
</select>
</BarCombo>
</div>
)
}
@@ -62,7 +48,7 @@ function ColorBar({ color, onChange, height = 22 }) {
style={{
width: '100%', height,
background: color,
borderRadius: 'var(--r)',
borderRadius: 999,
border: '1px solid var(--border)',
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.08)',
}}
@@ -99,7 +85,7 @@ function ColorBar({ color, onChange, height = 22 }) {
style={{
position: 'relative', display: 'block',
width: '100%', height,
borderRadius: 'var(--r)',
borderRadius: 999,
border: '1px solid var(--border)',
overflow: 'hidden',
cursor: 'pointer',
@@ -214,14 +200,12 @@ function PenLw({ sel }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<button
className="btn-icon-sm"
<BarToggle
icon={source === 'layer' ? 'link' : 'link_off'}
active={source === 'layer'}
onClick={() => setLwSource(source === 'object' ? 'layer' : 'object', source === 'object' ? null : effective)}
title={source === 'object' ? 'Nach Ebene' : 'Übersteuern'}
style={{ color: source === 'layer' ? 'var(--text-primary)' : 'var(--text-muted)' }}
>
<Icon name={source === 'layer' ? 'link' : 'link_off'} size={14} />
</button>
/>
<LwPreview lw={effective} />
<NumInput
value={effective}
@@ -230,8 +214,8 @@ function PenLw({ sel }) {
width={52}
/>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<button className="btn-step" onClick={() => step(+1)}><Icon name="arrow_drop_up" size={12}/></button>
<button className="btn-step" onClick={() => step(-1)}><Icon name="arrow_drop_down" size={12}/></button>
<button className="btn-step" onClick={() => step(+1)}><Icon name="arrow_drop_up" size={11}/></button>
<button className="btn-step" onClick={() => step(-1)}><Icon name="arrow_drop_down" size={11}/></button>
</div>
</div>
)
@@ -259,31 +243,18 @@ function PenLinetype({ sel }) {
}
return (
<>
<div style={{ position: 'relative', display: 'flex' }}>
<Icon
name={source === 'layer' ? 'link' : 'link_off'}
size={14}
style={{
position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
color: source === 'layer' ? 'var(--text-primary)' : 'var(--text-muted)',
pointerEvents: 'none',
}}
/>
<select
<div style={{ display: 'flex' }}>
<BarCombo
icon={source === 'layer' ? 'link' : 'link_off'}
iconActive={source === 'layer'}
value={ltCurrent}
onChange={(ev) => ltChange(ev.target.value)}
style={{
paddingLeft: 30, flex: 1,
fontFamily: 'var(--font)',
fontWeight: 500,
fontSize: 11,
textTransform: 'none',
}}
onChange={ltChange}
title="Linientyp-Quelle"
>
<option value="__layer__">Nach Ebene</option>
{!hasOption && effective && <option value={effective}>{effective}</option>}
{options.map(n => <option key={n} value={n}>{n}</option>)}
</select>
</BarCombo>
</div>
{!isSolid && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
@@ -317,61 +288,34 @@ function PenBlock({ sel }) {
// ---------------------------------------------------------------------------
function FillBlock({ sel }) {
if (sel.canFill !== true) {
return (
<div style={{ padding: '0 14px 12px', fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
Keine geschlossenen 2D-Kurven in der Auswahl.
</div>
)
}
const enabled = sel.fillEnabled === true
const source = sel.fillSource || 'layer'
const color = sel.fillColor || sel.layerColor || '#cccccc'
const objectPat = enabled ? (sel.fillPattern || 'Solid') : 'None'
const scale = sel.fillScale ?? 1.0
const rotation = sel.fillRotation ?? 0.0
const patternList = sel.hatchPatterns || ['Solid']
// Special-Value im kombinierten Dropdown:
// "__layer__" = Nach Ebene -> Pattern/Scale/Rot/Color aus Ebenen-Einstellungen
// "None" = keine Fuellung
// "Solid" = volle Flaeche (Eigene Quelle)
// sonst = Hatch-Pattern (Eigene Quelle)
// source=='layer' -> immer "Nach Ebene" zeigen, auch wenn (noch) keine Hatch
// existiert (Ebene hat halt aktuell kein Pattern -> wird gefuellt, sobald
// eines definiert wird).
// Gemeinsamer Hatch-Editor — wiederverwendet fuer Fill (2D-Curves) und
// Section Style (3D-Solids). Beide haben die gleichen Einstellungs-
// moeglichkeiten: Pattern/Color/Scale/Rotation + "Nach Ebene"-Switch.
// `setter` bekommt (enabled, source, color, pattern, scale, rotation).
function HatchEditor({ sel, enabled, source, color, pattern, scale, rotation,
layerHint, setter, patternList }) {
const objectPat = enabled ? (pattern || 'Solid') : 'None'
const currentValue = source === 'layer'
? '__layer__'
: (!enabled ? 'None' : objectPat)
const dropdownOptions = [
{ value: '__layer__', label: 'Nach Ebene' },
{ value: 'None', label: 'None' },
{ value: 'Solid', label: 'Solid' },
...patternList
...(patternList || [])
.filter(p => p !== 'Solid' && p !== 'None')
.map(p => ({ value: p, label: p })),
]
// Falls aktueller Pattern-Name nicht in der Liste, anhaengen damit's nicht verloren geht
if (currentValue !== '__layer__' && currentValue !== 'None'
&& !dropdownOptions.some(o => o.value === currentValue)) {
dropdownOptions.push({ value: currentValue, label: currentValue })
}
const applyPattern = (newValue) => {
if (newValue === '__layer__') {
// Source -> layer, Python liest Pattern/Scale/Rot/Color aus Layer.SectionStyle
setFill(true, 'layer', null, null, null, null)
} else if (newValue === 'None') {
setFill(false, source, null, null, scale, rotation)
} else {
// Eigene Quelle mit gewaehltem Pattern
setFill(true, 'object', color, newValue, scale, rotation)
}
if (newValue === '__layer__') setter(true, 'layer', null, null, null, null)
else if (newValue === 'None') setter(false, source, null, null, scale, rotation)
else setter(true, 'object', color, newValue, scale, rotation)
}
// Anpassungen wenn schon im "Eigene"-Modus (Scale/Rotation/Color/Source-Toggle)
const apply = (over) => setFill(
const apply = (over) => setter(
true,
over.source ?? (source === 'layer' ? 'object' : source),
(over.source ?? source) === 'layer' ? null : (over.color ?? color),
@@ -383,33 +327,18 @@ function FillBlock({ sel }) {
return (
<div style={{ padding: '0 14px 12px', display: 'flex', flexDirection: 'column', gap: 8 }}>
{/* Pill mit Link-Icon im Dropdown (analog zur Pen-Source-Pill).
Link-Icon wenn "Nach Ebene", sonst link_off. */}
<div style={{ position: 'relative', display: 'flex' }}>
<Icon
name={isLayerSource ? 'link' : 'link_off'}
size={14}
style={{
position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
color: isLayerSource ? 'var(--text-primary)' : 'var(--text-muted)',
pointerEvents: 'none',
}}
/>
<select
<div style={{ display: 'flex' }}>
<BarCombo
icon={isLayerSource ? 'link' : 'link_off'}
iconActive={isLayerSource}
value={currentValue}
onChange={(ev) => applyPattern(ev.target.value)}
style={{
paddingLeft: 30, flex: 1,
fontFamily: 'var(--font)',
fontWeight: 500,
fontSize: 11,
textTransform: 'none',
}}
onChange={applyPattern}
title="Pattern-Quelle"
>
{dropdownOptions.map(o => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</BarCombo>
</div>
{currentValue !== 'None' && (
<>
@@ -436,7 +365,7 @@ function FillBlock({ sel }) {
)}
{isLayerSource && (
<div style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
Pattern, Skalierung &amp; Farbe folgen Layer-Section-Style
{layerHint}
</div>
)}
</>
@@ -445,6 +374,46 @@ function FillBlock({ sel }) {
)
}
function FillBlock({ sel }) {
if (sel.canFill !== true) {
return (
<div style={{ padding: '0 14px 12px', fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
Keine geschlossenen 2D-Kurven in der Auswahl.
</div>
)
}
const color = sel.fillColor || sel.layerColor || '#cccccc'
return <HatchEditor
sel={sel}
enabled={sel.fillEnabled === true}
source={sel.fillSource || 'layer'}
color={color}
pattern={sel.fillPattern || 'Solid'}
scale={sel.fillScale ?? 1.0}
rotation={sel.fillRotation ?? 0.0}
patternList={sel.hatchPatterns || ['Solid']}
layerHint="Pattern, Skalierung & Farbe folgen Layer-Section-Style"
setter={setFill}
/>
}
function SectionBlock({ sel }) {
const color = sel.sectionColor || sel.layerColor || '#cccccc'
return <HatchEditor
sel={sel}
enabled={sel.sectionEnabled === true}
source={sel.sectionSource || 'layer'}
color={color}
pattern={sel.sectionPattern || 'Solid'}
scale={sel.sectionScale ?? 1.0}
rotation={sel.sectionRotation ?? 0.0}
patternList={sel.hatchPatterns || ['Solid']}
layerHint="Pattern, Skalierung & Farbe folgen Layer-SectionStyle"
setter={setSectionStyle}
/>
}
function EmptyState() {
return (
<div style={{
@@ -472,6 +441,15 @@ export default function GestaltungApp() {
}, [])
const empty = sel.count === 0
// geometryKind kommt vom Backend — entscheidet welche Sections passend sind:
// curve → Fill + Pen (2D-Workflow: Hatch + Lineweight/Color)
// 3d → Section Style + Boundary (Solid: Schnitt-Hatch + Silhouetten)
// curveOpen → Pen (offene Kurve hat keine Fill)
// mixed → Pen only (gemischte Selektion: nur die gemeinsame Untermenge)
const kind = sel.geometryKind || 'curve'
const showFill = kind === 'curve'
const showSection = kind === '3d'
const penLabel = (kind === '3d') ? 'Boundary' : 'Pen'
return (
<div style={{
@@ -480,30 +458,26 @@ export default function GestaltungApp() {
background: 'var(--bg-base)',
position: 'relative',
}}>
<div style={{
display: 'flex', alignItems: 'center', gap: 8,
padding: '10px 14px',
borderBottom: '1px solid var(--border-light)',
}}>
<Icon name="tune" size={16} style={{ color: 'var(--text-muted)' }} />
<span style={{ flex: 1, fontWeight: 500, fontSize: 12, color: 'var(--text-primary)' }}>
Attribute
</span>
{sel.count > 0 && <span className="chip">{sel.count}</span>}
<button className="btn-icon-sm" onClick={() => requestSelection()} title="Aktualisieren">
<Icon name="refresh" size={14} />
</button>
</div>
<div style={{ flex: 1, overflowY: 'auto' }}>
{empty ? <EmptyState /> : (
<>
<SectionHead title="Fill" />
<FillBlock sel={sel} />
{showFill && (
<>
<SectionHead title="Fill" />
<FillBlock sel={sel} />
</>
)}
<SectionHead title="Pen" />
<SectionHead title={penLabel} />
<PenBlock sel={sel} />
{showSection && (
<>
<SectionHead title="Section Style" />
<SectionBlock sel={sel} />
</>
)}
<SectionHead title="Effects" />
<div style={{ padding: '0 14px 14px', fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
Schatten / Transparenz folgen spaeter.
+9 -9
View File
@@ -245,16 +245,16 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
onContextMenu={onContextMenu}
style={{
display: 'flex', alignItems: 'center', gap: 4,
padding: '1px 12px',
paddingLeft: 12 + (depth || 0) * 12,
padding: '1px 8px',
paddingLeft: 6 + (depth || 0) * 10,
margin: 0,
background: active ? 'var(--active-dim)'
: (e.visible !== false) ? 'var(--bg-item)'
: 'var(--bg-panel)',
// Eckige Tabellen-Zeile mit Accent-Strip links fuer aktive Ebene
borderRadius: 0,
borderLeft: '3px solid ' + (active ? 'var(--accent)' : 'transparent'),
borderBottom: '1px solid var(--border-light)',
// Aktive Zeile als Pill — kein Margin/Shift damit Inhalt nicht
// springt, nur Bg-Form aendert sich.
borderRadius: active ? 999 : 0,
borderBottom: active ? '1px solid transparent' : '1px solid var(--border-light)',
opacity: (!active && e.visible === false && mode !== 'all') ? 0.45 : 1,
cursor: 'pointer',
userSelect: 'none',
@@ -293,7 +293,7 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
value={e.name}
onCommit={onNameChange}
autoEditTrigger={autoEditName}
fontWeight={active ? 700 : 400}
fontWeight={500}
fontSize={11}
style={{
color: active ? 'var(--active-light)'
@@ -565,8 +565,8 @@ export default function EbenenManager({
</div>
<div style={{
display: 'flex', alignItems: 'center', gap: 5,
padding: '2px 14px',
display: 'flex', alignItems: 'center', gap: 4,
padding: '2px 8px 2px 9px',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border)',
}}>
+14 -13
View File
@@ -1,5 +1,6 @@
import { useState } from 'react'
import Icon from './Icon'
import { BarCombo } from './BarControls'
function Field({ label, hint, children }) {
return (
@@ -123,19 +124,19 @@ export default function EbenenSettingsDialog({
textTransform: 'uppercase', letterSpacing: 0.5 }}>
Ebene
</span>
<select
value={pickerSelected || draft.code}
onChange={(ev) => onPickEbene && onPickEbene(ev.target.value, draft)}
style={{ flex: 1, fontSize: 11, minWidth: 0,
fontFamily: 'var(--font-mono)' }}
title="Zwischen Ebenen wechseln — aktuelle Änderungen werden mit übernommen"
>
{pickerEbenen.map(e => (
<option key={e.code} value={e.code}>
{e.code} {e.name}
</option>
))}
</select>
<div style={{ flex: 1, display: 'flex', minWidth: 0 }}>
<BarCombo
value={pickerSelected || draft.code}
onChange={(v) => onPickEbene && onPickEbene(v, draft)}
title="Zwischen Ebenen wechseln — aktuelle Änderungen werden mit übernommen"
>
{pickerEbenen.map(e => (
<option key={e.code} value={e.code}>
{e.code} {e.name}
</option>
))}
</BarCombo>
</div>
</div>
) : !embedded && (
<div style={{
+3 -20
View File
@@ -51,9 +51,8 @@ function ZeichnungsebeneRow({
padding: '1px 12px',
margin: 0,
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
borderRadius: 0,
borderLeft: '3px solid ' + (active ? 'var(--accent)' : 'transparent'),
borderBottom: '1px solid var(--border-light)',
borderRadius: active ? 999 : 0,
borderBottom: active ? '1px solid transparent' : '1px solid var(--border-light)',
cursor: 'pointer',
userSelect: 'none',
minHeight: 24,
@@ -67,7 +66,7 @@ function ZeichnungsebeneRow({
><Icon name={eyeIcon} size={12} /></button>
<span style={{
fontWeight: active ? 700 : 500,
fontWeight: 500,
fontSize: 11,
color: active ? 'var(--active-light)' : 'var(--text-label)',
flex: 1, minWidth: 0,
@@ -128,9 +127,6 @@ export default function GeschossManager({
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
const sorted = [...zeichnungsebenen].reverse()
const gesamthoehe = zeichnungsebenen
.filter(z => z.isGeschoss)
.reduce((s, z) => s + (z.hoehe ?? 0), 0)
const addQuick = () => {
// Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung,
@@ -235,19 +231,6 @@ export default function GeschossManager({
</div>
</div>
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '3px 14px',
background: 'var(--bg-section)',
borderBottom: '1px solid var(--border-light)',
}}>
<span className="label-xs">Gebäudehöhe</span>
<span style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)' }}>
{gesamthoehe.toFixed(2)} m
</span>
</div>
{/* Master-Row: Master-Eye links + Master-Lock rechts (analog
EbenenManager). */}
<div style={{