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 ( -
- - - - Pille (index.css) damit's optisch konsistent mit dem Fill-Dropdown - wirkt. Nur paddingLeft ueberschreiben wegen Link-Icon. */ - }} + onChange={onChange} + title="Farb-Quelle" > - +
) } @@ -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 ( <> -
- - +
{!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. */} -
- - +
{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(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). */}