diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py index 7a5215e..98a34df 100644 --- a/rhino/rhinopanel.py +++ b/rhino/rhinopanel.py @@ -49,6 +49,154 @@ def _hatch_pattern_names(doc): return out +def _list_hatch_patterns_full(doc): + """Vollstaendiges Hatch-Pattern-Listing fuer die Verwaltungs-UI. + Inkludiert die HatchLines (angle, base, offset, dashes) damit + Frontend echte Pattern-Previews rendern kann statt Platzhalter.""" + out = [] + try: + for i in range(doc.HatchPatterns.Count): + try: + hp = doc.HatchPatterns[i] + if hp is None: continue + try: + if bool(hp.IsDeleted): continue + except Exception: pass + ftype = "Lines" + try: + ft = hp.FillType + ftype = str(ft).split(".")[-1] + except Exception: pass + ref = 0 + try: ref = int(hp.Reference) + except Exception: pass + desc = "" + try: desc = str(hp.Description) or "" + except Exception: pass + # HatchLines extrahieren (nur Lines-Typ) + hlines = [] + if ftype.lower() == "lines": + try: + # Probiere zuerst die Property, dann GetHatchLines() + lines = None + try: lines = hp.HatchLines + except Exception: pass + if lines is None: + try: lines = hp.GetHatchLines() + except Exception: pass + if lines is None: + print("[EBENEN] hp[{}] '{}' HatchLines=None".format( + i, hp.Name)) + else: + try: cnt = len(lines) + except Exception: + try: cnt = lines.Count + except Exception: cnt = -1 + print("[EBENEN] hp[{}] '{}' HatchLines type={} count={}".format( + i, hp.Name, type(lines).__name__, cnt)) + for hl in lines: + try: + bp = hl.BasePoint + off = hl.Offset + # Dashes optional — Property heisst auf + # manchen Rhino-Versionen GetDashes() oder + # DashCount/GetDash(i) + dashes = [] + try: + dr = hl.Dashes + if dr is not None: + for d in dr: dashes.append(float(d)) + except Exception: + # Versuche GetDashes() + try: + dr = hl.GetDashes() + if dr is not None: + for d in dr: dashes.append(float(d)) + except Exception: + # Versuche DashCount + GetDash(i) + try: + dc = int(hl.DashCount) + for k in range(dc): + dashes.append(float(hl.GetDash(k))) + except Exception: pass + entry = { + "angle": float(hl.Angle), + "baseX": float(bp.X), + "baseY": float(bp.Y), + "offX": float(off.X), + "offY": float(off.Y), + "dashes": dashes, + } + hlines.append(entry) + except Exception as ex: + print("[EBENEN] hp[{}] hl FAIL:".format(i), ex) + except Exception as ex: + print("[EBENEN] hp[{}] HatchLines outer FAIL:".format(i), ex) + out.append({ + "index": i, + "name": hp.Name or "", + "fillType": ftype, + "description": desc, + "isReference": (ref > 0), + "hatchLines": hlines, + }) + except Exception: + continue + except Exception as ex: + print("[EBENEN] _list_hatch_patterns_full:", ex) + return out + + +def _list_linetypes_full(doc): + """Vollstaendiges Linetype-Listing fuer die Verwaltungs-UI. + Mac Rhino 8 GetSegment(i) returnt (length: float, isLine: bool): + True = Line-Segment, False = Space (Gap). Bei Dot ist length=0.0 + + isLine=True (Punkt = unendlich kurzer Strich).""" + out = [] + try: + for i in range(doc.Linetypes.Count): + try: + lt = doc.Linetypes[i] + if lt is None or lt.IsDeleted: continue + segs = [] + sc_cnt = 0 + try: sc_cnt = int(lt.SegmentCount) + except Exception: pass + for s in range(sc_cnt): + try: + seg = lt.GetSegment(s) + if seg is None: continue + length = float(seg[0]) + # seg[1]: True = Line/Dot, False = Space/Gap + is_line = bool(seg[1]) + if is_line and length == 0.0: + stype = "Dot" + elif is_line: + stype = "Line" + else: + stype = "Space" + segs.append({"length": length, "type": stype}) + except Exception as ex: + print("[EBENEN] lt[{}] GetSegment({}) FAIL: {}".format( + i, s, ex)) + ref = 0 + try: ref = int(lt.Reference) + except Exception: pass + out.append({ + "index": i, + "name": lt.Name or "", + "segments": segs, + "isContinuous": (len(segs) == 0), + "isReference": (ref > 0), + }) + except Exception as ex: + print("[EBENEN] linetype outer FAIL:", ex) + continue + except Exception as ex: + print("[EBENEN] _list_linetypes_full:", ex) + return out + + def _read_launcher_schema(): """Liest das Default-Layer-Schema aus dossier_settings.json (Launcher-Pfad). Liefert eine Liste {code, name, color, lw} oder None wenn nicht gesetzt.""" @@ -692,6 +840,8 @@ class EbenenBridge(panel_base.BaseBridge): "materials": current.get("materials", []), "builtinMaterials": built_in, "hatchPatterns": _hatch_pattern_names(doc), + "hatchPatternsFull": _list_hatch_patterns_full(doc), + "linetypes": _list_linetypes_full(doc), } def on_save(updated): doc2 = Rhino.RhinoDoc.ActiveDoc @@ -774,6 +924,20 @@ class EbenenBridge(panel_base.BaseBridge): except Exception: pass elif t == "PICK_TEXTURE_FILE": self._pick_texture(p) + elif t == "RENAME_LINETYPE": + self._rename_linetype(p) + elif t == "DELETE_LINETYPE": + self._delete_linetype(p) + elif t == "LOAD_LINETYPE_DEFAULTS": + self._load_linetype_defaults() + elif t == "IMPORT_LINETYPE_FILE": + self._import_linetype_file() + elif t == "RENAME_HATCH": + self._rename_hatch(p) + elif t == "DELETE_HATCH": + self._delete_hatch(p) + elif t == "IMPORT_HATCH_FILE": + self._import_hatch_file() def _pick_texture(self, payload): slot = payload.get("slot") or "diffuse" try: @@ -794,6 +958,151 @@ class EbenenBridge(panel_base.BaseBridge): except Exception as ex: print("[PROJECT-SETTINGS] pick_texture:", ex) self.send("TEXTURE_PICKED", {"slot": slot, "path": None}) + + # ---- Linetype CRUD ---- + def _rename_linetype(self, payload): + idx = payload.get("index") + new_name = (payload.get("name") or "").strip() + if idx is None or not new_name: return + d = Rhino.RhinoDoc.ActiveDoc + if d is None: return + try: + lt = d.Linetypes[int(idx)] + if lt is None or lt.IsDeleted: return + lt.Name = new_name + d.Linetypes.Modify(lt, int(idx), True) + except Exception as ex: + print("[PROJECT-SETTINGS] rename_linetype:", ex) + self._send_tables() + + def _delete_linetype(self, payload): + idx = payload.get("index") + if idx is None: return + d = Rhino.RhinoDoc.ActiveDoc + if d is None: return + try: + lt = d.Linetypes[int(idx)] + if lt is None: return + # Default-Linetypes (Continuous, ByLayer) sollten nicht + # geloescht werden — Rhino's Delete macht es uns aber + # einfach: gibt False zurueck wenn nicht erlaubt. + d.Linetypes.Delete(int(idx), True) + except Exception as ex: + print("[PROJECT-SETTINGS] delete_linetype:", ex) + self._send_tables() + + def _load_linetype_defaults(self): + d = Rhino.RhinoDoc.ActiveDoc + if d is None: return + try: + d.Linetypes.LoadDefaultLinetypes(True) + except Exception as ex: + print("[PROJECT-SETTINGS] load_linetype_defaults:", ex) + self._send_tables() + + def _import_linetype_file(self): + """Datei-Picker fuer .lin (AutoCAD-Linetype) → Linetypes.Load.""" + d = Rhino.RhinoDoc.ActiveDoc + if d is None: return + try: + import Eto.Forms as forms + dlg = forms.OpenFileDialog() + dlg.Title = "Linientyp-Datei waehlen (.lin)" + dlg.MultiSelect = False + dlg.Filters.Add(forms.FileFilter("AutoCAD Linetypes", ".lin")) + dlg.Filters.Add(forms.FileFilter("Alle", ".*")) + parent = bridge_holder.get("form") + res = dlg.ShowDialog(parent) if parent else dlg.ShowDialog(None) + if str(res) != "Ok": return + path = dlg.FileName or "" + if not path: return + cnt_before = d.Linetypes.Count + # Rhino-API: Linetypes.Load(filename) — Achtung in + # manchen Builds heisst es LoadLinetypeFile(...). + ok = False + try: + ok = bool(d.Linetypes.Load(path)) + except Exception: + try: ok = bool(d.Linetypes.LoadLinetypeFile(path)) + except Exception as ex: + print("[PROJECT-SETTINGS] Linetypes.Load:", ex) + cnt_after = d.Linetypes.Count + print("[PROJECT-SETTINGS] linetype import: ok={} {} -> {} ({} neu)".format( + ok, cnt_before, cnt_after, cnt_after - cnt_before)) + except Exception as ex: + print("[PROJECT-SETTINGS] import_linetype_file:", ex) + self._send_tables() + + def _import_hatch_file(self): + """Datei-Picker fuer .pat (AutoCAD-Hatch) → + HatchPatterns.LoadFromFile.""" + d = Rhino.RhinoDoc.ActiveDoc + if d is None: return + try: + import Eto.Forms as forms + dlg = forms.OpenFileDialog() + dlg.Title = "Schraffur-Datei waehlen (.pat)" + dlg.MultiSelect = False + dlg.Filters.Add(forms.FileFilter("AutoCAD Hatches", ".pat")) + dlg.Filters.Add(forms.FileFilter("Alle", ".*")) + parent = bridge_holder.get("form") + res = dlg.ShowDialog(parent) if parent else dlg.ShowDialog(None) + if str(res) != "Ok": return + path = dlg.FileName or "" + if not path: return + cnt_before = d.HatchPatterns.Count + cnt_imported = 0 + try: + # LoadFromFile(file, replaceExisting) — returnt int + cnt_imported = int(d.HatchPatterns.LoadFromFile(path, False)) + except Exception: + try: + cnt_imported = int(d.HatchPatterns.Load(path)) + except Exception as ex: + print("[PROJECT-SETTINGS] HatchPatterns.Load:", ex) + cnt_after = d.HatchPatterns.Count + print("[PROJECT-SETTINGS] hatch import: {} (Tabelle {} -> {})".format( + cnt_imported, cnt_before, cnt_after)) + except Exception as ex: + print("[PROJECT-SETTINGS] import_hatch_file:", ex) + self._send_tables() + + # ---- Hatch-Pattern CRUD ---- + def _rename_hatch(self, payload): + idx = payload.get("index") + new_name = (payload.get("name") or "").strip() + if idx is None or not new_name: return + d = Rhino.RhinoDoc.ActiveDoc + if d is None: return + try: + hp = d.HatchPatterns[int(idx)] + if hp is None or hp.IsDeleted: return + hp.Name = new_name + d.HatchPatterns.Modify(hp, int(idx), True) + except Exception as ex: + print("[PROJECT-SETTINGS] rename_hatch:", ex) + self._send_tables() + + def _delete_hatch(self, payload): + idx = payload.get("index") + if idx is None: return + d = Rhino.RhinoDoc.ActiveDoc + if d is None: return + try: + d.HatchPatterns.Delete(int(idx), True) + except Exception as ex: + print("[PROJECT-SETTINGS] delete_hatch:", ex) + self._send_tables() + + def _send_tables(self): + """Sendet aktuelle Linetype + Hatch-Tabellen ans Frontend + (TABLES_UPDATED-Message). Frontend re-rendert die Listen.""" + d = Rhino.RhinoDoc.ActiveDoc + if d is None: return + self.send("TABLES_UPDATED", { + "linetypes": _list_linetypes_full(d), + "hatchPatternsFull": _list_hatch_patterns_full(d), + }) b = _ProjectSettingsBridge() bridge_holder["form"] = panel_base.open_satellite_window( "project_settings", diff --git a/src/components/ProjectSettingsDialog.jsx b/src/components/ProjectSettingsDialog.jsx index f075c20..737c616 100644 --- a/src/components/ProjectSettingsDialog.jsx +++ b/src/components/ProjectSettingsDialog.jsx @@ -1,7 +1,11 @@ import { useState, useEffect } from 'react' import Icon from './Icon' import { BarToggle, BarButton, BAR_H } from './BarControls' -import { openLibrary, pickTextureFile, onMessage } from '../lib/rhinoBridge' +import { + openLibrary, pickTextureFile, onMessage, + renameLinetype, deleteLinetype, loadLinetypeDefaults, importLinetypeFile, + renameHatch, deleteHatch, importHatchFile, +} from '../lib/rhinoBridge' /* Field — Stack-Layout fuer komplexe Inputs (mehrere Felder nebeneinander) */ function Field({ label, hint, children, style }) { @@ -67,6 +71,248 @@ function TabBar({ tabs, active, onChange }) { ) } +/* LinetypePreview — SVG-Linie mit Strich-Segmenten. segments = [{length,type}] + type ∈ Line/Space (manchmal auch Continuous-Ableitungen). Width in px; + wir skalieren die Segmente damit das Gesamtmuster in width passt. */ +function LinetypePreview({ segments, width = 120, height = 12 }) { + if (!segments || segments.length === 0) { + return ( + + + + ) + } + // Dot hat length=0 — fuer Layout-Zwecke kleinen Pseudo-Wert + const DOT_DRAW_LEN = 0.6 + const lenOf = (seg) => { + const l = Math.abs(seg.length || 0) + return seg.type === 'Dot' ? DOT_DRAW_LEN : l + } + const patternLen = segments.reduce((s, seg) => s + lenOf(seg), 0) + if (patternLen <= 0) { + return ( + + + + ) + } + // Statt das Pattern auf die ganze Breite zu strecken: tilen mit fixer + // Skala. Ziel: ca. 4-5 Pattern-Repetitions sichtbar, dann ist die + // Sequenz auch bei kurzen Patterns (Dots) deutlich erkennbar. + const TARGET_REPETITIONS = 4 + const scale = width / (patternLen * TARGET_REPETITIONS) + const scaledPatternLen = patternLen * scale + const repetitions = Math.ceil(width / scaledPatternLen) + 1 + const parts = [] + let x = 0 + let key = 0 + for (let r = 0; r < repetitions && x < width; r++) { + for (const seg of segments) { + const drawLen = lenOf(seg) * scale + if (x >= width) break + const x2 = Math.min(x + drawLen, width) + if (seg.type === 'Line' && drawLen > 0) { + parts.push() + } else if (seg.type === 'Dot') { + parts.push() + } + x += drawLen + } + } + return ( + + {parts} + + ) +} + +/* LinetypeListRow — Zeile mit Name + Preview + selected-state */ +function LinetypeListRow({ lt, isSelected, onSelect }) { + return ( +
{ + if (!isSelected) e.currentTarget.style.background = 'var(--bg-item-hover)' + }} + onMouseLeave={(e) => { + if (!isSelected) e.currentTarget.style.background = 'transparent' + }}> + {lt.name || 'Unbenannt'} + +
+ ) +} + +/* HatchPreview — rendert das ECHTE Hatch-Pattern (alle HatchLines mit + Winkel/Offset/Dashes) als SVG-Tile. Solid/Gradient haben keine Lines. + pixelsPerUnit skaliert das Pattern damit's im Tile gut sichtbar ist. */ +function HatchPreview({ pattern, size = 28, pixelsPerUnit = 8 }) { + const ft = (pattern.fillType || '').toLowerCase() + const stroke = 'var(--text-primary)' + const bg = 'var(--bg-input)' + if (ft === 'solid') { + return ( + + + + ) + } + if (ft === 'gradient') { + const id = 'gr_' + (pattern.index ?? 0) + return ( + + + + + + + + + + ) + } + // Lines: jede HatchLine ist eine Familie paralleler Linien, mit + // Angle, BasePoint, Offset (zwischen Linien), Dashes (Strich/Lücke). + const hls = pattern.hatchLines || [] + const clipId = 'hp_clip_' + (pattern.index ?? 0) + const pieces = [] + let lineKey = 0 + // Tile-Mitte im SVG-Koordinatensystem + const cx = size / 2 + const cy = size / 2 + hls.forEach((hl, hi) => { + // SVG hat Y nach unten — Rhino-Pattern definiert Y nach oben. + // Wir spiegeln nicht, weil's nur ein Visual ist; Rotation reicht. + const a = hl.angle || 0 // rad + const cosA = Math.cos(a) + const sinA = Math.sin(a) + // Perpendicular zur Linie: + const perpX = -sinA + const perpY = cosA + // Offset entlang perp + along — Rhino's Offset ist {X, Y} im + // Pattern-Koord-System. Die Komponente PARALLEL zur Linien-Richtung + // (entlang) entscheidet uebers Stagger (Strich-Versatz). + const offX = hl.offX || 0 + const offY = hl.offY || 0 + // Spacing = Komponente von Offset SENKRECHT zur Linien-Richtung + const spacing = Math.abs(offX * perpX + offY * perpY) + const drawSpacing = Math.max(spacing * pixelsPerUnit, 1.5) + // Stagger = Komponente von Offset PARALLEL zur Linien-Richtung + const stagger = offX * cosA + offY * sinA + const drawStagger = stagger * pixelsPerUnit + // Dashes in Pixel + const dashes = (hl.dashes || []).map(d => Math.abs(d) * pixelsPerUnit) + const dashLen = dashes.reduce((s, d) => s + d, 0) + const strokeDasharray = (dashes.length >= 2 && dashLen > 0.1) + ? dashes.join(',') : undefined + // Basis-Punkt in Pixel (von Tile-Center) + const baseX = (hl.baseX || 0) * pixelsPerUnit + const baseY = (hl.baseY || 0) * pixelsPerUnit + // Wir zeichnen genug parallele Linien um Tile zu fuellen + const halfDiag = size * 1.5 // overlap fuer rotate-clip + const nLines = Math.ceil((size * 2.0) / drawSpacing) + 4 + const start = -Math.floor(nLines / 2) + for (let i = 0; i < nLines; i++) { + const k = start + i + // Mittelpunkt dieser Linie: base + k*offset, vom Tile-Center aus + // Wir setzen die Pattern-Origin in die Tile-Mitte. + const mx = cx + baseX + k * (offX * pixelsPerUnit) + const my = cy + baseY + k * (offY * pixelsPerUnit) + // Linie streckt sich +-halfDiag entlang Linien-Richtung + const x1 = mx - cosA * halfDiag + const y1 = my - sinA * halfDiag + const x2 = mx + cosA * halfDiag + const y2 = my + sinA * halfDiag + pieces.push() + } + }) + // Fallback wenn keine HatchLines kamen (Hatch ist 'Lines' aber leer) + const hasLines = pieces.length > 0 + return ( + + + + + + + + + {hasLines ? pieces : ( + // Fallback: einzelne Diagonale alle 4 px + Array.from({length: Math.ceil(size * 2 / 4)}, (_, i) => i * 4 - size) + .map(i => ( + + )) + )} + + + ) +} + +/* HatchListRow */ +function HatchListRow({ hp, isSelected, onSelect }) { + return ( +
{ + if (!isSelected) e.currentTarget.style.background = 'var(--bg-item-hover)' + }} + onMouseLeave={(e) => { + if (!isSelected) e.currentTarget.style.background = 'transparent' + }}> + +
+
{hp.name || 'Unbenannt'}
+
{hp.fillType}
+
+
+ ) +} + /* MaterialListRow — schmale Listen-Zeile links (ArchiCAD-Stil): Color-Swatch + Name + Source-Badge. Click selektiert. */ function MaterialListRow({ mat, isBuiltin, isSelected, onSelect }) { @@ -343,6 +589,12 @@ export default function ProjectSettingsDialog({ return null }) const [matSearch, setMatSearch] = useState('') + // Linetype + Hatch-Tabellen — initial aus params, danach via + // TABLES_UPDATED Message vom Backend nach jeder CRUD-Op. + const [linetypes, setLinetypes] = useState(initial.linetypes || []) + const [hatches, setHatches] = useState(initial.hatchPatternsFull || []) + const [selLt, setSelLt] = useState(null) + const [selHt, setSelHt] = useState(null) const builtin = initial.builtinMaterials || [] // Aktuell ausgewaehltes Material aus Selection ableiten @@ -367,6 +619,14 @@ export default function ProjectSettingsDialog({ setSelMat(null) } + // Linetype/Hatch-Tabelle nach Backend-CRUD aktualisieren + useEffect(() => { + onMessage('TABLES_UPDATED', ({ linetypes: lts, hatchPatternsFull: hps }) => { + if (lts) setLinetypes(lts) + if (hps) setHatches(hps) + }) + }, []) + // Backend-File-Picker-Antwort: aktualisiert das Slot im aktuell // selektierten Material. Wenn path leer = User abgebrochen → no-op. useEffect(() => { @@ -436,6 +696,8 @@ export default function ProjectSettingsDialog({ {/* Body */} @@ -556,6 +818,188 @@ export default function ProjectSettingsDialog({ )} + + {tab === 'linetypes' && ( +
+
+
+ {linetypes.length === 0 && ( +
+ Keine Linientypen geladen. +
+ )} + {linetypes.map((lt) => ( + setSelLt(lt.index)} /> + ))} +
+
+ + +
+
+
+ {(() => { + const lt = linetypes.find(x => x.index === selLt) + if (!lt) return ( +
+ Linientyp links auswählen. +
+ ) + return ( + <> + +
+ { + const newName = ev.target.value + setLinetypes(arr => arr.map(x => + x.index === lt.index ? { ...x, name: newName } : x)) + }} + onBlur={(ev) => renameLinetype(lt.index, ev.target.value)} + style={{ flex: 1, height: BAR_H, padding: '0 12px', + fontSize: 12, fontWeight: 500 }} /> + {!lt.isContinuous && ( + { deleteLinetype(lt.index); setSelLt(null) }} + title="Linientyp löschen" /> + )} +
+
+ +
+ +
+
+ {lt.segments.length > 0 && ( + +
+ {lt.segments.map((s, i) => ( +
+ {String(i+1).padStart(2, '0')} — + {' '}{s.type.padEnd(10)} + {' '}{s.length.toFixed(3)} +
+ ))} +
+
+ )} + + ) + })()} +
+
+ )} + + {tab === 'hatches' && ( +
+
+
+ {hatches.length === 0 && ( +
+ Keine Schraffuren geladen. +
+ )} + {hatches.map((hp) => ( + setSelHt(hp.index)} /> + ))} +
+
+ +
+
+
+ {(() => { + const hp = hatches.find(x => x.index === selHt) + if (!hp) return ( +
+ Schraffur links auswählen. +
+ ) + return ( + <> + +
+ + { + const newName = ev.target.value + setHatches(arr => arr.map(x => + x.index === hp.index ? { ...x, name: newName } : x)) + }} + onBlur={(ev) => renameHatch(hp.index, ev.target.value)} + style={{ flex: 1, height: BAR_H, padding: '0 12px', + fontSize: 12, fontWeight: 500 }} /> + { deleteHatch(hp.index); setSelHt(null) }} + title="Schraffur löschen" /> +
+
+ Typ: {hp.fillType} + {hp.isReference && ' · importiert'} +
+
+ {hp.description && ( + +
+ {hp.description} +
+
+ )} + +
+ Schraffuren werden ueber das Gestaltung-Panel + oder via Layer Section-Hatch im Ebenen-Editor + auf Objekte/Layer angewendet. +
+
+ + ) + })()} +
+
+ )} {/* Footer — Pill-Buttons */} diff --git a/src/lib/rhinoBridge.js b/src/lib/rhinoBridge.js index fbf150e..9ddf44c 100644 --- a/src/lib/rhinoBridge.js +++ b/src/lib/rhinoBridge.js @@ -194,6 +194,16 @@ export function openLibrary() { send('OPEN_LIBRARY', {}) } export function pickTextureFile(slot) { send('PICK_TEXTURE_FILE', { slot: slot || 'diffuse' }) } +// Linetype-Tabelle (doc.Linetypes) verwalten — Antwort via +// TABLES_UPDATED-Message mit {linetypes, hatchPatternsFull}. +export function renameLinetype(index, name) { send('RENAME_LINETYPE', { index, name }) } +export function deleteLinetype(index) { send('DELETE_LINETYPE', { index }) } +export function loadLinetypeDefaults() { send('LOAD_LINETYPE_DEFAULTS', {}) } +export function importLinetypeFile() { send('IMPORT_LINETYPE_FILE', {}) } +// Hatch-Pattern-Tabelle (doc.HatchPatterns) verwalten +export function renameHatch(index, name) { send('RENAME_HATCH', { index, name }) } +export function deleteHatch(index) { send('DELETE_HATCH', { index }) } +export function importHatchFile() { send('IMPORT_HATCH_FILE', {}) } // Schnitt/Ansicht — interaktiver 2-Punkt-Pick im Rhino-Viewport. Erzeugt // eine neue Zeichnungsebene type=schnitt + 2D-Plan-Symbol + aktiviert sie. // opts: { cutAtLine: bool, depthBack: m, heightMin: m, heightMax: m, namePrefix }