Oeffnungen-Sublayer + Sturzlinien + Referenz-Layer + Pill-Inputs + Anordnen-Pill

- Oeffnungen-Subtree (Rahmen/Glas/Tuerblatt/Sims/Pane/Schwung/Sturz) als
  nested Children unter WAENDE im dossier_ebenen-Tree registriert + per-Kind
  Material (Glas mit Transparenz)
- Sturzlinien bei 1:100 Tueren mit Innen/Aussen/Beide/Keine-Dropdown
- Referenzlinien-Layer (19) als eigene Ebene fuer wand_axis + oeffnung_point
- Swisstopo Patch-Terrain (Brep.CreatePatch) ersetzt das falsche Loft
- Pill-Style fuer alle Inputs zentral via index.css
- 2x2 Anordnen-Pill in der Oberleiste (BringToFront/Forward/Backward/SendToBack
  via Rhinos DisplayOrder, kein Z-Offset)
- Chevron-Verschiebung in Ebenen-Panel ohne dass Siblings shiften
- Fix: _update_ebene_field walked nur Top-Level, nested Sublayer-Style-
  Changes wurden nicht persistiert
- Fix: Sturz-Linetype wurde bei jedem Wand-Regen zurueckgesetzt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 16:07:44 +02:00
parent 3dc6e31374
commit 3277f61ced
12 changed files with 803 additions and 80 deletions
+2 -2
View File
@@ -90,13 +90,13 @@ export default function AusschnittSettingsApp() {
</Field>
<Field label="DARSTELLUNG"
hint="SIA-400 Detaillierungsgrad — leer = per-Element-Setting respektieren">
hint="SIA-400 Detaillierungsgrad fuer diesen Ausschnitt — leer = beim Restore nicht aendern">
<select
value={snap.darstellung || ''}
onChange={(ev) => set({ darstellung: ev.target.value })}
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
>
<option value=""> per Element </option>
<option value=""> nicht aendern </option>
<option value="einfach">Einfach (1:100)</option>
<option value="standard">Standard (1:50)</option>
<option value="detail">Detail (1:20)</option>
+10 -3
View File
@@ -22,6 +22,8 @@ function fmtNum(v) {
// Input-Komponente: zeigt formatierten Wert, sendet onCommit bei Enter/Blur.
// Verhindert Update waehrend des Tippens, damit der Cursor nicht springt.
// Pill-Chrome (Border, Radius, bg-input) kommt aus dem globalen CSS —
// hier nur der Flex-Container fuer Input + optionalen Suffix.
function NumInput({ value, onCommit, disabled, suffix, width }) {
const [text, setText] = useState(fmtNum(value))
const [focused, setFocused] = useState(false)
@@ -32,7 +34,8 @@ function NumInput({ value, onCommit, disabled, suffix, width }) {
else setText(fmtNum(value)) // ungueltig → zurueck auf alten Wert
}
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, flex: width ? 0 : 1, width }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4,
flex: width ? 0 : 1, width }}>
<input
type="text"
value={text}
@@ -44,9 +47,13 @@ function NumInput({ value, onCommit, disabled, suffix, width }) {
if (e.key === 'Enter') { e.target.blur() }
else if (e.key === 'Escape') { setText(fmtNum(value)); e.target.blur() }
}}
style={{ flex: 1, width: '100%', fontFamily: 'DM Mono, monospace', fontSize: 11, textAlign: 'right' }}
style={{ flex: 1, width: '100%', textAlign: 'right' }}
/>
{suffix && <span style={{ fontSize: 10, color: 'var(--text-muted)', flexShrink: 0 }}>{suffix}</span>}
{suffix && (
<span style={{ fontSize: 10, color: 'var(--text-muted)', flexShrink: 0 }}>
{suffix}
</span>
)}
</div>
)
}
+21 -2
View File
@@ -1777,9 +1777,10 @@ function OeffnungProperties({ oeff, onUpdate, onDelete, oeffStyles = [] }) {
</span>
<div style={{ flex: 1, display: 'flex' }}>
<BarCombo
value={oeff.darstellung || 'standard'}
value={oeff.darstellung || 'auto'}
onChange={(v) => onUpdate({ darstellung: v })}
title="Detaillierungsgrad — beeinflusst die generierte Geometrie">
title="Detaillierungsgrad — Auto folgt der Modelldarstellung in der Topbar">
<option value="auto">Nach Modelldarstellung</option>
<option value="einfach">Einfach (1:100)</option>
<option value="standard">Standard (1:50)</option>
<option value="detail">Detail (1:20)</option>
@@ -1874,6 +1875,24 @@ function OeffnungProperties({ oeff, onUpdate, onDelete, oeffStyles = [] }) {
</div>
)}
{!isFenster && (oeff.tuerTyp || 'normal') === 'normal' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}
title="Sturzlinien-Anzeige bei 1:100 (gestrichelt). Aussen = Linie an Wand-Aussenseite, Innen = Wand-Innenseite, Beide = beide Linien">
Sturz
</span>
<select
value={oeff.sturz || 'beide'}
onChange={(e) => onUpdate({ sturz: e.target.value })}
style={{ flex: 1, fontSize: 11 }}>
<option value="keine">Keine</option>
<option value="innen">Innen</option>
<option value="aussen">Aussen</option>
<option value="beide">Beide</option>
</select>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ fontSize: 10, color: 'var(--text-secondary)', width: 50 }}>Breite</span>
<input type="text" value={breite}
+62 -3
View File
@@ -15,6 +15,7 @@ import {
openAbout, createText, setTextSettings,
applyTextStyle, saveTextStyle, deleteTextStyle,
setDarstellung,
arrangeSelection,
} from './lib/rhinoBridge'
const PRESETS = [
@@ -403,12 +404,11 @@ export default function OberleisteApp() {
{/* Reihe 1, Spalte 2: Modelldarstellung (SIA-400 LoD) */}
<BarCombo
icon="tune"
value={state.aktiveDarstellung || ''}
value={state.aktiveDarstellung || 'einfach'}
onChange={(v) => setDarstellung(v)}
title="Darstellungs-Override fuer Fenster/Tueren (SIA-400 LoD)"
title="Modelldarstellung — Default fuer Fenster/Tueren auf 'Auto'. Einzelobjekt-Override im Properties-Panel."
width={PRESET_W}
>
<option value=""> per Element </option>
<option value="einfach">Einfach (1:100)</option>
<option value="standard">Standard (1:50)</option>
<option value="detail">Detail (1:20)</option>
@@ -631,6 +631,65 @@ export default function OberleisteApp() {
<div style={sep} />
{/* ====== ANORDNEN (2D-Z-Stack via Rhino-DisplayOrder) ======
2x2-Grid (quadratisch): oben Aufwaerts-Aktionen, unten Abwaerts.
Reihe 1: Vorderste | 1 hoch (vertical_align_top, expand_less)
Reihe 2: 1 runter | Hinterste (expand_more, vertical_align_bottom)
Selection-Check + Rhino-DisplayOrder im Backend, keine Z-Offsets. */}
{(() => {
const CELL = 26 // quadratisch: 2 * CELL Breite, ~2 * BAR_H Hoehe
const Btn = ({ icon, dir, title, isFirst }) => (
<button onClick={() => arrangeSelection(dir)} title={title}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--bg-item-hover)'
e.currentTarget.style.color = 'var(--accent-light)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'var(--bg-input)'
e.currentTarget.style.color = 'var(--text-primary)'
}}
style={{
height: BAR_H, minHeight: BAR_H, maxHeight: BAR_H, width: CELL,
background: 'var(--bg-input)', color: 'var(--text-primary)',
border: 'none',
borderLeft: isFirst ? 'none' : '1px solid var(--border)',
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', padding: 0, flexShrink: 0,
appearance: 'none', WebkitAppearance: 'none',
lineHeight: 1, boxSizing: 'border-box',
transition: 'background 0.15s, color 0.15s',
}}>
<Icon name={icon} size={11} />
</button>
)
const rowStyle = {
display: 'inline-flex', width: CELL * 2,
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}>
<Btn icon="vertical_align_top" dir="front" isFirst
title="In den Vordergrund (Bring to Front)" />
<Btn icon="expand_less" dir="forward"
title="Eine Stufe hoch (Bring Forward)" />
</div>
<div style={rowStyle}>
<Btn icon="expand_more" dir="backward" isFirst
title="Eine Stufe runter (Send Backward)" />
<Btn icon="vertical_align_bottom" dir="back"
title="In den Hintergrund (Send to Back)" />
</div>
</div>
)
})()}
<div style={sep} />
{/* ====== TEXT-Block (Vectorworks-Stil) ======
Reihe 1: Style ▼ | Font ▼ | Size ▼
Reihe 2: [B][I][U] | [L][C][R] | [+]
+13 -2
View File
@@ -64,6 +64,7 @@ export default function SwisstopoApp() {
const [getContours, setGetContours] = useState(false)
const [getContourTin,setGetContourTin]= useState(false)
const [getContourSchicht, setGetContourSchicht] = useState(false)
const [getContourPatch, setGetContourPatch] = useState(false)
const [contourInt, setContourInt] = useState('2.0')
// TLM3D deaktiviert: swisstopo liefert nur GDB/SHP/GPKG — kein DXF.
// Rhino kann das nicht nativ importieren; OSM-Importer ist die Alternative
@@ -134,7 +135,7 @@ export default function SwisstopoApp() {
const handleImport = () => {
if (!center) { addLog('Bitte zuerst einen Standort wählen'); return }
if (!getBuild && !getTerrain && !getContours && !getContourTin && !getContourSchicht && !getTlm) {
if (!getBuild && !getTerrain && !getContours && !getContourTin && !getContourSchicht && !getContourPatch && !getTlm) {
addLog('Mindestens eine Datenquelle wählen'); return
}
setLogs([])
@@ -147,6 +148,7 @@ export default function SwisstopoApp() {
if (getContours) kinds.push('contours')
if (getContourTin) kinds.push('contour_tin')
if (getContourSchicht)kinds.push('contour_schicht')
if (getContourPatch) kinds.push('contour_patch')
if (getTlm) kinds.push('tlm')
const tlmList = Object.entries(tlmKinds).filter(([, v]) => v).map(([k]) => k)
send('RUN_IMPORT', {
@@ -344,7 +346,16 @@ export default function SwisstopoApp() {
<Icon name="stacks" size={13} /> Schichtenmodell aus Höhenlinien
</label>
</Field>
{(getContours || getContourTin || getContourSchicht) && (
<Field label=""
hint="Patch-Terrain: NURBS-Surface gefittet durch alle Höhenlinien (Rhinos Patch-Befehl). Glatte, kontinuierliche Topo-Oberfläche — die kanonische Methode für Terrain aus Konturen.">
<label style={{ display: 'flex', alignItems: 'center', gap: 6,
fontSize: 11, cursor: 'pointer' }}>
<input type="checkbox" checked={getContourPatch}
onChange={(e) => setGetContourPatch(e.target.checked)} />
<Icon name="landscape" size={13} /> Patch-Terrain aus Höhenlinien
</label>
</Field>
{(getContours || getContourTin || getContourSchicht || getContourPatch) && (
<Field label="HÖHEN-ABSTAND">
<Radio value={contourInt}
options={[
+7 -2
View File
@@ -263,15 +263,20 @@ function EbeneRow({ e, depth, hasChildren, expanded, onToggleExpand, active, mod
minHeight: 24,
}}
>
{/* Chevron sitzt visuell weiter rechts (marginLeft) — marginRight
kompensiert das, damit die nachfolgenden Elemente (Auge, Code,
Farbe, Name) nicht mitrutschen. Spacer fuer kinderlose Zeilen
spiegelt dasselbe Offset, sonst springt die Eye-Spalte zwischen
Parent- und Leaf-Zeilen. */}
{hasChildren ? (
<button
className="btn-icon-xs"
onClick={(ev) => { ev.stopPropagation(); onToggleExpand() }}
title={expanded ? 'Einklappen' : 'Aufklappen'}
style={{ width: 12, height: 12 }}
style={{ width: 12, height: 12, marginLeft: 6, marginRight: -6 }}
><Icon name={expanded ? 'expand_more' : 'chevron_right'} size={11} /></button>
) : (
<span style={{ width: 12, flexShrink: 0 }} />
<span style={{ width: 12, flexShrink: 0, marginLeft: 6, marginRight: -6 }} />
)}
<button
className={`btn-icon-sm ${eyeOn ? 'is-on' : ''}`}
+20 -4
View File
@@ -161,17 +161,33 @@ input, select {
background: var(--bg-input);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 5px 8px;
border-radius: 999px;
padding: 4px 12px;
outline: none;
transition: border-color 0.16s, box-shadow 0.16s;
transition: border-color 0.16s, background 0.16s, box-shadow 0.16s;
}
input:hover {
border-color: var(--accent);
background: var(--bg-item-hover);
}
input:hover { border-color: var(--text-muted); }
input:focus, select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-dim);
}
input[type="number"]::-webkit-inner-spin-button { opacity: 0.3; }
/* Checkboxes + Color-Picker: kein Pill — native rendering. */
input[type="checkbox"], input[type="radio"], input[type="color"],
input[type="file"], input[type="range"] {
border-radius: 0;
padding: 0;
background: transparent;
}
input[type="checkbox"]:hover, input[type="radio"]:hover,
input[type="color"]:hover, input[type="file"]:hover,
input[type="range"]:hover {
background: transparent;
border-color: var(--border);
}
/* Pill-shaped select */
select {
+2
View File
@@ -173,6 +173,8 @@ export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) }
export function openElementeUebersicht() { send('OPEN_ELEMENTE_UEBERSICHT', {}) }
export function openElementeProperties() { send('OPEN_ELEMENTE_PROPERTIES', {}) }
export function setDarstellung(d) { send('SET_DARSTELLUNG', { darstellung: d || '' }) }
// Anordnen — 2D-Z-Stack via Rhino-DisplayOrder. dir: 'front'|'forward'|'backward'|'back'
export function arrangeSelection(dir) { send('ARRANGE', { dir }) }
export function saveOeffStyle(name, settings) {
send('SAVE_OEFF_STYLE', { name, settings })
}