diff --git a/rhino/gestaltung.py b/rhino/gestaltung.py
index fc7ab61..550e43d 100644
--- a/rhino/gestaltung.py
+++ b/rhino/gestaltung.py
@@ -853,6 +853,18 @@ def _selection_summary(doc):
fill_scales = set()
fill_rots = set()
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:
a = obj.Attributes
@@ -895,6 +907,72 @@ def _selection_summary(doc):
nm = _safe_layer_label(doc, layer, a.LayerIndex)
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
if _is_closed_planar_curve(obj.Geometry):
has_closed_curves = True
@@ -987,6 +1065,18 @@ def _selection_summary(doc):
"layerLinetype": single(layer_lts),
"layerName": single(layer_names),
"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),
"fillColor": single(fill_colors),
"fillSource": single(fill_sources),
@@ -1061,6 +1151,15 @@ class GestaltungBridge(panel_base.BaseBridge):
p.get("scale"),
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):
doc = Rhino.RhinoDoc.ActiveDoc
@@ -1376,6 +1475,79 @@ class GestaltungBridge(panel_base.BaseBridge):
doc.Views.Redraw()
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 ----------------------------------------------------
diff --git a/src/GestaltungApp.jsx b/src/GestaltungApp.jsx
index 0a23faa..29ace7d 100644
--- a/src/GestaltungApp.jsx
+++ b/src/GestaltungApp.jsx
@@ -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 (
-
-
-
)
}
@@ -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 (
-
+ />
-
-
+
+
)
@@ -259,31 +243,18 @@ function PenLinetype({ sel }) {
}
return (
<>
-
-
-
+ ltChange(ev.target.value)}
- style={{
- paddingLeft: 30, flex: 1,
- fontFamily: 'var(--font)',
- fontWeight: 500,
- fontSize: 11,
- textTransform: 'none',
- }}
+ onChange={ltChange}
+ title="Linientyp-Quelle"
>
{!hasOption && effective && }
{options.map(n => )}
-
+
{!isSolid && (
@@ -317,61 +288,34 @@ function PenBlock({ sel }) {
// ---------------------------------------------------------------------------
-function FillBlock({ sel }) {
- if (sel.canFill !== true) {
- return (
-
- Keine geschlossenen 2D-Kurven in der Auswahl.
-
- )
- }
- 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 (
- {/* Pill mit Link-Icon im Dropdown (analog zur Pen-Source-Pill).
- Link-Icon wenn "Nach Ebene", sonst link_off. */}
-
-
-
+ 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 => (
))}
-
+
{currentValue !== 'None' && (
<>
@@ -436,7 +365,7 @@ function FillBlock({ sel }) {
)}
{isLayerSource && (
- Pattern, Skalierung & Farbe folgen Layer-Section-Style
+ {layerHint}
)}
>
@@ -445,6 +374,46 @@ function FillBlock({ sel }) {
)
}
+function FillBlock({ sel }) {
+ if (sel.canFill !== true) {
+ return (
+
+ Keine geschlossenen 2D-Kurven in der Auswahl.
+
+ )
+ }
+ const color = sel.fillColor || sel.layerColor || '#cccccc'
+ return
+}
+
+function SectionBlock({ sel }) {
+ const color = sel.sectionColor || sel.layerColor || '#cccccc'
+ return
+}
+
+
function EmptyState() {
return (
-
-
-
- Attribute
-
- {sel.count > 0 && {sel.count}}
-
-
-
{empty ?
: (
<>
-
-
+ {showFill && (
+ <>
+
+
+ >
+ )}
-
+
+ {showSection && (
+ <>
+
+
+ >
+ )}
+
Schatten / Transparenz folgen spaeter.
diff --git a/src/components/EbenenManager.jsx b/src/components/EbenenManager.jsx
index 2e5e0f1..837271f 100644
--- a/src/components/EbenenManager.jsx
+++ b/src/components/EbenenManager.jsx
@@ -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({
diff --git a/src/components/EbenenSettingsDialog.jsx b/src/components/EbenenSettingsDialog.jsx
index 47c06b4..7945c19 100644
--- a/src/components/EbenenSettingsDialog.jsx
+++ b/src/components/EbenenSettingsDialog.jsx
@@ -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
-
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 => (
-
- ))}
-
+
+ onPickEbene && onPickEbene(v, draft)}
+ title="Zwischen Ebenen wechseln — aktuelle Änderungen werden mit übernommen"
+ >
+ {pickerEbenen.map(e => (
+
+ ))}
+
+
) : !embedded && (
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({
-
- Gebäudehöhe
-
- {gesamthoehe.toFixed(2)} m
-
-
-
-
{/* Master-Row: Master-Eye links + Master-Lock rechts (analog
EbenenManager). */}