diff --git a/rhino/elemente.py b/rhino/elemente.py index 11881b5..b683a1d 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -4815,6 +4815,7 @@ def _make_treppe_volume(axis_curve, breite, referenz, n_stufen, uk, ok, return None + def _regenerate_element(doc, element_id): """Regeneriert das Volumen eines Elements (Wand oder Decke) anhand seines Source-Objekts (Achse bzw. Outline).""" @@ -9733,6 +9734,81 @@ def _apply_oeffnung_constraint(new_obj, meta, old_obj=None): return geom_changed +def _is_active_view_top_like(): + """True wenn der aktive Viewport von oben/unten reinschaut (Plan- + Ansicht). Erkennt Top + Bottom + custom Views deren Kamera-Richtung + ueberwiegend vertikal ist.""" + try: + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return False + view = doc.Views.ActiveView + if view is None: return False + vp = view.ActiveViewport + if vp is None: return False + cam = vp.CameraDirection + if cam.Length < 1e-9: return False + cam_n = rg.Vector3d(cam.X, cam.Y, cam.Z) + cam_n.Unitize() + # Vertikal = |Z-Komponente| nahe 1.0 + return abs(cam_n.Z) > 0.95 + except Exception: + return False + + +def _suppress_z_drift_if_top_view(e): + """Bei Move in Top-View: Z-Komponente des Drifts zurueckrollen. + Vergleicht Z der OldRhinoObject-Geometry mit der NewRhinoObject- + Geometry; wenn signifikanter Z-Drift, translatiert die neue Geometry + in -Z zurueck. + + Wird NUR fuer SOURCE-Curves gerufen (Caller filtert vorher). Smart- + Elements sollen in Plan-Ansicht 2D-Verhalten haben — Z-Drift via + Z-Snap oder Gumball-Z ist meist ungewollt. + + Wenn der User WIRKLICH Z aendern will: einfach in eine andere View + wechseln (Front, Right, Perspective).""" + if not _is_active_view_top_like(): return + try: + old_obj = e.OldRhinoObject + new_obj = e.NewRhinoObject + if old_obj is None or new_obj is None: return + old_geom = old_obj.Geometry + new_geom = new_obj.Geometry + if old_geom is None or new_geom is None: return + # BBox-Min-Z als Referenz nehmen — robust gegen unterschiedliche + # Curve-Typen (Line, Polyline, Point, Curve). + try: bb_old = old_geom.GetBoundingBox(True) + except Exception: return + try: bb_new = new_geom.GetBoundingBox(True) + except Exception: return + if not bb_old.IsValid or not bb_new.IsValid: return + z_old = bb_old.Min.Z + z_new = bb_new.Min.Z + dz = z_new - z_old + # Schwellwert 0.001 Doc-Units (≈ 1mm wenn Doc in m) damit Floating- + # Point-Mikro-Drift nicht panisch korrigiert wird. + if abs(dz) < 0.001: return + # Korrigiere: new geometry um -dz in Z verschieben. + corrected = new_geom.Duplicate() + try: + corrected.Transform(rg.Transform.Translation(0, 0, -dz)) + except Exception: return + # Replace — _REGEN_BUSY setzen damit der Replace-Event nicht + # rekursiv wieder unseren Guard triggert. + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + _was = sc.sticky.get(_REGEN_BUSY, False) + sc.sticky[_REGEN_BUSY] = True + try: + doc.Objects.Replace(new_obj.Id, corrected) + print("[ELEMENTE] Top-View Z-Guard: Δz={:.4f} → 0 fuer {}".format( + dz, new_obj.Attributes.GetUserString(_KEY_ID) or "?")) + finally: + sc.sticky[_REGEN_BUSY] = _was + except Exception as ex: + print("[ELEMENTE] _suppress_z_drift_if_top_view:", ex) + + def _on_object_replaced(sender, e): """Wenn eine Source (Wand-Achse/Decke-Outline/etc.) veraendert wird → Regeneration queuen (debounct ueber Idle, 50 ms Ruhe). @@ -9792,6 +9868,14 @@ def _on_object_replaced_body(sender, e): except Exception: pass if meta is None or meta.get("type") not in SOURCE_TYPES: return + # Top-View Z-Guard: in Plan-Ansicht soll keine Z-Verschiebung + # passieren. User bewegt Wand/Decke via _Move oder Gumball und + # erwartet 2D-Verhalten — Z-Drift durch versehentliches Snappen + # auf Z!=0 Objekte oder Gumball-Z wird zurueckgenommen. + try: + _suppress_z_drift_if_top_view(e) + except Exception as ex: + print("[ELEMENTE] z-guard:", ex) try: new_obj = e.NewRhinoObject if new_obj and not _read_meta(new_obj): diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index f2d4cec..d5a0828 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -722,17 +722,95 @@ def _set_display_mode(name): # --- Snap / Ortho via ModelAidSettings -------------------------------------- +# Architekten-relevante Osnap-Modes — UI-Key → OsnapModes-Flag. +# Vollstaendige Liste in Rhino: End, Near, Focus, Center, Vertex, Knot, +# Quadrant, Midpoint, Intersection, Perpendicular, Tangent, Point. +# Architektur-Workflow nutzt v.a. die ersten 6. +def _osnap_flag_map(): + try: + OM = Rhino.ApplicationSettings.OsnapModes + return { + "end": OM.End, + "mid": OM.Midpoint, + "int": OM.Intersection, + "perp": OM.Perpendicular, + "cen": OM.Center, + "near": OM.Near, + } + except Exception: + return {} + + +def _get_osnap_modes_dict(): + flags = _osnap_flag_map() + if not flags: return {} + try: + cur = int(Rhino.ApplicationSettings.ModelAidSettings.OsnapModes) + return {k: bool(cur & int(f)) for k, f in flags.items()} + except Exception: + return {k: False for k in flags} + + +def _set_osnap_mode(key, enabled): + flags = _osnap_flag_map() + flag = flags.get(key) + if flag is None: return + try: + s = Rhino.ApplicationSettings.ModelAidSettings + cur = int(s.OsnapModes) + flag_i = int(flag) + new = (cur | flag_i) if enabled else (cur & ~flag_i) + s.OsnapModes = Rhino.ApplicationSettings.OsnapModes(new) + except Exception as ex: + print("[OBERLEISTE] _set_osnap_mode:", ex) + + +def _is_grid_visible(): + try: + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return True + view = doc.Views.ActiveView + if view is None: return True + vp = view.ActiveViewport + try: return bool(vp.ConstructionGridVisible) + except Exception: + try: return bool(vp.ShowConstructionGrid) + except Exception: return True + except Exception: + return True + + +def _set_grid_visible(visible): + """Schaltet Konstruktions-Grid in ALLEN Modell-Viewports an/aus.""" + doc = Rhino.RhinoDoc.ActiveDoc + if doc is None: return + v_in = bool(visible) + for v in doc.Views: + try: + vp = v.ActiveViewport + try: vp.ConstructionGridVisible = v_in + except Exception: + try: vp.ShowConstructionGrid = v_in + except Exception: pass + except Exception: pass + try: doc.Views.Redraw() + except Exception: pass + + def _get_snap_state(): try: s = Rhino.ApplicationSettings.ModelAidSettings return { - "ortho": bool(s.Ortho), - "gridSnap": bool(s.GridSnap), - "osnap": bool(s.UseHorizontalDialog) if False else bool(getattr(s, "Osnap", False)) or False, - "planar": bool(getattr(s, "ProjectOsnapsToCPlane", False)), + "ortho": bool(s.Ortho), + "gridSnap": bool(s.GridSnap), + "osnap": bool(getattr(s, "Osnap", False)) or bool(getattr(s, "OsnapEnabled", False)), + "planar": bool(getattr(s, "ProjectOsnapsToCPlane", False)), + "gridVisible": _is_grid_visible(), + "osnapModes": _get_osnap_modes_dict(), } except Exception: - return {"ortho": False, "gridSnap": False, "osnap": False, "planar": False} + return {"ortho": False, "gridSnap": False, "osnap": False, + "planar": False, "gridVisible": True, "osnapModes": {}} def _set_ortho(v): @@ -753,11 +831,11 @@ def _set_osnap_master(v): """Master-Toggle fuer Object-Snap (alle aktiven Snaps).""" try: s = Rhino.ApplicationSettings.ModelAidSettings - if hasattr(s, "Osnap"): - s.Osnap = bool(v) - elif hasattr(s, "UsePoints"): - # Fallback: einzelne Modi durch - pass + # Verschiedene Rhino-Versionen — beide Properties probieren + for attr in ("Osnap", "OsnapEnabled"): + if hasattr(s, attr): + setattr(s, attr, bool(v)) + return except Exception as ex: print("[OBERLEISTE] _set_osnap_master:", ex) @@ -1018,6 +1096,12 @@ class OberleisteBridge(panel_base.BaseBridge): elif t == "TOGGLE_OSNAP": _set_osnap_master(bool(p.get("enabled"))) self._send_state(force=True) + elif t == "SET_OSNAP_MODE": + _set_osnap_mode(p.get("key") or "", bool(p.get("enabled"))) + self._send_state(force=True) + elif t == "TOGGLE_GRID_VISIBLE": + _set_grid_visible(bool(p.get("visible"))) + self._send_state(force=True) # --- Graphical Overrides ---------------------------------------- elif t == "TOGGLE_OVERRIDES": @@ -1464,6 +1548,8 @@ class OberleisteBridge(panel_base.BaseBridge): info.get("viewMode"), info.get("displayMode"), info.get("ortho"), info.get("gridSnap"), info.get("osnap"), + info.get("gridVisible"), + tuple(sorted((info.get("osnapModes") or {}).items())), info.get("showLineweights"), info["overridesEnabled"], info["overridesCount"], info.get("overridesActivePreset"), diff --git a/rhino/schnitte.py b/rhino/schnitte.py index 60a43c4..22f943e 100644 --- a/rhino/schnitte.py +++ b/rhino/schnitte.py @@ -247,8 +247,23 @@ def activate_schnitt(doc, z, skip_view=False): obj = _add_clipping_plane(doc, back_plane, du, dv, vp_ids, "back") if obj is not None: n_planes += 1 - # View setzen: Parallel-Projektion, Kamera senkrecht zur Linie. - # Bei skip_view=True (Grip-Drag-Re-Activate) komplett ueberspringen. + # Projektion: 'parallel' (klassischer Schnitt) oder 'perspective' + # (Schnittperspektive — perspektivische Section mit gleicher Cut- + # Logik). Bei perspective wird Kamera leicht naeher geholt + FOV + # gesetzt; Cut-Planes sind identisch. + projection = (z.get("projection") or "parallel").strip().lower() + if projection not in ("parallel", "perspective"): projection = "parallel" + + # Kamera-Z fuer Perspektive: explizit ueber cameraHeight setzbar, + # sonst Default = Mitte der Hoehenrange (= plane_z). Bei Parallel + # ignoriert weil Kamera-Z in Orthoprojektion das Bild nicht aendert. + try: + cam_z = float(z.get("cameraHeight")) if z.get("cameraHeight") is not None else plane_z + except Exception: + cam_z = plane_z + + # View setzen — Kamera senkrecht zur Linie. Bei skip_view=True + # (Grip-Drag-Re-Activate) komplett ueberspringen. if not skip_view: try: view = doc.Views.ActiveView @@ -257,18 +272,28 @@ def activate_schnitt(doc, z, skip_view=False): if view is not None: vp = view.ActiveViewport cam_dist = max(50.0, depth_back * 3 + line_len) + # Bei Perspektive: Kamera + Target auf cam_z. Bei Parallel: + # plane_z (Mitte Hoehenrange) — Z spielt eh keine Rolle + # fuers Bild, aber sauber gesetzt fuer konsistente + # Kamera-Ausrichtung. + view_z = cam_z if projection == "perspective" else plane_z cam_pos = rg.Point3d( mid.X - view_dir.X * cam_dist, mid.Y - view_dir.Y * cam_dist, - plane_z) + view_z) target = rg.Point3d( mid.X + view_dir.X * (depth_back * 0.5), mid.Y + view_dir.Y * (depth_back * 0.5), - plane_z) - vp.ChangeToParallelProjection(True) + view_z) + if projection == "perspective": + vp.ChangeToPerspectiveProjection(True, 50.0) + else: + vp.ChangeToParallelProjection(True) vp.SetCameraLocations(target, cam_pos) vp.CameraUp = rg.Vector3d(0, 0, 1) - # Zoom auf Schnitt-BoundingBox + etwas Rand + # Zoom auf Schnitt-BoundingBox + etwas Rand. Bei Perspektive + # macht ZoomBoundingBox auch Sinn — Rhino passt das FOV-Frame + # entsprechend an. bb = rg.BoundingBox( rg.Point3d(min(p1.X, p2.X) - margin, min(p1.Y, p2.Y) - margin, h_min - margin), diff --git a/src/OberleisteApp.jsx b/src/OberleisteApp.jsx index 431d364..9a62a60 100644 --- a/src/OberleisteApp.jsx +++ b/src/OberleisteApp.jsx @@ -17,6 +17,8 @@ import { setDarstellung, arrangeSelection, toggleReferenzlinien, + toggleOsnap, + setOsnapMode, toggleGridVisible, } from './lib/rhinoBridge' const PRESETS = [ @@ -706,6 +708,97 @@ export default function OberleisteApp() {
+ {/* ====== SNAP-BAR (Architektur-Osnaps + Grid) ====== + 4×2 Grid, nur Icons, schlank. Symbol-Wahl orientiert sich an + Rhinos eigenen Snap-Markern (End=Quadrat, Mid=Dreieck, Cen= + Kreis, Int=X, Perp=Winkel, Near=Plus). Ortho + Grid-Snap sind + in Rhinos Footer-Bar — hier nur was dort fehlt. + Reihe 1: Master-O | End | Mid | Int + Reihe 2: Perp | Cen | Near | Grid */} + {(() => { + const om = state.osnapModes || {} + const osnapDisabled = !state.osnap + const IconBtn = ({ icon, active, disabled, onClick, isFirst, title }) => ( + + ) + const rowStyle = { + display: 'inline-flex', width: BAR_H * 4, + height: BAR_H + 2, boxSizing: 'border-box', + border: '1px solid var(--border)', borderRadius: 999, + overflow: 'hidden', flexShrink: 0, + } + return ( +
+
+ toggleOsnap(!state.osnap)} + title={state.osnap ? 'Object-Snap an' : 'Object-Snap aus'} /> + setOsnapMode('end', !om.end)} + title="Endpunkt (End)" /> + setOsnapMode('mid', !om.mid)} + title="Mittelpunkt (Mid)" /> + setOsnapMode('int', !om.int)} + title="Schnittpunkt (Intersection)" /> +
+
+ setOsnapMode('perp', !om.perp)} + title="Lotrecht (Perpendicular)" /> + setOsnapMode('cen', !om.cen)} + title="Kreis-/Bogen-Mittelpunkt (Center)" /> + setOsnapMode('near', !om.near)} + title="Naechster Punkt (Near)" /> + toggleGridVisible(state.gridVisible === false)} + title="Konstruktions-Raster ein-/ausblenden" /> +
+
+ ) + })()} + +
+ {/* ====== TEXT-Block (Vectorworks-Stil) ====== Reihe 1: Style ▼ | Font ▼ | Size ▼ Reihe 2: [B][I][U] | [L][C][R] | [+] diff --git a/src/components/GeschossManager.jsx b/src/components/GeschossManager.jsx index 8607225..df41673 100644 --- a/src/components/GeschossManager.jsx +++ b/src/components/GeschossManager.jsx @@ -11,6 +11,8 @@ function GeschossBadge({ name }) { function ZeichnungsebeneRow({ z, active, mode, onClick, onContextMenu, onToggleVisible, onToggleLock, onToggleClipping, onDelete, + onDragStart, onDragOver, onDragLeave, onDrop, onDragEnd, + isDragging, dropPos, }) { const isGeschoss = !!z.isGeschoss const isSchnitt = z.type === 'schnitt' @@ -48,6 +50,12 @@ function ZeichnungsebeneRow({ } return (
onDragStart && onDragStart(ev, z.id)} + onDragOver={(ev) => onDragOver && onDragOver(ev, z.id)} + onDragLeave={(ev) => onDragLeave && onDragLeave(ev, z.id)} + onDrop={(ev) => onDrop && onDrop(ev, z.id)} + onDragEnd={(ev) => onDragEnd && onDragEnd(ev)} onClick={onClick} onContextMenu={onContextMenu} style={{ @@ -56,7 +64,16 @@ function ZeichnungsebeneRow({ margin: 0, background: active ? 'var(--active-dim)' : 'var(--bg-item)', borderRadius: active ? 999 : 0, - borderBottom: active ? '1px solid transparent' : '1px solid var(--border-light)', + // Drop-Indikator: 2px accent-Linie an Top oder Bottom — zeigt + // wo der gedragte Eintrag nach Loslassen landet. Sonst normale + // unten-Trennlinie. isDragging: leichtes Faden des gehobenen + // Eintrags fuer visuelles Feedback. + borderTop: dropPos === 'top' + ? '2px solid var(--accent)' : '0 solid transparent', + borderBottom: dropPos === 'bottom' + ? '2px solid var(--accent)' + : (active ? '1px solid transparent' : '1px solid var(--border-light)'), + opacity: isDragging ? 0.4 : 1, cursor: 'pointer', userSelect: 'none', minHeight: 24, @@ -146,6 +163,59 @@ export default function GeschossManager({ const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id } const [addMenu, setAddMenu] = useState(null) // { x, y } — Picker beim + const [geschossDialog, setGeschossDialog] = useState(null) // { x, y, pos:'above'|'below', name, hoehe, schnitthoehe, anchorId } + // Drag-State fuer Reorder + const [dragId, setDragId] = useState(null) + const [dropOverId, setDropOverId] = useState(null) + const [dropPos, setDropPos] = useState(null) // 'top' | 'bottom' + + const handleDragStart = (ev, id) => { + setDragId(id) + try { + ev.dataTransfer.effectAllowed = 'move' + ev.dataTransfer.setData('text/plain', id) + } catch (e) {} + } + const handleDragOver = (ev, targetId) => { + if (!dragId || dragId === targetId) return + ev.preventDefault() + try { ev.dataTransfer.dropEffect = 'move' } catch (e) {} + const rect = ev.currentTarget.getBoundingClientRect() + const pos = (ev.clientY < rect.top + rect.height / 2) ? 'top' : 'bottom' + if (targetId !== dropOverId) setDropOverId(targetId) + if (pos !== dropPos) setDropPos(pos) + } + const handleDragLeave = (ev, targetId) => { + // Wenn der Cursor die ganze Row verlaesst (nicht in ein Child), reset. + // Heuristik: relatedTarget liegt nicht innerhalb der Row. + if (!ev.currentTarget.contains(ev.relatedTarget)) { + if (dropOverId === targetId) { + setDropOverId(null); setDropPos(null) + } + } + } + const handleDrop = (ev, targetId) => { + ev.preventDefault() + const srcId = dragId + setDragId(null); setDropOverId(null); setDropPos(null) + if (!srcId || srcId === targetId) return + // Array-Reihenfolge: bottom-up (recalcOkff stacks von index 0 hoch). + // Display ist umgekehrt (top = letztes Array-Element). Klick auf + // 'top' eines Display-Eintrags Z heisst: oberhalb Z, d.h. nach Z + // im Array (höherer Index). 'bottom' = unterhalb Z in Display = + // vor Z im Array. + const arr = [...zeichnungsebenen] + const srcIdx = arr.findIndex(z => z.id === srcId) + if (srcIdx < 0) return + const [moved] = arr.splice(srcIdx, 1) + const tgtIdx = arr.findIndex(z => z.id === targetId) + if (tgtIdx < 0) return + const insertIdx = (dropPos === 'top') ? tgtIdx + 1 : tgtIdx + arr.splice(insertIdx, 0, moved) + onChange(arr) // ZeichnungsebenenApp ruft recalcOkff → OKFFs stimmen wieder + } + const handleDragEnd = () => { + setDragId(null); setDropOverId(null); setDropPos(null) + } const sorted = [...zeichnungsebenen].reverse() @@ -320,6 +390,10 @@ export default function GeschossManager({ const toggleClipping = (id) => { onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, hasClipping: !z.hasClipping } : z)) + // Backend rendert die Clipping-Plane nur fuer das AKTIVE Geschoss. + // Klick auf cut-Icon einer nicht-aktiven Zeile → mit-aktivieren, + // sonst kriegt der User keine sichtbare Rueckmeldung. + if (id !== activeId) onActiveChange(id) } const duplicate = (id) => { @@ -459,6 +533,13 @@ export default function GeschossManager({ onToggleLock={() => toggleLock(z.id)} onToggleClipping={() => toggleClipping(z.id)} onDelete={() => remove(z.id)} + isDragging={dragId === z.id} + dropPos={dropOverId === z.id ? dropPos : null} + onDragStart={handleDragStart} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + onDragEnd={handleDragEnd} /> ))}
diff --git a/src/components/GeschossSettingsDialog.jsx b/src/components/GeschossSettingsDialog.jsx index fa2a986..24c9dca 100644 --- a/src/components/GeschossSettingsDialog.jsx +++ b/src/components/GeschossSettingsDialog.jsx @@ -48,7 +48,12 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe const clipZ = (okff + schnitt).toFixed(2) // Schnitt-Felder const cutAtLine = draft.cutAtLine !== false // default true = Schnitt + const projection = draft.projection || 'parallel' const depthBack = draft.depthBack ?? 8.0 + // Kamera-Hoehe (m) — nur bei Perspektive relevant. Default = Mitte + // der Hoehenrange (= klassischer "Augenpunkt-in-der-Mitte"-Look). + const camHeightDefault = ((draft.heightMin ?? -1.0) + (draft.heightMax ?? 12.0)) / 2 + const cameraHeight = draft.cameraHeight ?? camHeightDefault const heightMin = draft.heightMin ?? -1.0 const heightMax = draft.heightMax ?? 12.0 const dirSign = draft.dirSign ?? 1 @@ -172,6 +177,30 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe onClick={() => set({ dirSign: -1 })} style={{ flex: 1, fontSize: 11 }}>Seite B → + + + + + + + {projection === 'perspective' && ( + + set({ cameraHeight: parseFloat(ev.target.value) })} + style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }} + /> + + )} )} @@ -251,6 +280,13 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe if (out.heightMax == null) out.heightMax = 12.0 if (out.dirSign == null) out.dirSign = 1 if (out.cutAtLine == null) out.cutAtLine = true + if (out.projection == null) out.projection = 'parallel' + // cameraHeight: nur wenn Perspektive aktiv UND noch nicht + // gesetzt → Default aus Hoehen-Mitte. Bei Parallel das Feld + // nicht ungewollt persistieren (bleibt undefined). + if (out.projection === 'perspective' && out.cameraHeight == null) { + out.cameraHeight = ((out.heightMin ?? -1.0) + (out.heightMax ?? 12.0)) / 2 + } } onSave(out) }}>Übernehmen diff --git a/src/lib/rhinoBridge.js b/src/lib/rhinoBridge.js index c33b66c..d30f24a 100644 --- a/src/lib/rhinoBridge.js +++ b/src/lib/rhinoBridge.js @@ -181,6 +181,10 @@ export function arrangeSelection(dir) { send('ARRANGE', { dir }) } export function toggleReferenzlinien(visible) { send('TOGGLE_REFERENZLINIEN', { visible: !!visible }) } +// Snap-Toggles (Architektur-relevante Osnaps + Grid) — neue Helpers. +// toggleOrtho/toggleGridSnap/toggleOsnap existieren bereits weiter oben. +export function setOsnapMode(key, on) { send('SET_OSNAP_MODE', { key, enabled: !!on }) } +export function toggleGridVisible(on) { send('TOGGLE_GRID_VISIBLE', { visible: !!on }) } // 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 }