Initial commit — Dossier Rhino 8 Plugin
OpenStudio-Suite Architektur-Plugin fuer Rhino 8 (Mac): - Smart-Elemente: Wand, Decke, Dach (Pult/Sattel/Walm/Mansarde), Oeffnungen (Fenster/Tueren mit Rahmen + Sims + Glas + Fluegel), Treppen (gerade · L · Wendel mit Schrittmass-Validierung) - Live-Previews mit Step-Lines + Soll-Range-Clamping - Bidirektionale Selection-Sync zwischen Source-Linie und Volume - Geschoss-/Ebenen-Verwaltung mit OKFF-Persistenz - Layouts mit PDF-Export - Ausschnitte / Massstab / Override-Regeln - Petrol-Gruen Theme (Rapport-konform) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,515 @@
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import ContextMenu from './components/ContextMenu'
|
||||
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
|
||||
import {
|
||||
onMessage, notifyReady,
|
||||
listAusschnitte, saveAusschnitt, updateAusschnitt,
|
||||
restoreAusschnitt, applyAusschnittToDetail,
|
||||
renameAusschnitt, deleteAusschnitt,
|
||||
setAusschnittFolder, setAusschnittScale,
|
||||
duplicateAusschnitt, addAusschnittFolder, removeAusschnittFolder,
|
||||
getAusschnittLayers, updateAusschnittLayers,
|
||||
saveLayerPreset, deleteLayerPreset,
|
||||
} from './lib/rhinoBridge'
|
||||
|
||||
function EditableInline({ value, onCommit, autoEdit, style, fontSize }) {
|
||||
const [editing, setEditing] = useState(autoEdit || false)
|
||||
const [val, setVal] = useState(value)
|
||||
useEffect(() => { setVal(value) }, [value])
|
||||
useEffect(() => { if (autoEdit) setEditing(true) }, [autoEdit])
|
||||
|
||||
const commit = () => {
|
||||
const trimmed = (val ?? '').trim()
|
||||
if (trimmed && trimmed !== value) onCommit(trimmed)
|
||||
else setVal(value)
|
||||
setEditing(false)
|
||||
}
|
||||
if (editing) {
|
||||
return (
|
||||
<input
|
||||
value={val}
|
||||
autoFocus
|
||||
onChange={(ev) => setVal(ev.target.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter') commit()
|
||||
if (ev.key === 'Escape') { setVal(value); setEditing(false) }
|
||||
}}
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
style={{ ...style, fontSize, padding: '2px 6px' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span
|
||||
onDoubleClick={(ev) => { ev.stopPropagation(); setVal(value); setEditing(true) }}
|
||||
style={{ ...style, fontSize, cursor: 'text' }}
|
||||
>{value}</span>
|
||||
)
|
||||
}
|
||||
|
||||
function ScaleCell({ snap, onChange }) {
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [val, setVal] = useState(snap.scale || '')
|
||||
useEffect(() => { setVal(snap.scale || '') }, [snap.scale])
|
||||
|
||||
const commit = () => {
|
||||
onChange(snap.id, val.trim())
|
||||
setEditing(false)
|
||||
}
|
||||
if (editing) {
|
||||
return (
|
||||
<input
|
||||
value={val}
|
||||
autoFocus
|
||||
onChange={(ev) => setVal(ev.target.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(ev) => {
|
||||
if (ev.key === 'Enter') commit()
|
||||
if (ev.key === 'Escape') { setVal(snap.scale || ''); setEditing(false) }
|
||||
}}
|
||||
onClick={(ev) => ev.stopPropagation()}
|
||||
placeholder="1:50"
|
||||
style={{ width: 64, fontSize: 10, padding: '2px 6px', textAlign: 'right' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span
|
||||
onDoubleClick={(ev) => { ev.stopPropagation(); setEditing(true) }}
|
||||
className={snap.scale ? 'chip chip-accent' : 'chip'}
|
||||
style={{
|
||||
fontSize: 9, cursor: 'text',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
title={snap.scale ? `Maßstab ${snap.scale} — wird auf Detail angewendet` : 'Doppelklick um Maßstab einzutragen (z.B. 1:50)'}
|
||||
>{snap.scale || '—:—'}</span>
|
||||
)
|
||||
}
|
||||
|
||||
function OrientationBadge({ orientation }) {
|
||||
const variant = orientation === 'perspective' ? {
|
||||
icon: 'view_in_ar', color: 'var(--accent)',
|
||||
title: 'Perspektive',
|
||||
} : orientation === 'horizontal' ? {
|
||||
icon: 'align_horizontal_center', color: 'var(--active)',
|
||||
title: 'Horizontaler Schnitt (Grundriss)',
|
||||
} : {
|
||||
icon: 'align_vertical_center', color: 'var(--warn)',
|
||||
title: 'Vertikaler Schnitt (Schnitt / Ansicht)',
|
||||
}
|
||||
return (
|
||||
<span
|
||||
title={variant.title}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: 24, height: 24, flexShrink: 0,
|
||||
borderRadius: 999,
|
||||
background: 'var(--bg-input)',
|
||||
color: variant.color,
|
||||
}}
|
||||
>
|
||||
<Icon name={variant.icon} size={14} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function AusschnittCard({ snap, onClick, onContextMenu, onMenuClick, onRename, onScaleChange, onDragStart, onDragEnd, dragging }) {
|
||||
return (
|
||||
<div
|
||||
draggable
|
||||
onClick={onClick}
|
||||
onContextMenu={onContextMenu}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 8px',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 'var(--r)',
|
||||
background: 'var(--bg-input)',
|
||||
cursor: 'grab', userSelect: 'none',
|
||||
marginBottom: 4,
|
||||
opacity: dragging ? 0.4 : 1,
|
||||
transition: 'background 0.14s, border-color 0.14s, opacity 0.14s',
|
||||
}}
|
||||
onMouseEnter={(ev) => { ev.currentTarget.style.background = 'var(--bg-item-hover)' }}
|
||||
onMouseLeave={(ev) => { ev.currentTarget.style.background = 'var(--bg-input)' }}
|
||||
>
|
||||
<OrientationBadge orientation={snap.orientation} />
|
||||
<EditableInline
|
||||
value={snap.name}
|
||||
onCommit={(n) => onRename(snap.id, n)}
|
||||
fontSize={11}
|
||||
style={{
|
||||
flex: 1, minWidth: 0,
|
||||
color: 'var(--text-primary)', fontWeight: 500,
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
}}
|
||||
/>
|
||||
<ScaleCell snap={snap} onChange={onScaleChange} />
|
||||
<button
|
||||
className="btn-icon-sm"
|
||||
onClick={(ev) => { ev.stopPropagation(); onMenuClick(ev) }}
|
||||
title="Aktionen"
|
||||
>
|
||||
<Icon name="more_vert" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FolderCard({
|
||||
name, count, collapsed, onToggle, onContextMenu, onMenuClick,
|
||||
onDragOver, onDragLeave, onDrop, dragOver, children,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
onContextMenu={onContextMenu}
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
style={{
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
background: dragOver ? 'var(--accent-dim)' : 'var(--bg-section)',
|
||||
marginBottom: 8,
|
||||
padding: 8,
|
||||
transition: 'background 0.14s, border-color 0.14s',
|
||||
borderColor: dragOver ? 'var(--accent-border)' : 'var(--border)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={onToggle}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
cursor: 'pointer', userSelect: 'none',
|
||||
padding: 2,
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
transform: collapsed ? 'rotate(-90deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
color: 'var(--text-muted)', display: 'inline-flex',
|
||||
}}>
|
||||
<Icon name="arrow_drop_down" size={16} />
|
||||
</span>
|
||||
<Icon name="folder" size={14} style={{ color: 'var(--warn)' }} />
|
||||
<span style={{ flex: 1, fontSize: 11, fontWeight: 500, color: 'var(--text-primary)' }}>{name}</span>
|
||||
<span className="chip" style={{ fontSize: 8 }}>{count}</span>
|
||||
<button
|
||||
className="btn-icon-sm"
|
||||
onClick={(ev) => { ev.stopPropagation(); onMenuClick(ev) }}
|
||||
title="Ordner-Aktionen"
|
||||
>
|
||||
<Icon name="more_vert" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div style={{ marginTop: 6, display: 'flex', flexDirection: 'column' }}>
|
||||
{children || (
|
||||
<div style={{
|
||||
padding: '10px 6px', fontSize: 10, color: 'var(--text-muted)',
|
||||
textAlign: 'center', fontStyle: 'italic',
|
||||
}}>
|
||||
Leer — Ausschnitte hier ablegen.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RootDropZone({ children, onDragOver, onDragLeave, onDrop, dragOver, empty }) {
|
||||
return (
|
||||
<div
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
style={{
|
||||
background: dragOver ? 'var(--accent-dim)' : 'transparent',
|
||||
border: '1px dashed transparent',
|
||||
borderColor: dragOver ? 'var(--accent-border)' : 'transparent',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
padding: dragOver || empty ? 6 : 0,
|
||||
marginBottom: empty ? 0 : 8,
|
||||
transition: 'background 0.14s, border-color 0.14s, padding 0.14s',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AusschnitteApp() {
|
||||
const [snaps, setSnaps] = useState([])
|
||||
const [extraFolders, setExtraFolders] = useState([])
|
||||
const [presets, setPresets] = useState([])
|
||||
const [newName, setNewName] = useState('')
|
||||
const [ctxMenu, setCtxMenu] = useState(null)
|
||||
const [collapsed, setCollapsed] = useState({})
|
||||
const [draggingId, setDraggingId] = useState(null)
|
||||
const [dragTarget, setDragTarget] = useState(null)
|
||||
const [layerDialog, setLayerDialog] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
onMessage('LIST', ({ snapshots, folders, presets }) => {
|
||||
setSnaps(snapshots || [])
|
||||
setExtraFolders(folders || [])
|
||||
setPresets(presets || [])
|
||||
})
|
||||
onMessage('LAYERS_DATA', ({ id, name, layers, presets }) => {
|
||||
setLayerDialog({ id, name, layers: layers || [], presets: presets || [] })
|
||||
})
|
||||
notifyReady()
|
||||
const blockContext = (ev) => ev.preventDefault()
|
||||
document.addEventListener('contextmenu', blockContext)
|
||||
return () => document.removeEventListener('contextmenu', blockContext)
|
||||
}, [])
|
||||
|
||||
const groups = useMemo(() => {
|
||||
const map = {}
|
||||
snaps.forEach(s => {
|
||||
const f = s.folder || ''
|
||||
if (!map[f]) map[f] = []
|
||||
map[f].push(s)
|
||||
})
|
||||
return map
|
||||
}, [snaps])
|
||||
|
||||
const allFolders = useMemo(() => {
|
||||
const set = new Set(extraFolders)
|
||||
snaps.forEach(s => { if (s.folder) set.add(s.folder) })
|
||||
return [...set].sort((a, b) => a.localeCompare(b))
|
||||
}, [snaps, extraFolders])
|
||||
|
||||
const handleSave = () => {
|
||||
const name = newName.trim() || `Ausschnitt ${snaps.length + 1}`
|
||||
saveAusschnitt(name)
|
||||
setNewName('')
|
||||
}
|
||||
|
||||
const handleAddFolder = () => {
|
||||
const name = window.prompt('Name für neuen Ordner:')
|
||||
if (name && name.trim()) addAusschnittFolder(name.trim())
|
||||
}
|
||||
|
||||
const ctxItems = (id) => [
|
||||
{ label: 'Wiederherstellen', icon: 'restore', onClick: () => restoreAusschnitt(id) },
|
||||
{ label: 'Auf Detail anwenden', icon: 'crop_landscape', onClick: () => applyAusschnittToDetail(id) },
|
||||
{ divider: true },
|
||||
{ label: 'Sichtbarkeit bearbeiten…', icon: 'layers', onClick: () => getAusschnittLayers(id) },
|
||||
{ divider: true },
|
||||
{ label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateAusschnitt(id) },
|
||||
{ label: 'Aktualisieren', icon: 'sync', onClick: () => updateAusschnitt(id) },
|
||||
{ divider: true },
|
||||
{ label: 'Löschen', icon: 'delete', danger: true, onClick: () => deleteAusschnitt(id) },
|
||||
]
|
||||
|
||||
const folderCtxItems = (folderName) => [
|
||||
{ label: 'Ordner umbenennen', icon: 'edit', onClick: () => {
|
||||
const newName = window.prompt('Neuer Ordnername:', folderName)
|
||||
if (newName && newName.trim() && newName !== folderName) {
|
||||
snaps.filter(s => s.folder === folderName).forEach(s => setAusschnittFolder(s.id, newName.trim()))
|
||||
addAusschnittFolder(newName.trim())
|
||||
removeAusschnittFolder(folderName)
|
||||
}
|
||||
}},
|
||||
{ divider: true },
|
||||
{ label: 'Ordner löschen', icon: 'folder_off', danger: true, onClick: () => {
|
||||
if (window.confirm(`Ordner "${folderName}" löschen? Ausschnitte werden zur Wurzel verschoben.`)) {
|
||||
removeAusschnittFolder(folderName)
|
||||
}
|
||||
}},
|
||||
]
|
||||
|
||||
const handleDrop = (folderName) => (ev) => {
|
||||
ev.preventDefault()
|
||||
setDragTarget(null)
|
||||
const id = ev.dataTransfer.getData('text/plain') || draggingId
|
||||
if (id) setAusschnittFolder(id, folderName || '')
|
||||
setDraggingId(null)
|
||||
}
|
||||
|
||||
const handleDragOver = (folderName) => (ev) => {
|
||||
ev.preventDefault()
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
setDragTarget(folderName || 'root')
|
||||
}
|
||||
|
||||
const handleDragLeave = () => () => setDragTarget(null)
|
||||
|
||||
const renderSnapshot = (s) => (
|
||||
<AusschnittCard
|
||||
key={s.id}
|
||||
snap={s}
|
||||
dragging={draggingId === s.id}
|
||||
onClick={() => restoreAusschnitt(s.id)}
|
||||
onContextMenu={(ev) => { ev.preventDefault(); setCtxMenu({ x: ev.clientX, y: ev.clientY, id: s.id, kind: 'snap' }) }}
|
||||
onMenuClick={(ev) => setCtxMenu({ x: ev.clientX, y: ev.clientY, id: s.id, kind: 'snap' })}
|
||||
onRename={(id, name) => renameAusschnitt(id, name)}
|
||||
onScaleChange={(id, scale) => setAusschnittScale(id, scale)}
|
||||
onDragStart={(ev) => {
|
||||
ev.dataTransfer.setData('text/plain', s.id)
|
||||
ev.dataTransfer.effectAllowed = 'move'
|
||||
setDraggingId(s.id)
|
||||
}}
|
||||
onDragEnd={() => { setDraggingId(null); setDragTarget(null) }}
|
||||
/>
|
||||
)
|
||||
|
||||
const actions = (
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<button className="btn-icon-tonal" onClick={handleAddFolder} title="Neuer Ordner">
|
||||
<Icon name="create_new_folder" size={14} />
|
||||
</button>
|
||||
<button className="btn-icon-tonal" onClick={() => listAusschnitte()} title="Aktualisieren">
|
||||
<Icon name="refresh" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const rootItems = groups[''] || []
|
||||
const isEmpty = snaps.length === 0 && allFolders.length === 0
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
height: '100vh', overflow: 'hidden',
|
||||
background: 'var(--bg-base)',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{/* Fixed Header — wie Layouts/Overrides Pattern */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 10px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<span style={{ flex: 1, fontWeight: 600, letterSpacing: '0.08em',
|
||||
color: 'var(--text-primary)' }}>
|
||||
AUSSCHNITTE
|
||||
</span>
|
||||
<span className="chip" style={{ fontSize: 8 }}>{snaps.length}</span>
|
||||
{actions}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
|
||||
{/* Save-Bar als Card */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: 8,
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
background: 'var(--bg-section)',
|
||||
marginBottom: 8,
|
||||
marginTop: 6,
|
||||
}}>
|
||||
<input
|
||||
value={newName}
|
||||
onChange={(ev) => setNewName(ev.target.value)}
|
||||
onKeyDown={(ev) => { if (ev.key === 'Enter') handleSave() }}
|
||||
placeholder="Name für neuen Ausschnitt…"
|
||||
style={{ flex: 1, fontSize: 11, fontFamily: 'var(--font)', minWidth: 0 }}
|
||||
/>
|
||||
<button className="btn-add" onClick={handleSave} title="Ausschnitt speichern">
|
||||
<Icon name="add" size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isEmpty ? (
|
||||
<div style={{
|
||||
padding: '32px 16px', textAlign: 'center',
|
||||
color: 'var(--text-muted)', fontSize: 11,
|
||||
border: '1px dashed var(--border)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<Icon name="photo_library" size={32} style={{ color: 'var(--text-muted)', opacity: 0.5 }} />
|
||||
<div style={{ marginTop: 8 }}>Noch keine Ausschnitte.</div>
|
||||
<div style={{ marginTop: 4, fontSize: 10 }}>Oben einen Namen eingeben und <Icon name="add" size={11} /> klicken.</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Root-Snapshots */}
|
||||
<RootDropZone
|
||||
dragOver={dragTarget === 'root'}
|
||||
empty={rootItems.length === 0}
|
||||
onDragOver={handleDragOver('')}
|
||||
onDragLeave={handleDragLeave()}
|
||||
onDrop={handleDrop('')}
|
||||
>
|
||||
{rootItems.map(s => renderSnapshot(s))}
|
||||
{rootItems.length === 0 && draggingId && (
|
||||
<div style={{
|
||||
padding: '8px 14px', fontSize: 10, color: 'var(--text-muted)',
|
||||
textAlign: 'center', fontStyle: 'italic',
|
||||
}}>Hier ablegen für Wurzel</div>
|
||||
)}
|
||||
</RootDropZone>
|
||||
|
||||
{/* Ordner-Cards */}
|
||||
{allFolders.map(folder => {
|
||||
const isCollapsed = !!collapsed[folder]
|
||||
const items = groups[folder] || []
|
||||
return (
|
||||
<FolderCard
|
||||
key={folder}
|
||||
name={folder}
|
||||
count={items.length}
|
||||
collapsed={isCollapsed}
|
||||
dragOver={dragTarget === folder}
|
||||
onToggle={() => setCollapsed(c => ({ ...c, [folder]: !c[folder] }))}
|
||||
onContextMenu={(ev) => { ev.preventDefault(); setCtxMenu({ x: ev.clientX, y: ev.clientY, name: folder, kind: 'folder' }) }}
|
||||
onMenuClick={(ev) => setCtxMenu({ x: ev.clientX, y: ev.clientY, name: folder, kind: 'folder' })}
|
||||
onDragOver={handleDragOver(folder)}
|
||||
onDragLeave={handleDragLeave()}
|
||||
onDrop={handleDrop(folder)}
|
||||
>
|
||||
{items.length > 0 ? items.map(s => renderSnapshot(s)) : null}
|
||||
</FolderCard>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
<div style={{ padding: '12px 4px 0', fontSize: 9, color: 'var(--text-muted)',
|
||||
lineHeight: 1.6, fontStyle: 'italic' }}>
|
||||
Drag & Drop auf Ordner-Card zum Verschieben · Doppelklick auf Name/Maßstab = bearbeiten · ⋮ für Aktionen
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ctxMenu && (
|
||||
<ContextMenu
|
||||
x={ctxMenu.x} y={ctxMenu.y}
|
||||
items={ctxMenu.kind === 'folder' ? folderCtxItems(ctxMenu.name) : ctxItems(ctxMenu.id)}
|
||||
onClose={() => setCtxMenu(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{layerDialog && (
|
||||
<AusschnittLayerDialog
|
||||
snapName={layerDialog.name}
|
||||
layers={layerDialog.layers}
|
||||
presets={layerDialog.presets}
|
||||
onSave={(layers) => {
|
||||
updateAusschnittLayers(layerDialog.id,
|
||||
layers.map(l => ({ id: l.id, visible: l.visible, locked: l.locked })))
|
||||
setLayerDialog(null)
|
||||
}}
|
||||
onClose={() => setLayerDialog(null)}
|
||||
onSavePreset={(name, layers) => {
|
||||
saveLayerPreset(name, layers)
|
||||
setLayerDialog(d => d ? { ...d, presets: [...d.presets.filter(p => p.name !== name), { name, layers }] } : d)
|
||||
}}
|
||||
onDeletePreset={(name) => {
|
||||
deleteLayerPreset(name)
|
||||
setLayerDialog(d => d ? { ...d, presets: d.presets.filter(p => p.name !== name) } : d)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user