Snap-Bar + Drag-Reorder + Schnittperspektive + Top-View Z-Guard + Multi-Geschoss-Clipping
Snap-Bar (Oberleiste): - 4x2 Icon-Grid mit architektonischen Osnap-Modi (End/Mid/Int/Perp/Cen/Near) - Master-O Toggle + Grid-Sichtbarkeit - Symbol-Wahl angelehnt an Rhinos eigene Snap-Marker - Ortho + Grid-Snap raus (in Rhino-Footer) - Backend: _osnap_flag_map + _get/_set_osnap_modes + _set_grid_visible Drag-to-Reorder (GeschossManager): - HTML5-Drag auf jeder Zeile - Drop-Indikator (accent-Border oben/unten je nach Cursor-Position) - Gedraggte Row faded auf opacity 0.4 - Array-Reorder + onChange triggert recalcOkff -> OKFFs konsistent Schnittperspektive: - projection: 'parallel' | 'perspective' im Schnitt-Settings-Dialog - Augenhoehe (cameraHeight) nur bei Perspektive sichtbar - activate_schnitt mit ChangeToPerspectiveProjection(50 FOV) - skip_view=True bei Grip-Drag-Re-Activate damit View nicht ploetzlich in Section springt Top-View Z-Guard: - _is_active_view_top_like + _suppress_z_drift_if_top_view in _on_object_replaced — bei Plan-View wird Z-Drift einer source-curve automatisch zurueckgerollt (gegen ungewolltes Snappen auf z!=0 oder Gumball-Z) Multi-Geschoss-Clipping-UX: - Klick auf cut-Icon einer nicht-aktiven Geschoss-Zeile aktiviert das Geschoss mit + toggelt Clipping → Plane erscheint sofort Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -4815,6 +4815,7 @@ def _make_treppe_volume(axis_curve, breite, referenz, n_stufen, uk, ok,
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _regenerate_element(doc, element_id):
|
def _regenerate_element(doc, element_id):
|
||||||
"""Regeneriert das Volumen eines Elements (Wand oder Decke) anhand
|
"""Regeneriert das Volumen eines Elements (Wand oder Decke) anhand
|
||||||
seines Source-Objekts (Achse bzw. Outline)."""
|
seines Source-Objekts (Achse bzw. Outline)."""
|
||||||
@@ -9733,6 +9734,81 @@ def _apply_oeffnung_constraint(new_obj, meta, old_obj=None):
|
|||||||
return geom_changed
|
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):
|
def _on_object_replaced(sender, e):
|
||||||
"""Wenn eine Source (Wand-Achse/Decke-Outline/etc.) veraendert wird
|
"""Wenn eine Source (Wand-Achse/Decke-Outline/etc.) veraendert wird
|
||||||
→ Regeneration queuen (debounct ueber Idle, 50 ms Ruhe).
|
→ Regeneration queuen (debounct ueber Idle, 50 ms Ruhe).
|
||||||
@@ -9792,6 +9868,14 @@ def _on_object_replaced_body(sender, e):
|
|||||||
except Exception: pass
|
except Exception: pass
|
||||||
if meta is None or meta.get("type") not in SOURCE_TYPES:
|
if meta is None or meta.get("type") not in SOURCE_TYPES:
|
||||||
return
|
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:
|
try:
|
||||||
new_obj = e.NewRhinoObject
|
new_obj = e.NewRhinoObject
|
||||||
if new_obj and not _read_meta(new_obj):
|
if new_obj and not _read_meta(new_obj):
|
||||||
|
|||||||
+96
-10
@@ -722,17 +722,95 @@ def _set_display_mode(name):
|
|||||||
|
|
||||||
# --- Snap / Ortho via ModelAidSettings --------------------------------------
|
# --- 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():
|
def _get_snap_state():
|
||||||
try:
|
try:
|
||||||
s = Rhino.ApplicationSettings.ModelAidSettings
|
s = Rhino.ApplicationSettings.ModelAidSettings
|
||||||
return {
|
return {
|
||||||
"ortho": bool(s.Ortho),
|
"ortho": bool(s.Ortho),
|
||||||
"gridSnap": bool(s.GridSnap),
|
"gridSnap": bool(s.GridSnap),
|
||||||
"osnap": bool(s.UseHorizontalDialog) if False else bool(getattr(s, "Osnap", False)) or False,
|
"osnap": bool(getattr(s, "Osnap", False)) or bool(getattr(s, "OsnapEnabled", False)),
|
||||||
"planar": bool(getattr(s, "ProjectOsnapsToCPlane", False)),
|
"planar": bool(getattr(s, "ProjectOsnapsToCPlane", False)),
|
||||||
|
"gridVisible": _is_grid_visible(),
|
||||||
|
"osnapModes": _get_osnap_modes_dict(),
|
||||||
}
|
}
|
||||||
except Exception:
|
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):
|
def _set_ortho(v):
|
||||||
@@ -753,11 +831,11 @@ def _set_osnap_master(v):
|
|||||||
"""Master-Toggle fuer Object-Snap (alle aktiven Snaps)."""
|
"""Master-Toggle fuer Object-Snap (alle aktiven Snaps)."""
|
||||||
try:
|
try:
|
||||||
s = Rhino.ApplicationSettings.ModelAidSettings
|
s = Rhino.ApplicationSettings.ModelAidSettings
|
||||||
if hasattr(s, "Osnap"):
|
# Verschiedene Rhino-Versionen — beide Properties probieren
|
||||||
s.Osnap = bool(v)
|
for attr in ("Osnap", "OsnapEnabled"):
|
||||||
elif hasattr(s, "UsePoints"):
|
if hasattr(s, attr):
|
||||||
# Fallback: einzelne Modi durch
|
setattr(s, attr, bool(v))
|
||||||
pass
|
return
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[OBERLEISTE] _set_osnap_master:", ex)
|
print("[OBERLEISTE] _set_osnap_master:", ex)
|
||||||
|
|
||||||
@@ -1018,6 +1096,12 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
elif t == "TOGGLE_OSNAP":
|
elif t == "TOGGLE_OSNAP":
|
||||||
_set_osnap_master(bool(p.get("enabled")))
|
_set_osnap_master(bool(p.get("enabled")))
|
||||||
self._send_state(force=True)
|
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 ----------------------------------------
|
# --- Graphical Overrides ----------------------------------------
|
||||||
elif t == "TOGGLE_OVERRIDES":
|
elif t == "TOGGLE_OVERRIDES":
|
||||||
@@ -1464,6 +1548,8 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
info.get("viewMode"),
|
info.get("viewMode"),
|
||||||
info.get("displayMode"),
|
info.get("displayMode"),
|
||||||
info.get("ortho"), info.get("gridSnap"), info.get("osnap"),
|
info.get("ortho"), info.get("gridSnap"), info.get("osnap"),
|
||||||
|
info.get("gridVisible"),
|
||||||
|
tuple(sorted((info.get("osnapModes") or {}).items())),
|
||||||
info.get("showLineweights"),
|
info.get("showLineweights"),
|
||||||
info["overridesEnabled"], info["overridesCount"],
|
info["overridesEnabled"], info["overridesCount"],
|
||||||
info.get("overridesActivePreset"),
|
info.get("overridesActivePreset"),
|
||||||
|
|||||||
+31
-6
@@ -247,8 +247,23 @@ def activate_schnitt(doc, z, skip_view=False):
|
|||||||
obj = _add_clipping_plane(doc, back_plane, du, dv, vp_ids, "back")
|
obj = _add_clipping_plane(doc, back_plane, du, dv, vp_ids, "back")
|
||||||
if obj is not None: n_planes += 1
|
if obj is not None: n_planes += 1
|
||||||
|
|
||||||
# View setzen: Parallel-Projektion, Kamera senkrecht zur Linie.
|
# Projektion: 'parallel' (klassischer Schnitt) oder 'perspective'
|
||||||
# Bei skip_view=True (Grip-Drag-Re-Activate) komplett ueberspringen.
|
# (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:
|
if not skip_view:
|
||||||
try:
|
try:
|
||||||
view = doc.Views.ActiveView
|
view = doc.Views.ActiveView
|
||||||
@@ -257,18 +272,28 @@ def activate_schnitt(doc, z, skip_view=False):
|
|||||||
if view is not None:
|
if view is not None:
|
||||||
vp = view.ActiveViewport
|
vp = view.ActiveViewport
|
||||||
cam_dist = max(50.0, depth_back * 3 + line_len)
|
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(
|
cam_pos = rg.Point3d(
|
||||||
mid.X - view_dir.X * cam_dist,
|
mid.X - view_dir.X * cam_dist,
|
||||||
mid.Y - view_dir.Y * cam_dist,
|
mid.Y - view_dir.Y * cam_dist,
|
||||||
plane_z)
|
view_z)
|
||||||
target = rg.Point3d(
|
target = rg.Point3d(
|
||||||
mid.X + view_dir.X * (depth_back * 0.5),
|
mid.X + view_dir.X * (depth_back * 0.5),
|
||||||
mid.Y + view_dir.Y * (depth_back * 0.5),
|
mid.Y + view_dir.Y * (depth_back * 0.5),
|
||||||
plane_z)
|
view_z)
|
||||||
vp.ChangeToParallelProjection(True)
|
if projection == "perspective":
|
||||||
|
vp.ChangeToPerspectiveProjection(True, 50.0)
|
||||||
|
else:
|
||||||
|
vp.ChangeToParallelProjection(True)
|
||||||
vp.SetCameraLocations(target, cam_pos)
|
vp.SetCameraLocations(target, cam_pos)
|
||||||
vp.CameraUp = rg.Vector3d(0, 0, 1)
|
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(
|
bb = rg.BoundingBox(
|
||||||
rg.Point3d(min(p1.X, p2.X) - margin, min(p1.Y, p2.Y) - margin,
|
rg.Point3d(min(p1.X, p2.X) - margin, min(p1.Y, p2.Y) - margin,
|
||||||
h_min - margin),
|
h_min - margin),
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
setDarstellung,
|
setDarstellung,
|
||||||
arrangeSelection,
|
arrangeSelection,
|
||||||
toggleReferenzlinien,
|
toggleReferenzlinien,
|
||||||
|
toggleOsnap,
|
||||||
|
setOsnapMode, toggleGridVisible,
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
const PRESETS = [
|
const PRESETS = [
|
||||||
@@ -706,6 +708,97 @@ export default function OberleisteApp() {
|
|||||||
|
|
||||||
<div style={sep} />
|
<div style={sep} />
|
||||||
|
|
||||||
|
{/* ====== 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 }) => (
|
||||||
|
<button onClick={onClick} disabled={disabled} title={title}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (disabled || active) return
|
||||||
|
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||||
|
e.currentTarget.style.color = 'var(--accent-light)'
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (active) return
|
||||||
|
e.currentTarget.style.background = 'var(--bg-input)'
|
||||||
|
e.currentTarget.style.color = 'var(--text-primary)'
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
height: BAR_H, width: BAR_H, padding: 0,
|
||||||
|
background: active ? 'var(--accent)' : 'var(--bg-input)',
|
||||||
|
color: active ? 'var(--bg-panel)' : 'var(--text-primary)',
|
||||||
|
border: 'none',
|
||||||
|
borderLeft: isFirst ? 'none' : '1px solid var(--border)',
|
||||||
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
|
opacity: disabled ? 0.35 : 1,
|
||||||
|
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
appearance: 'none', WebkitAppearance: 'none',
|
||||||
|
lineHeight: 1, boxSizing: 'border-box', flexShrink: 0,
|
||||||
|
transition: 'background 0.15s, color 0.15s',
|
||||||
|
}}>
|
||||||
|
<Icon name={icon} size={12} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
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 (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', flexDirection: 'column', gap: 4, flexShrink: 0,
|
||||||
|
}}>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<IconBtn icon="gps_fixed" isFirst
|
||||||
|
active={!!state.osnap}
|
||||||
|
onClick={() => toggleOsnap(!state.osnap)}
|
||||||
|
title={state.osnap ? 'Object-Snap an' : 'Object-Snap aus'} />
|
||||||
|
<IconBtn icon="crop_square" disabled={osnapDisabled}
|
||||||
|
active={!!om.end}
|
||||||
|
onClick={() => setOsnapMode('end', !om.end)}
|
||||||
|
title="Endpunkt (End)" />
|
||||||
|
<IconBtn icon="change_history" disabled={osnapDisabled}
|
||||||
|
active={!!om.mid}
|
||||||
|
onClick={() => setOsnapMode('mid', !om.mid)}
|
||||||
|
title="Mittelpunkt (Mid)" />
|
||||||
|
<IconBtn icon="close" disabled={osnapDisabled}
|
||||||
|
active={!!om.int}
|
||||||
|
onClick={() => setOsnapMode('int', !om.int)}
|
||||||
|
title="Schnittpunkt (Intersection)" />
|
||||||
|
</div>
|
||||||
|
<div style={rowStyle}>
|
||||||
|
<IconBtn icon="square_foot" isFirst disabled={osnapDisabled}
|
||||||
|
active={!!om.perp}
|
||||||
|
onClick={() => setOsnapMode('perp', !om.perp)}
|
||||||
|
title="Lotrecht (Perpendicular)" />
|
||||||
|
<IconBtn icon="radio_button_unchecked" disabled={osnapDisabled}
|
||||||
|
active={!!om.cen}
|
||||||
|
onClick={() => setOsnapMode('cen', !om.cen)}
|
||||||
|
title="Kreis-/Bogen-Mittelpunkt (Center)" />
|
||||||
|
<IconBtn icon="add" disabled={osnapDisabled}
|
||||||
|
active={!!om.near}
|
||||||
|
onClick={() => setOsnapMode('near', !om.near)}
|
||||||
|
title="Naechster Punkt (Near)" />
|
||||||
|
<IconBtn
|
||||||
|
icon={state.gridVisible === false ? 'grid_off' : 'grid_on'}
|
||||||
|
active={state.gridVisible !== false}
|
||||||
|
onClick={() => toggleGridVisible(state.gridVisible === false)}
|
||||||
|
title="Konstruktions-Raster ein-/ausblenden" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
|
||||||
|
<div style={sep} />
|
||||||
|
|
||||||
{/* ====== TEXT-Block (Vectorworks-Stil) ======
|
{/* ====== TEXT-Block (Vectorworks-Stil) ======
|
||||||
Reihe 1: Style ▼ | Font ▼ | Size ▼
|
Reihe 1: Style ▼ | Font ▼ | Size ▼
|
||||||
Reihe 2: [B][I][U] | [L][C][R] | [+]
|
Reihe 2: [B][I][U] | [L][C][R] | [+]
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ function GeschossBadge({ name }) {
|
|||||||
function ZeichnungsebeneRow({
|
function ZeichnungsebeneRow({
|
||||||
z, active, mode, onClick, onContextMenu,
|
z, active, mode, onClick, onContextMenu,
|
||||||
onToggleVisible, onToggleLock, onToggleClipping, onDelete,
|
onToggleVisible, onToggleLock, onToggleClipping, onDelete,
|
||||||
|
onDragStart, onDragOver, onDragLeave, onDrop, onDragEnd,
|
||||||
|
isDragging, dropPos,
|
||||||
}) {
|
}) {
|
||||||
const isGeschoss = !!z.isGeschoss
|
const isGeschoss = !!z.isGeschoss
|
||||||
const isSchnitt = z.type === 'schnitt'
|
const isSchnitt = z.type === 'schnitt'
|
||||||
@@ -48,6 +50,12 @@ function ZeichnungsebeneRow({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
draggable
|
||||||
|
onDragStart={(ev) => 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}
|
onClick={onClick}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
style={{
|
style={{
|
||||||
@@ -56,7 +64,16 @@ function ZeichnungsebeneRow({
|
|||||||
margin: 0,
|
margin: 0,
|
||||||
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
|
background: active ? 'var(--active-dim)' : 'var(--bg-item)',
|
||||||
borderRadius: active ? 999 : 0,
|
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',
|
cursor: 'pointer',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
minHeight: 24,
|
minHeight: 24,
|
||||||
@@ -146,6 +163,59 @@ export default function GeschossManager({
|
|||||||
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
|
const [ctxMenu, setCtxMenu] = useState(null) // { x, y, id }
|
||||||
const [addMenu, setAddMenu] = useState(null) // { x, y } — Picker beim +
|
const [addMenu, setAddMenu] = useState(null) // { x, y } — Picker beim +
|
||||||
const [geschossDialog, setGeschossDialog] = useState(null) // { x, y, pos:'above'|'below', name, hoehe, schnitthoehe, anchorId }
|
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()
|
const sorted = [...zeichnungsebenen].reverse()
|
||||||
|
|
||||||
@@ -320,6 +390,10 @@ export default function GeschossManager({
|
|||||||
|
|
||||||
const toggleClipping = (id) => {
|
const toggleClipping = (id) => {
|
||||||
onChange(zeichnungsebenen.map(z => z.id === id ? { ...z, hasClipping: !z.hasClipping } : z))
|
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) => {
|
const duplicate = (id) => {
|
||||||
@@ -459,6 +533,13 @@ export default function GeschossManager({
|
|||||||
onToggleLock={() => toggleLock(z.id)}
|
onToggleLock={() => toggleLock(z.id)}
|
||||||
onToggleClipping={() => toggleClipping(z.id)}
|
onToggleClipping={() => toggleClipping(z.id)}
|
||||||
onDelete={() => remove(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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,7 +48,12 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
|||||||
const clipZ = (okff + schnitt).toFixed(2)
|
const clipZ = (okff + schnitt).toFixed(2)
|
||||||
// Schnitt-Felder
|
// Schnitt-Felder
|
||||||
const cutAtLine = draft.cutAtLine !== false // default true = Schnitt
|
const cutAtLine = draft.cutAtLine !== false // default true = Schnitt
|
||||||
|
const projection = draft.projection || 'parallel'
|
||||||
const depthBack = draft.depthBack ?? 8.0
|
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 heightMin = draft.heightMin ?? -1.0
|
||||||
const heightMax = draft.heightMax ?? 12.0
|
const heightMax = draft.heightMax ?? 12.0
|
||||||
const dirSign = draft.dirSign ?? 1
|
const dirSign = draft.dirSign ?? 1
|
||||||
@@ -172,6 +177,30 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
|||||||
onClick={() => set({ dirSign: -1 })}
|
onClick={() => set({ dirSign: -1 })}
|
||||||
style={{ flex: 1, fontSize: 11 }}>Seite B →</button>
|
style={{ flex: 1, fontSize: 11 }}>Seite B →</button>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field label="PROJEKTION"
|
||||||
|
hint={projection === 'perspective'
|
||||||
|
? 'Schnittperspektive — perspektivische Section mit gleichem Clipping. Cutaway-Visualisierung.'
|
||||||
|
: 'Klassischer Schnitt — Parallelprojektion, masstabsgetreu.'}>
|
||||||
|
<button className={projection === 'parallel' ? 'btn-contained' : 'btn-outlined'}
|
||||||
|
onClick={() => set({ projection: 'parallel' })}
|
||||||
|
style={{ flex: 1, fontSize: 11 }}>Parallel</button>
|
||||||
|
<button className={projection === 'perspective' ? 'btn-contained' : 'btn-outlined'}
|
||||||
|
onClick={() => set({ projection: 'perspective' })}
|
||||||
|
style={{ flex: 1, fontSize: 11 }}>Perspektive</button>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{projection === 'perspective' && (
|
||||||
|
<Field label="AUGENHÖHE (m)"
|
||||||
|
hint="Z-Höhe von Kamera und Blickziel. 1.60 = Augenhöhe (klassische Schnittperspektive). 0 = Bodennivau. Höher = Bird's-Eye.">
|
||||||
|
<input
|
||||||
|
type="number" step="0.1"
|
||||||
|
value={cameraHeight}
|
||||||
|
onChange={(ev) => set({ cameraHeight: parseFloat(ev.target.value) })}
|
||||||
|
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -251,6 +280,13 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
|||||||
if (out.heightMax == null) out.heightMax = 12.0
|
if (out.heightMax == null) out.heightMax = 12.0
|
||||||
if (out.dirSign == null) out.dirSign = 1
|
if (out.dirSign == null) out.dirSign = 1
|
||||||
if (out.cutAtLine == null) out.cutAtLine = true
|
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)
|
onSave(out)
|
||||||
}}>Übernehmen</button>
|
}}>Übernehmen</button>
|
||||||
|
|||||||
@@ -181,6 +181,10 @@ export function arrangeSelection(dir) { send('ARRANGE', { dir }) }
|
|||||||
export function toggleReferenzlinien(visible) {
|
export function toggleReferenzlinien(visible) {
|
||||||
send('TOGGLE_REFERENZLINIEN', { visible: !!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
|
// Schnitt/Ansicht — interaktiver 2-Punkt-Pick im Rhino-Viewport. Erzeugt
|
||||||
// eine neue Zeichnungsebene type=schnitt + 2D-Plan-Symbol + aktiviert sie.
|
// eine neue Zeichnungsebene type=schnitt + 2D-Plan-Symbol + aktiviert sie.
|
||||||
// opts: { cutAtLine: bool, depthBack: m, heightMin: m, heightMax: m, namePrefix }
|
// opts: { cutAtLine: bool, depthBack: m, heightMin: m, heightMax: m, namePrefix }
|
||||||
|
|||||||
Reference in New Issue
Block a user