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:
@@ -853,6 +853,18 @@ def _selection_summary(doc):
|
|||||||
fill_scales = set()
|
fill_scales = set()
|
||||||
fill_rots = set()
|
fill_rots = set()
|
||||||
has_closed_curves = False
|
has_closed_curves = False
|
||||||
|
# Section-Style (3D)
|
||||||
|
sec_enabled = set()
|
||||||
|
sec_sources = set()
|
||||||
|
sec_colors = set()
|
||||||
|
sec_patterns = set()
|
||||||
|
sec_scales = set()
|
||||||
|
sec_rots = set()
|
||||||
|
# Geometry-Kind-Klassifikation: 'curve' (closed planar 2D), 'curveOpen'
|
||||||
|
# (offene Kurve), '3d' (Brep/Extrusion/Mesh — Volumen mit Schnittflaeche),
|
||||||
|
# 'other'. Aggregiert ueber alle Selektions-Objekte zu kind=
|
||||||
|
# 'curve' / '3d' / 'mixed' / 'other'.
|
||||||
|
geometry_kinds = set()
|
||||||
|
|
||||||
for obj in objs:
|
for obj in objs:
|
||||||
a = obj.Attributes
|
a = obj.Attributes
|
||||||
@@ -895,6 +907,72 @@ def _selection_summary(doc):
|
|||||||
nm = _safe_layer_label(doc, layer, a.LayerIndex)
|
nm = _safe_layer_label(doc, layer, a.LayerIndex)
|
||||||
layer_names.add(nm)
|
layer_names.add(nm)
|
||||||
|
|
||||||
|
# Geometry-Klassifikation. DOSSIER-Source-Curves (wand_axis,
|
||||||
|
# decke_outline, ...) sind Meta-Geometrie und keine User-facing
|
||||||
|
# Form — fuer den Section/Fill-Entscheid ignorieren. Dann wird
|
||||||
|
# eine Wand-Selektion (Achse + Volume) als reines 3D klassifiziert.
|
||||||
|
g = obj.Geometry
|
||||||
|
is_3d = isinstance(g, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD))
|
||||||
|
dossier_type = ""
|
||||||
|
try: dossier_type = a.GetUserString("dossier_element_type") or ""
|
||||||
|
except Exception: pass
|
||||||
|
is_dossier_source = dossier_type.endswith(("_axis", "_outline", "_point"))
|
||||||
|
if isinstance(g, rg.Curve) and not is_dossier_source:
|
||||||
|
geometry_kinds.add('curve' if (g.IsClosed and g.IsPlanar()) else 'curveOpen')
|
||||||
|
elif is_3d:
|
||||||
|
geometry_kinds.add('3d')
|
||||||
|
elif isinstance(g, rg.Curve) and is_dossier_source:
|
||||||
|
pass # ignorieren — Volume zaehlt fuer die Klassifikation
|
||||||
|
else:
|
||||||
|
geometry_kinds.add('other')
|
||||||
|
|
||||||
|
# Section-Style aus Object-Attributes lesen (Rhino 8, mit Fallbacks
|
||||||
|
# fuer Property-Namen die je nach Build variieren).
|
||||||
|
if is_3d:
|
||||||
|
src_attr = None
|
||||||
|
try:
|
||||||
|
src_attr = getattr(a, "SectionAttributesSource", None)
|
||||||
|
except Exception: src_attr = None
|
||||||
|
if src_attr is not None:
|
||||||
|
try:
|
||||||
|
src_name = str(src_attr).lower()
|
||||||
|
if "layer" in src_name: sec_sources.add("layer")
|
||||||
|
elif "object" in src_name: sec_sources.add("object")
|
||||||
|
except Exception: pass
|
||||||
|
# Hatch-Index/Scale/Rotation
|
||||||
|
hidx = None
|
||||||
|
for n in ("SectionHatchIndex", "HatchPatternIndex"):
|
||||||
|
if hasattr(a, n):
|
||||||
|
try:
|
||||||
|
v = getattr(a, n)
|
||||||
|
if v is not None: hidx = int(v); break
|
||||||
|
except Exception: pass
|
||||||
|
if hidx is not None and hidx >= 0 and hidx < doc.HatchPatterns.Count:
|
||||||
|
sec_enabled.add(True)
|
||||||
|
try: sec_patterns.add(doc.HatchPatterns[hidx].Name)
|
||||||
|
except Exception: pass
|
||||||
|
elif hidx == -1:
|
||||||
|
sec_enabled.add(False)
|
||||||
|
for n, target in (
|
||||||
|
(("SectionHatchScale", "HatchPatternScale"), sec_scales),
|
||||||
|
(("SectionHatchRotation", "HatchPatternRotation"), sec_rots),
|
||||||
|
):
|
||||||
|
for nn in n:
|
||||||
|
if hasattr(a, nn):
|
||||||
|
try:
|
||||||
|
v = float(getattr(a, nn))
|
||||||
|
target.add(round(v, 4)
|
||||||
|
if target is sec_scales
|
||||||
|
else round(math.degrees(v), 2))
|
||||||
|
break
|
||||||
|
except Exception: pass
|
||||||
|
for n in ("SectionFillColor", "SectionHatchColor", "HatchColor"):
|
||||||
|
if hasattr(a, n):
|
||||||
|
try:
|
||||||
|
c = _color_to_hex(getattr(a, n))
|
||||||
|
if c: sec_colors.add(c); break
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
# Fuellung
|
# Fuellung
|
||||||
if _is_closed_planar_curve(obj.Geometry):
|
if _is_closed_planar_curve(obj.Geometry):
|
||||||
has_closed_curves = True
|
has_closed_curves = True
|
||||||
@@ -987,6 +1065,18 @@ def _selection_summary(doc):
|
|||||||
"layerLinetype": single(layer_lts),
|
"layerLinetype": single(layer_lts),
|
||||||
"layerName": single(layer_names),
|
"layerName": single(layer_names),
|
||||||
"canFill": has_closed_curves,
|
"canFill": has_closed_curves,
|
||||||
|
# Section-Style (3D)
|
||||||
|
"sectionEnabled": single(sec_enabled),
|
||||||
|
"sectionSource": single(sec_sources),
|
||||||
|
"sectionColor": single(sec_colors),
|
||||||
|
"sectionPattern": single(sec_patterns),
|
||||||
|
"sectionScale": single(sec_scales),
|
||||||
|
"sectionRotation": single(sec_rots),
|
||||||
|
# geometryKind: 'curve' | 'curveOpen' | '3d' | 'mixed' | 'other'
|
||||||
|
"geometryKind": (
|
||||||
|
'mixed' if len(geometry_kinds & {'curve', 'curveOpen', '3d'}) > 1
|
||||||
|
else (next(iter(geometry_kinds)) if len(geometry_kinds) == 1 else 'other')
|
||||||
|
),
|
||||||
"fillEnabled": single(fill_enabled),
|
"fillEnabled": single(fill_enabled),
|
||||||
"fillColor": single(fill_colors),
|
"fillColor": single(fill_colors),
|
||||||
"fillSource": single(fill_sources),
|
"fillSource": single(fill_sources),
|
||||||
@@ -1061,6 +1151,15 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
p.get("scale"),
|
p.get("scale"),
|
||||||
p.get("rotation"),
|
p.get("rotation"),
|
||||||
)
|
)
|
||||||
|
elif t == "SET_SECTION_STYLE":
|
||||||
|
self._set_section_style(
|
||||||
|
bool(p.get("enabled")),
|
||||||
|
p.get("source", "object"),
|
||||||
|
p.get("color"),
|
||||||
|
p.get("pattern"),
|
||||||
|
p.get("scale"),
|
||||||
|
p.get("rotation"),
|
||||||
|
)
|
||||||
|
|
||||||
def _send_selection(self):
|
def _send_selection(self):
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
@@ -1376,6 +1475,79 @@ class GestaltungBridge(panel_base.BaseBridge):
|
|||||||
doc.Views.Redraw()
|
doc.Views.Redraw()
|
||||||
self._send_selection()
|
self._send_selection()
|
||||||
|
|
||||||
|
# ---- SectionStyle (per-Object, Rhino 8) -------------------------------
|
||||||
|
|
||||||
|
def _set_section_style(self, enabled, source, color_hex,
|
||||||
|
pattern_name=None, scale=None, rotation_deg=None):
|
||||||
|
"""Setzt Per-Object SectionStyle-Properties auf die selektierten
|
||||||
|
3D-Objekte. Rhino 8 expone diese Properties auf ObjectAttributes
|
||||||
|
unter teils variierenden Namen — wir versuchen die bekannten."""
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
|
is_layer_source = (source == "layer")
|
||||||
|
|
||||||
|
# Hatch-Pattern-Index ermitteln
|
||||||
|
pat_idx = -1
|
||||||
|
if pattern_name and pattern_name not in ("None", ""):
|
||||||
|
try: pat_idx = doc.HatchPatterns.Find(pattern_name, True)
|
||||||
|
except Exception: pat_idx = -1
|
||||||
|
if pat_idx < 0 and pattern_name not in ("None", ""):
|
||||||
|
try: pat_idx = doc.HatchPatterns.Find("Solid", True)
|
||||||
|
except Exception: pat_idx = -1
|
||||||
|
|
||||||
|
col = _hex_to_color(color_hex) if color_hex else None
|
||||||
|
scale_v = float(scale) if scale is not None else 1.0
|
||||||
|
rot_rad = math.radians(float(rotation_deg)) if rotation_deg is not None else 0.0
|
||||||
|
|
||||||
|
def _try_set_attr(a, names, value):
|
||||||
|
for n in names:
|
||||||
|
if hasattr(a, n):
|
||||||
|
try:
|
||||||
|
setattr(a, n, value)
|
||||||
|
return n
|
||||||
|
except Exception: pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
n_ok = 0
|
||||||
|
for obj in objs:
|
||||||
|
geom = obj.Geometry
|
||||||
|
if not isinstance(geom, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD)):
|
||||||
|
continue
|
||||||
|
a = obj.Attributes.Duplicate()
|
||||||
|
|
||||||
|
# Source: FromLayer vs FromObject — verschiedene Enum-Namen
|
||||||
|
if is_layer_source:
|
||||||
|
# Versuche SectionAttributesSource auf FromLayer
|
||||||
|
_try_set_attr(a, ("SectionAttributesSource",),
|
||||||
|
Rhino.DocObjects.SectionAttributesSource.FromLayer
|
||||||
|
if hasattr(Rhino.DocObjects, "SectionAttributesSource") else 0)
|
||||||
|
else:
|
||||||
|
_try_set_attr(a, ("SectionAttributesSource",),
|
||||||
|
Rhino.DocObjects.SectionAttributesSource.FromObject
|
||||||
|
if hasattr(Rhino.DocObjects, "SectionAttributesSource") else 1)
|
||||||
|
|
||||||
|
if not enabled or pattern_name == "None":
|
||||||
|
# Hatch-Index auf -1 = keine Fuellung
|
||||||
|
_try_set_attr(a, ("SectionHatchIndex", "HatchPatternIndex"), -1)
|
||||||
|
else:
|
||||||
|
if pat_idx >= 0:
|
||||||
|
_try_set_attr(a, ("SectionHatchIndex", "HatchPatternIndex"), pat_idx)
|
||||||
|
_try_set_attr(a, ("SectionHatchScale", "HatchPatternScale"), scale_v)
|
||||||
|
_try_set_attr(a, ("SectionHatchRotation", "HatchPatternRotation"), rot_rad)
|
||||||
|
if col is not None:
|
||||||
|
_try_set_attr(a, ("SectionFillColor", "SectionHatchColor",
|
||||||
|
"HatchColor"), col)
|
||||||
|
|
||||||
|
try:
|
||||||
|
doc.Objects.ModifyAttributes(obj, a, True)
|
||||||
|
n_ok += 1
|
||||||
|
except Exception as ex:
|
||||||
|
print("[GESTALTUNG] SectionStyle ModifyAttributes:", ex)
|
||||||
|
|
||||||
|
print("[GESTALTUNG] SectionStyle auf {} Objekt(e) appliziert".format(n_ok))
|
||||||
|
doc.Views.Redraw()
|
||||||
|
self._send_selection()
|
||||||
|
|
||||||
|
|
||||||
# --- Selection-Events ----------------------------------------------------
|
# --- Selection-Events ----------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
+109
-135
@@ -1,8 +1,10 @@
|
|||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import Icon from './components/Icon'
|
import Icon from './components/Icon'
|
||||||
|
import { BarCombo, BarToggle, BarButton } from './components/BarControls'
|
||||||
import {
|
import {
|
||||||
onMessage, notifyReady,
|
onMessage, notifyReady,
|
||||||
requestSelection, setColorSource, setLwSource, setLinetypeSource, setLinetypeScale, setFill,
|
setColorSource, setLwSource, setLinetypeSource, setLinetypeScale, setFill,
|
||||||
|
setSectionStyle,
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
const LW_PRESETS = [0.02, 0.10, 0.13, 0.18, 0.25, 0.35, 0.50, 0.70, 1.00]
|
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' }) {
|
function SourceSelect({ source, onChange, overrideLabel = 'Eigene' }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative', display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<Icon
|
<BarCombo
|
||||||
name={source === 'layer' ? 'link' : 'link_off'}
|
icon={source === 'layer' ? 'link' : 'link_off'}
|
||||||
size={14}
|
iconActive={source === 'layer'}
|
||||||
style={{
|
|
||||||
position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
|
|
||||||
color: source === 'layer' ? 'var(--text-primary)' : 'var(--text-muted)',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
value={source}
|
value={source}
|
||||||
onChange={(ev) => onChange(ev.target.value)}
|
onChange={onChange}
|
||||||
style={{
|
title="Farb-Quelle"
|
||||||
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. */
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<option value="layer">Nach Ebene</option>
|
<option value="layer">Nach Ebene</option>
|
||||||
<option value="object">{overrideLabel}</option>
|
<option value="object">{overrideLabel}</option>
|
||||||
</select>
|
</BarCombo>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -62,7 +48,7 @@ function ColorBar({ color, onChange, height = 22 }) {
|
|||||||
style={{
|
style={{
|
||||||
width: '100%', height,
|
width: '100%', height,
|
||||||
background: color,
|
background: color,
|
||||||
borderRadius: 'var(--r)',
|
borderRadius: 999,
|
||||||
border: '1px solid var(--border)',
|
border: '1px solid var(--border)',
|
||||||
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.08)',
|
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.08)',
|
||||||
}}
|
}}
|
||||||
@@ -99,7 +85,7 @@ function ColorBar({ color, onChange, height = 22 }) {
|
|||||||
style={{
|
style={{
|
||||||
position: 'relative', display: 'block',
|
position: 'relative', display: 'block',
|
||||||
width: '100%', height,
|
width: '100%', height,
|
||||||
borderRadius: 'var(--r)',
|
borderRadius: 999,
|
||||||
border: '1px solid var(--border)',
|
border: '1px solid var(--border)',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@@ -214,14 +200,12 @@ function PenLw({ sel }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<button
|
<BarToggle
|
||||||
className="btn-icon-sm"
|
icon={source === 'layer' ? 'link' : 'link_off'}
|
||||||
|
active={source === 'layer'}
|
||||||
onClick={() => setLwSource(source === 'object' ? 'layer' : 'object', source === 'object' ? null : effective)}
|
onClick={() => setLwSource(source === 'object' ? 'layer' : 'object', source === 'object' ? null : effective)}
|
||||||
title={source === 'object' ? 'Nach Ebene' : 'Übersteuern'}
|
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} />
|
<LwPreview lw={effective} />
|
||||||
<NumInput
|
<NumInput
|
||||||
value={effective}
|
value={effective}
|
||||||
@@ -230,8 +214,8 @@ function PenLw({ sel }) {
|
|||||||
width={52}
|
width={52}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
<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_up" size={11}/></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_down" size={11}/></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -259,31 +243,18 @@ function PenLinetype({ sel }) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ position: 'relative', display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<Icon
|
<BarCombo
|
||||||
name={source === 'layer' ? 'link' : 'link_off'}
|
icon={source === 'layer' ? 'link' : 'link_off'}
|
||||||
size={14}
|
iconActive={source === 'layer'}
|
||||||
style={{
|
|
||||||
position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)',
|
|
||||||
color: source === 'layer' ? 'var(--text-primary)' : 'var(--text-muted)',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
value={ltCurrent}
|
value={ltCurrent}
|
||||||
onChange={(ev) => ltChange(ev.target.value)}
|
onChange={ltChange}
|
||||||
style={{
|
title="Linientyp-Quelle"
|
||||||
paddingLeft: 30, flex: 1,
|
|
||||||
fontFamily: 'var(--font)',
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: 11,
|
|
||||||
textTransform: 'none',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<option value="__layer__">Nach Ebene</option>
|
<option value="__layer__">Nach Ebene</option>
|
||||||
{!hasOption && effective && <option value={effective}>{effective}</option>}
|
{!hasOption && effective && <option value={effective}>{effective}</option>}
|
||||||
{options.map(n => <option key={n} value={n}>{n}</option>)}
|
{options.map(n => <option key={n} value={n}>{n}</option>)}
|
||||||
</select>
|
</BarCombo>
|
||||||
</div>
|
</div>
|
||||||
{!isSolid && (
|
{!isSolid && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
@@ -317,61 +288,34 @@ function PenBlock({ sel }) {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function FillBlock({ sel }) {
|
// Gemeinsamer Hatch-Editor — wiederverwendet fuer Fill (2D-Curves) und
|
||||||
if (sel.canFill !== true) {
|
// Section Style (3D-Solids). Beide haben die gleichen Einstellungs-
|
||||||
return (
|
// moeglichkeiten: Pattern/Color/Scale/Rotation + "Nach Ebene"-Switch.
|
||||||
<div style={{ padding: '0 14px 12px', fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
// `setter` bekommt (enabled, source, color, pattern, scale, rotation).
|
||||||
Keine geschlossenen 2D-Kurven in der Auswahl.
|
function HatchEditor({ sel, enabled, source, color, pattern, scale, rotation,
|
||||||
</div>
|
layerHint, setter, patternList }) {
|
||||||
)
|
const objectPat = enabled ? (pattern || 'Solid') : 'None'
|
||||||
}
|
|
||||||
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).
|
|
||||||
const currentValue = source === 'layer'
|
const currentValue = source === 'layer'
|
||||||
? '__layer__'
|
? '__layer__'
|
||||||
: (!enabled ? 'None' : objectPat)
|
: (!enabled ? 'None' : objectPat)
|
||||||
|
|
||||||
const dropdownOptions = [
|
const dropdownOptions = [
|
||||||
{ value: '__layer__', label: 'Nach Ebene' },
|
{ value: '__layer__', label: 'Nach Ebene' },
|
||||||
{ value: 'None', label: 'None' },
|
{ value: 'None', label: 'None' },
|
||||||
{ value: 'Solid', label: 'Solid' },
|
{ value: 'Solid', label: 'Solid' },
|
||||||
...patternList
|
...(patternList || [])
|
||||||
.filter(p => p !== 'Solid' && p !== 'None')
|
.filter(p => p !== 'Solid' && p !== 'None')
|
||||||
.map(p => ({ value: p, label: p })),
|
.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'
|
if (currentValue !== '__layer__' && currentValue !== 'None'
|
||||||
&& !dropdownOptions.some(o => o.value === currentValue)) {
|
&& !dropdownOptions.some(o => o.value === currentValue)) {
|
||||||
dropdownOptions.push({ value: currentValue, label: currentValue })
|
dropdownOptions.push({ value: currentValue, label: currentValue })
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyPattern = (newValue) => {
|
const applyPattern = (newValue) => {
|
||||||
if (newValue === '__layer__') {
|
if (newValue === '__layer__') setter(true, 'layer', null, null, null, null)
|
||||||
// Source -> layer, Python liest Pattern/Scale/Rot/Color aus Layer.SectionStyle
|
else if (newValue === 'None') setter(false, source, null, null, scale, rotation)
|
||||||
setFill(true, 'layer', null, null, null, null)
|
else setter(true, 'object', color, newValue, scale, rotation)
|
||||||
} else if (newValue === 'None') {
|
|
||||||
setFill(false, source, null, null, scale, rotation)
|
|
||||||
} else {
|
|
||||||
// Eigene Quelle mit gewaehltem Pattern
|
|
||||||
setFill(true, 'object', color, newValue, scale, rotation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Anpassungen wenn schon im "Eigene"-Modus (Scale/Rotation/Color/Source-Toggle)
|
const apply = (over) => setter(
|
||||||
const apply = (over) => setFill(
|
|
||||||
true,
|
true,
|
||||||
over.source ?? (source === 'layer' ? 'object' : source),
|
over.source ?? (source === 'layer' ? 'object' : source),
|
||||||
(over.source ?? source) === 'layer' ? null : (over.color ?? color),
|
(over.source ?? source) === 'layer' ? null : (over.color ?? color),
|
||||||
@@ -383,33 +327,18 @@ function FillBlock({ sel }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '0 14px 12px', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
<div style={{ padding: '0 14px 12px', display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
{/* Pill mit Link-Icon im Dropdown (analog zur Pen-Source-Pill).
|
<div style={{ display: 'flex' }}>
|
||||||
Link-Icon wenn "Nach Ebene", sonst link_off. */}
|
<BarCombo
|
||||||
<div style={{ position: 'relative', display: 'flex' }}>
|
icon={isLayerSource ? 'link' : 'link_off'}
|
||||||
<Icon
|
iconActive={isLayerSource}
|
||||||
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
|
|
||||||
value={currentValue}
|
value={currentValue}
|
||||||
onChange={(ev) => applyPattern(ev.target.value)}
|
onChange={applyPattern}
|
||||||
style={{
|
title="Pattern-Quelle"
|
||||||
paddingLeft: 30, flex: 1,
|
|
||||||
fontFamily: 'var(--font)',
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: 11,
|
|
||||||
textTransform: 'none',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{dropdownOptions.map(o => (
|
{dropdownOptions.map(o => (
|
||||||
<option key={o.value} value={o.value}>{o.label}</option>
|
<option key={o.value} value={o.value}>{o.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</BarCombo>
|
||||||
</div>
|
</div>
|
||||||
{currentValue !== 'None' && (
|
{currentValue !== 'None' && (
|
||||||
<>
|
<>
|
||||||
@@ -436,7 +365,7 @@ function FillBlock({ sel }) {
|
|||||||
)}
|
)}
|
||||||
{isLayerSource && (
|
{isLayerSource && (
|
||||||
<div style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
<div style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
||||||
Pattern, Skalierung & Farbe folgen Layer-Section-Style
|
{layerHint}
|
||||||
</div>
|
</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() {
|
function EmptyState() {
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -472,6 +441,15 @@ export default function GestaltungApp() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const empty = sel.count === 0
|
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 (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -480,30 +458,26 @@ export default function GestaltungApp() {
|
|||||||
background: 'var(--bg-base)',
|
background: 'var(--bg-base)',
|
||||||
position: 'relative',
|
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' }}>
|
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||||
{empty ? <EmptyState /> : (
|
{empty ? <EmptyState /> : (
|
||||||
<>
|
<>
|
||||||
<SectionHead title="Fill" />
|
{showFill && (
|
||||||
<FillBlock sel={sel} />
|
<>
|
||||||
|
<SectionHead title="Fill" />
|
||||||
|
<FillBlock sel={sel} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<SectionHead title="Pen" />
|
<SectionHead title={penLabel} />
|
||||||
<PenBlock sel={sel} />
|
<PenBlock sel={sel} />
|
||||||
|
|
||||||
|
{showSection && (
|
||||||
|
<>
|
||||||
|
<SectionHead title="Section Style" />
|
||||||
|
<SectionBlock sel={sel} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<SectionHead title="Effects" />
|
<SectionHead title="Effects" />
|
||||||
<div style={{ padding: '0 14px 14px', fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
<div style={{ padding: '0 14px 14px', fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
||||||
Schatten / Transparenz folgen spaeter.
|
Schatten / Transparenz folgen spaeter.
|
||||||
|
|||||||
@@ -245,16 +245,16 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
|
|||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 4,
|
display: 'flex', alignItems: 'center', gap: 4,
|
||||||
padding: '1px 12px',
|
padding: '1px 8px',
|
||||||
paddingLeft: 12 + (depth || 0) * 12,
|
paddingLeft: 6 + (depth || 0) * 10,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
background: active ? 'var(--active-dim)'
|
background: active ? 'var(--active-dim)'
|
||||||
: (e.visible !== false) ? 'var(--bg-item)'
|
: (e.visible !== false) ? 'var(--bg-item)'
|
||||||
: 'var(--bg-panel)',
|
: 'var(--bg-panel)',
|
||||||
// Eckige Tabellen-Zeile mit Accent-Strip links fuer aktive Ebene
|
// Aktive Zeile als Pill — kein Margin/Shift damit Inhalt nicht
|
||||||
borderRadius: 0,
|
// springt, nur Bg-Form aendert sich.
|
||||||
borderLeft: '3px solid ' + (active ? 'var(--accent)' : 'transparent'),
|
borderRadius: active ? 999 : 0,
|
||||||
borderBottom: '1px solid var(--border-light)',
|
borderBottom: active ? '1px solid transparent' : '1px solid var(--border-light)',
|
||||||
opacity: (!active && e.visible === false && mode !== 'all') ? 0.45 : 1,
|
opacity: (!active && e.visible === false && mode !== 'all') ? 0.45 : 1,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
@@ -293,7 +293,7 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
|
|||||||
value={e.name}
|
value={e.name}
|
||||||
onCommit={onNameChange}
|
onCommit={onNameChange}
|
||||||
autoEditTrigger={autoEditName}
|
autoEditTrigger={autoEditName}
|
||||||
fontWeight={active ? 700 : 400}
|
fontWeight={500}
|
||||||
fontSize={11}
|
fontSize={11}
|
||||||
style={{
|
style={{
|
||||||
color: active ? 'var(--active-light)'
|
color: active ? 'var(--active-light)'
|
||||||
@@ -565,8 +565,8 @@ export default function EbenenManager({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', alignItems: 'center', gap: 5,
|
display: 'flex', alignItems: 'center', gap: 4,
|
||||||
padding: '2px 14px',
|
padding: '2px 8px 2px 9px',
|
||||||
background: 'var(--bg-section)',
|
background: 'var(--bg-section)',
|
||||||
borderBottom: '1px solid var(--border)',
|
borderBottom: '1px solid var(--border)',
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Icon from './Icon'
|
import Icon from './Icon'
|
||||||
|
import { BarCombo } from './BarControls'
|
||||||
|
|
||||||
function Field({ label, hint, children }) {
|
function Field({ label, hint, children }) {
|
||||||
return (
|
return (
|
||||||
@@ -123,19 +124,19 @@ export default function EbenenSettingsDialog({
|
|||||||
textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||||
Ebene
|
Ebene
|
||||||
</span>
|
</span>
|
||||||
<select
|
<div style={{ flex: 1, display: 'flex', minWidth: 0 }}>
|
||||||
value={pickerSelected || draft.code}
|
<BarCombo
|
||||||
onChange={(ev) => onPickEbene && onPickEbene(ev.target.value, draft)}
|
value={pickerSelected || draft.code}
|
||||||
style={{ flex: 1, fontSize: 11, minWidth: 0,
|
onChange={(v) => onPickEbene && onPickEbene(v, draft)}
|
||||||
fontFamily: 'var(--font-mono)' }}
|
title="Zwischen Ebenen wechseln — aktuelle Änderungen werden mit übernommen"
|
||||||
title="Zwischen Ebenen wechseln — aktuelle Änderungen werden mit übernommen"
|
>
|
||||||
>
|
{pickerEbenen.map(e => (
|
||||||
{pickerEbenen.map(e => (
|
<option key={e.code} value={e.code}>
|
||||||
<option key={e.code} value={e.code}>
|
{e.code} — {e.name}
|
||||||
{e.code} — {e.name}
|
</option>
|
||||||
</option>
|
))}
|
||||||
))}
|
</BarCombo>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : !embedded && (
|
) : !embedded && (
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
@@ -51,9 +51,8 @@ function ZeichnungsebeneRow({
|
|||||||
padding: '1px 12px',
|
padding: '1px 12px',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
|
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
|
||||||
borderRadius: 0,
|
borderRadius: active ? 999 : 0,
|
||||||
borderLeft: '3px solid ' + (active ? 'var(--accent)' : 'transparent'),
|
borderBottom: active ? '1px solid transparent' : '1px solid var(--border-light)',
|
||||||
borderBottom: '1px solid var(--border-light)',
|
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
minHeight: 24,
|
minHeight: 24,
|
||||||
@@ -67,7 +66,7 @@ function ZeichnungsebeneRow({
|
|||||||
><Icon name={eyeIcon} size={12} /></button>
|
><Icon name={eyeIcon} size={12} /></button>
|
||||||
|
|
||||||
<span style={{
|
<span style={{
|
||||||
fontWeight: active ? 700 : 500,
|
fontWeight: 500,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: active ? 'var(--active-light)' : 'var(--text-label)',
|
color: active ? 'var(--active-light)' : 'var(--text-label)',
|
||||||
flex: 1, minWidth: 0,
|
flex: 1, minWidth: 0,
|
||||||
@@ -128,9 +127,6 @@ export default function GeschossManager({
|
|||||||
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
|
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
|
||||||
|
|
||||||
const sorted = [...zeichnungsebenen].reverse()
|
const sorted = [...zeichnungsebenen].reverse()
|
||||||
const gesamthoehe = zeichnungsebenen
|
|
||||||
.filter(z => z.isGeschoss)
|
|
||||||
.reduce((s, z) => s + (z.hoehe ?? 0), 0)
|
|
||||||
|
|
||||||
const addQuick = () => {
|
const addQuick = () => {
|
||||||
// Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung,
|
// Standard: NICHT-Geschoss-Zeichnungsebene (z.B. Möblierung, Bemassung,
|
||||||
@@ -235,19 +231,6 @@ export default function GeschossManager({
|
|||||||
</div>
|
</div>
|
||||||
</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
|
{/* Master-Row: Master-Eye links + Master-Lock rechts (analog
|
||||||
EbenenManager). */}
|
EbenenManager). */}
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
Reference in New Issue
Block a user