Styles: default pen for new objects + csharp build doc

Gestaltung panel now shows pen controls (color, lineweight, linetype)
when nothing is selected. Settings persist in sticky and are stamped
onto every newly drawn object (curves, text, hatch, dims — not 3D
solids or DOSSIER element geometry) via the existing AddRhinoObject
listener. Active state shown with badge; resets by switching all back
to "Nach Ebene".

Also adds csharp/BUILD.md with full build + post-reinstall checklist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 02:00:47 +02:00
parent e8da519d29
commit d558fac2c3
3 changed files with 243 additions and 11 deletions
+77
View File
@@ -0,0 +1,77 @@
# DOSSIER C# Plugin — Build & Install
Das Plugin (`.rhp`) bootstrappt beim Rhino-Start die Python-Module und
registriert native Commands (`dWall`, `dDoor`, `dSlab`, …).
## Voraussetzungen
```bash
brew install dotnet@7
```
Oder direkt von Microsoft: https://dotnet.microsoft.com/download/dotnet/7.0
RhinoCommon wird beim ersten Build automatisch via NuGet geladen.
## Repo-Pfad setzen (nach Neuinstallation wichtig)
Das Plugin sucht das Repo in dieser Reihenfolge:
1. Env-Var `DOSSIER_HOME`
2. Datei `~/.dossier_home` (eine Zeile: absoluter Pfad zum Repo-Root)
3. Hardcoded Fallback `/Users/karim/STUDIO/DOSSIER`
Einfachste Variante — einmalig nach dem Klonen:
```bash
echo "/Users/karim/PROJECTS/DOSSIER" > ~/.dossier_home
```
Ohne das findet das Plugin `rhino/startup.py` nicht und bootet nicht.
## Build
```bash
cd csharp/DOSSIER
./build.sh # Release → bin/Release/net7.0/DOSSIER.rhp
./build.sh debug # Debug-Build mit Symbols
./build.sh clean # bin/ + obj/ löschen
./build.sh install # Build + yak install in Rhino-User-Plugin-Pfad
```
## Installation in Rhino (einmalig nach Build)
Mac Rhino 8 unterstützt kein Drag & Drop für `.rhp`-Dateien.
1. Rhino 8 öffnen
2. Befehl: `PluginManager`
3. Button **Install…**`csharp/DOSSIER/bin/Release/net7.0/DOSSIER.rhp`
4. Rhino neu starten
Der Pfad bleibt in Rhinos Settings-XML registriert. Bei späteren Builds
einfach wieder in denselben Output-Pfad bauen — Rhino lädt den neuen Stand
automatisch beim nächsten Start.
## Startup-Eintrag (Python-Bootstrap)
Der Launcher trägt den Startup-Eintrag automatisch ein. Für manuelle
Dev-Setups ohne Launcher:
Rhino → Options → General → „Run these commands every time a model is opened":
```
_-RunPythonScript "/Users/karim/PROJECTS/DOSSIER/rhino/startup.py"
```
(Mit Dash, mit Quotes, voller Pfad — siehe CLAUDE.md für Details warum.)
## Nach Neuinstallation Mac — Checkliste
- [ ] Repo klonen: `git clone https://git.kgva.ch/karim/DOSSIER.git`
- [ ] `echo "/Users/karim/PROJECTS/DOSSIER" > ~/.dossier_home`
- [ ] `brew install dotnet@7 node`
- [ ] `cd DOSSIER && npm install && npm run build`
- [ ] `cd csharp/DOSSIER && ./build.sh install`
- [ ] Rhino öffnen → PluginManager → Install → `.rhp` registrieren
- [ ] Rhino neu starten → DOSSIER bootstrappt (Panels + Commands)
- [ ] `~/Library/Application Support/RhinoPanel/override_presets.json` von Backup zurückkopieren
+130 -1
View File
@@ -189,6 +189,94 @@ def _hex_to_color(h):
return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)) return Drawing.Color.FromArgb(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
# Default-Pen ("Stift fuer neue Objekte"): wird in on_add auf frisch
# gezeichnete Kurven gestempelt, solange aktiv. Persistiert in Sticky und
# gilt bis der User wieder auf "Nach Ebene" zurueckstellt. Wird vom selben
# UI-Block gesetzt wie der Selektions-Pen — nur ohne Auswahl (siehe Setter).
_DEFAULT_PEN_KEY = "_dossier_default_pen"
def _default_pen():
p = sc.sticky.get(_DEFAULT_PEN_KEY)
if not isinstance(p, dict):
p = {}
return {
"colorSource": p.get("colorSource", "layer"),
"color": p.get("color"),
"lwSource": p.get("lwSource", "layer"),
"lw": p.get("lw"),
"linetypeSource": p.get("linetypeSource", "layer"),
"linetype": p.get("linetype"),
}
def _set_default_pen(**changes):
p = _default_pen()
p.update(changes)
sc.sticky[_DEFAULT_PEN_KEY] = p
def _default_pen_active(pen=None):
p = pen or _default_pen()
return (p["colorSource"] == "object" or p["lwSource"] == "object"
or p["linetypeSource"] == "object")
def _has_selection(doc):
return bool(list(doc.Objects.GetSelectedObjects(False, False)))
def _apply_pen_to_attrs(doc, a, pen):
"""Stempelt den aktiven Default-Pen auf eine Attribut-Kopie (in-place)."""
if pen["colorSource"] == "object":
a.ColorSource = _FROM_OBJECT
if pen["color"]:
a.ObjectColor = _hex_to_color(pen["color"])
_sync_plot_color_to_display(a)
if pen["lwSource"] == "object" and pen["lw"] is not None:
a.PlotWeightSource = _LW_FROM_OBJECT
try:
import massstab
massstab.write_plotweight(doc, a, float(pen["lw"]))
except Exception:
a.PlotWeight = float(pen["lw"])
if pen["linetypeSource"] == "object" and pen["linetype"]:
idx = -1
try: idx = doc.Linetypes.Find(pen["linetype"], True)
except Exception: idx = -1
if idx >= 0:
a.LinetypeSource = _LT_FROM_OBJECT
a.LinetypeIndex = idx
def _new_object_pen_summary(doc, pen):
"""PenBlock-kompatibles Objekt fuer den leeren Panel-Zustand: zeigt den
aktiven Default-Pen, "Nach Ebene"-Vorschau aus der aktuellen Ebene."""
cur = doc.Layers.CurrentLayer
layer_color = _color_to_hex(cur.Color)
try:
import massstab as _ms
layer_lw = round(_ms.read_plotweight(cur), 4)
except Exception:
layer_lw = round(cur.PlotWeight, 4)
layer_lt = _linetype_name(doc, cur.LinetypeIndex)
return {
"colorSource": pen["colorSource"],
"color": pen["color"] or layer_color,
"layerColor": layer_color,
"lwSource": pen["lwSource"],
"lw": pen["lw"] if pen["lw"] is not None else layer_lw,
"layerLw": layer_lw,
"linetypeSource": pen["linetypeSource"],
"linetype": pen["linetype"] or layer_lt,
"layerLinetype": layer_lt,
"linetypes": _all_linetypes(doc),
"layerName": _safe_layer_label(doc, cur, doc.Layers.CurrentLayerIndex),
"geometryKind": "curveOpen",
"active": _default_pen_active(pen),
}
def _force_load_linetypes(doc): def _force_load_linetypes(doc):
"""Rhinos Linetype-Tabelle wird lazy initialisiert — wir triggern es.""" """Rhinos Linetype-Tabelle wird lazy initialisiert — wir triggern es."""
# 1) Eingebaute Methode (falls present) # 1) Eingebaute Methode (falls present)
@@ -840,6 +928,7 @@ def _selection_summary(doc):
objs = list(doc.Objects.GetSelectedObjects(False, False)) objs = list(doc.Objects.GetSelectedObjects(False, False))
base = {"count": 0, "linetypes": _all_linetypes(doc), "hatchPatterns": _all_hatch_patterns(doc)} base = {"count": 0, "linetypes": _all_linetypes(doc), "hatchPatterns": _all_hatch_patterns(doc)}
if not objs: if not objs:
base["newObjectPen"] = _new_object_pen_summary(doc, _default_pen())
return base return base
color_sources, colors = set(), set() color_sources, colors = set(), set()
@@ -1238,6 +1327,12 @@ class GestaltungBridge(panel_base.BaseBridge):
self._send_selection() self._send_selection()
def _set_color_source(self, source, color_hex): def _set_color_source(self, source, color_hex):
doc = Rhino.RhinoDoc.ActiveDoc
if not _has_selection(doc):
_set_default_pen(colorSource=source,
color=(color_hex if source == "object" else None))
self._send_selection()
return
col = _hex_to_color(color_hex) if (source == "object" and color_hex) else None col = _hex_to_color(color_hex) if (source == "object" and color_hex) else None
def m(a, _obj): def m(a, _obj):
if source == "layer": if source == "layer":
@@ -1255,11 +1350,16 @@ class GestaltungBridge(panel_base.BaseBridge):
# Print-Mode-aware: bei aktivem Print-View werden PlotWeights skaliert. # Print-Mode-aware: bei aktivem Print-View werden PlotWeights skaliert.
# write_plotweight() kuemmert sich um beides (Original-Speicherung + # write_plotweight() kuemmert sich um beides (Original-Speicherung +
# Skalierungs-Multiplier). # Skalierungs-Multiplier).
doc = Rhino.RhinoDoc.ActiveDoc
if not _has_selection(doc):
_set_default_pen(lwSource=source,
lw=(lw if source == "object" else None))
self._send_selection()
return
try: try:
import massstab import massstab
except Exception: except Exception:
massstab = None massstab = None
doc = Rhino.RhinoDoc.ActiveDoc
def m(a, _obj): def m(a, _obj):
if source == "layer": if source == "layer":
a.PlotWeightSource = _LW_FROM_LAYER a.PlotWeightSource = _LW_FROM_LAYER
@@ -1316,6 +1416,11 @@ class GestaltungBridge(panel_base.BaseBridge):
def _set_linetype_source(self, source, name): def _set_linetype_source(self, source, name):
doc = Rhino.RhinoDoc.ActiveDoc doc = Rhino.RhinoDoc.ActiveDoc
if not _has_selection(doc):
_set_default_pen(linetypeSource=source,
linetype=(name if source == "object" else None))
self._send_selection()
return
idx = -1 idx = -1
if source == "object" and name: if source == "object" and name:
try: try:
@@ -2012,6 +2117,30 @@ def _install_selection_listener(bridge):
# Waehrend Move/Rotate werden Sub-Volumen erzeugt die kein Auto-Fill # Waehrend Move/Rotate werden Sub-Volumen erzeugt die kein Auto-Fill
# brauchen, und elemente uebernimmt die Coupling. # brauchen, und elemente uebernimmt die Coupling.
if sc.sticky.get("_dossier_user_transform_active"): return if sc.sticky.get("_dossier_user_transform_active"): return
# 2b) Default-Pen ("Stift fuer neue Objekte") auf frisch gezeichnete
# 2D-Objekte stempeln (Kurven, Text, Hatch, Bemassung ...), solange
# aktiv. Ausgeschlossen: 3D-Volumen (Pen ist ein 2D-Begriff) und
# DOSSIER-Element-Geometrie (dossier_element_type), die elemente.py setzt.
pen = _default_pen()
g = obj.Geometry
is_3d = isinstance(g, (rg.Brep, rg.Extrusion, rg.Mesh, rg.SubD))
if _default_pen_active(pen) and g is not None and not is_3d:
try: dossier_type = obj.Attributes.GetUserString("dossier_element_type") or ""
except Exception: dossier_type = ""
if not dossier_type:
try:
a = obj.Attributes.Duplicate()
_apply_pen_to_attrs(doc, a, pen)
_processing.add(obj.Id)
try:
doc.Objects.ModifyAttributes(obj, a, True)
finally:
_processing.discard(obj.Id)
doc.Views.Redraw()
except Exception as ex:
print("[STYLES] on_add default-pen:", ex)
try: try:
ok = _apply_ebene_fill(doc, obj) ok = _apply_ebene_fill(doc, obj)
except Exception as ex: except Exception as ex:
+36 -10
View File
@@ -493,16 +493,42 @@ function SectionBlock({ sel }) {
} }
function EmptyState() { // Leerer Zustand = "Stift fuer neue Objekte". Dieselben Pen-Controls wie bei
// einer Selektion (PenBlock), aber ohne Auswahl routet das Backend die Setter
// auf den Default-Pen, der dann auf frisch gezeichnete Kurven gestempelt wird.
function NewObjectPen({ pen }) {
const sel = pen || {
linetypes: [], geometryKind: 'curveOpen',
colorSource: 'layer', lwSource: 'layer', linetypeSource: 'layer',
}
return ( return (
<div style={{ <>
padding: '60px 24px', textAlign: 'center', <div style={{ padding: '12px 14px 4px', display: 'flex', flexDirection: 'column', gap: 6 }}>
color: 'var(--text-muted)', fontSize: 11, <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
display: 'flex', flexDirection: 'column', gap: 14, alignItems: 'center', <Icon name="draw" size={16} style={{ color: 'var(--accent)' }} />
}}> <span style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-primary)' }}>
<Icon name="touch_app" size={36} style={{ color: 'var(--text-muted)' }} /> Stift für neue Objekte
<div>Waehle ein oder mehrere Objekte in Rhino aus.</div> </span>
</div> {sel.active && (
<span style={{
fontSize: 9, fontWeight: 600, letterSpacing: '0.06em',
textTransform: 'uppercase', color: 'var(--accent)',
border: '1px solid var(--accent)', borderRadius: 999,
padding: '1px 7px',
}}>aktiv</span>
)}
</div>
<div style={{ fontSize: 10, color: 'var(--text-muted)', lineHeight: 1.4 }}>
{sel.active
? 'Neu gezeichnete Linien & Kurven bekommen diesen Stift. Überall „Nach Ebene" = aus.'
: 'Farbe, Stärke oder Linientyp wählen — gilt für alle neu gezeichneten Objekte, bis du wieder auf „Nach Ebene" stellst.'}
</div>
</div>
<PenBlock sel={sel} />
<div style={{ padding: '2px 14px 14px', fontSize: 9, color: 'var(--text-muted)', fontStyle: 'italic' }}>
Oder wähle ein Objekt in Rhino, um dessen Stift zu ändern.
</div>
</>
) )
} }
@@ -538,7 +564,7 @@ export default function GestaltungApp() {
position: 'relative', position: 'relative',
}}> }}>
<div style={{ flex: 1, overflowY: 'auto' }}> <div style={{ flex: 1, overflowY: 'auto' }}>
{empty ? <EmptyState /> : ( {empty ? <NewObjectPen pen={sel.newObjectPen} /> : (
<> <>
{showFill && ( {showFill && (
<> <>