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:
@@ -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 (
|
||||
<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}
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user