Files
DOSSIER/src/LayoutDialogApp.jsx
T
karim 13a5e1eb7a AGPL-3.0 Dual-Lizenz + Pill-Stil-UI + Section-Style-Overhaul + Plan-Mode-Template
Lizenz:
- AGPL-3.0 LICENSE-File im Repo-Root (GNU Volltext)
- SPDX-Header + Copyright in allen Source-Files (Python/JSX/JS/Rust)
- license-Feld in package.json + Cargo.toml
- About-App komplett neu: Dual-Lizenz-Block (AGPL + Commercial),
  openbureau-Branding, Version-Pills, made-in-Switzerland-Footer

UI-Restyle (3 Wellen) — alle Dialoge + Satellites + Panel-Sidebars
auf gemeinsamen Pill-Stil aus BarControls (BarToggle/BarButton/BarCombo):
- Welle 1: GeschossDialog/Settings, AusschnittSettings, LayoutDialog
- Welle 2: ConfirmDeleteEbene, Kamera, MasseSettings, Osm, Swisstopo,
  TextEditor, AusschnittLayerDialog, LayerCombinations
- Welle 3: LayoutsApp, MassstabApp, WerkzeugeApp, OverridesApp,
  ZeichnungsebenenApp; Werkzeuge mit ElementeApp-PillGroup-Layout

GeschossDialog Header-Refactor: +Geschoss/+Zeichnung in Toolbar oben,
move-Pfeile-Spalte breiter (kein Overlap mit G-Haken)

Ausschnitte Rows als Pills, kein Outer-Border ums Suchfeld

Section-Style komplett neu (gestaltung.py + GestaltungApp.jsx):
- ObjectSectionAttributesSource.FromObject (richtiger Enum-Name fuer Mac)
- HatchPatternPrintColor + BoundaryPrintColor mit-setzen (Display = Print)
- BoundaryColor nur bei explizitem User-Override, sonst Rhino-Default
- background_color_hex Parameter (BackgroundFillMode=SolidColor)
- Readback aus GetCustomSectionStyle statt direkt aus Attributes
- UI: Schnittkante > Section Style > Solid-Fill mit proper SectionHead
- 'Boundary' (3D Pen) -> 'Background' weil sich's wie Section-Hintergrund verhaelt

Plan-Mode 'Dossier Plan' via Template:
- rhino/templates/dossier_plan.ini wird direkt geladen
- Fallback auf Technical-Clone + ini-Patch wenn Template fehlt
- Auto-Cleanup von Orphan-Modes vor Import (Name- oder Guid-Match)
- ClipSectionUsage=1 + TechnicalMask=15 als bekannte Soll-Werte
- Bei Template-Pfad keine ini-Patches (1:1 wie User exportiert)
- Sanity-Print listet alle registrierten Modes nach Anlegen

Bridge-Unification: 4 Settings-Apps (Ebenen/Project/Geschoss*Dialog)
benutzen jetzt chunkende send() statt eigene bridgeSend ohne Chunk-
Logik -> grosse Payloads (Hatch-Refs etc.) kommen nicht mehr truncated
bei Python an (loeste 'JSON-Fehler char 990'-Regression in Ebenen-
Settings)

Library-Imports robust: 'import library' jetzt Top-Level in elemente.py
+ rhinopanel.py (statt Lazy in Methoden) -> 'No module named library'-
Crashes weg auch wenn sys.path zwischendurch resettet wird

Tools fuer Display-Mode-Maintenance:
- _clean_display_modes.py (loescht alle Custom-Modes, Built-ins bleiben)
- _inspect_plan_mode.py / _inspect_obj_section.py / _inspect_obj_boundary.py
  (Diagnose-Skripte fuer SectionStyle-Property-Reverse-Engineering)
- _reset_rhino_settings.sh (Backup + Nuke der Rhino-Settings als
  letzte Bastion gegen korrupte Display-Modes)
2026-05-26 17:09:18 +02:00

208 lines
6.9 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useEffect, useState } from 'react'
import { onMessage, notifyReady } from './lib/rhinoBridge'
import { BarToggle, BAR_H } from './components/BarControls'
function send(type, payload = {}) {
if (!window.RHINO_MODE) { console.log('[LayoutDialog] →', type, payload); return }
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
}
const PAPER_SIZES = ['A4', 'A3', 'A2', 'A1', 'A0', 'Letter']
const pillInput = {
height: BAR_H,
background: 'var(--bg-input)',
border: '1px solid var(--border)',
borderRadius: 999,
color: 'var(--text-primary)',
fontSize: 11, fontFamily: 'var(--font)',
padding: '0 10px',
outline: 'none',
boxSizing: 'border-box',
}
export default function LayoutDialogApp() {
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
const [mode, setMode] = useState(initial.mode || 'new')
const [layout, setLayout] = useState(initial.layout || null)
const [name, setName] = useState('')
const [format, setFormat] = useState('A3')
const [landscape, setLandscape] = useState(true)
const [cw, setCw] = useState('420')
const [ch, setCh] = useState('297')
useEffect(() => {
onMessage('LAYOUT_DIALOG_STATE', (p) => {
if (p.mode) setMode(p.mode)
if (p.layout) {
setLayout(p.layout)
if (p.mode === 'edit') {
setFormat('custom')
setCw(String(Math.round(p.layout.width || 420)))
setCh(String(Math.round(p.layout.height || 297)))
}
}
})
notifyReady()
const blockContext = (ev) => ev.preventDefault()
document.addEventListener('contextmenu', blockContext)
return () => document.removeEventListener('contextmenu', blockContext)
}, [])
const editing = mode === 'edit'
const submit = () => {
const payload = { name: name.trim(), format, landscape }
if (format === 'custom') {
const w = parseFloat(cw), h = parseFloat(ch)
if (!(w > 0) || !(h > 0)) { alert('Bitte gültige Größe eingeben.'); return }
payload.customWidth = w
payload.customHeight = h
}
send('SAVE', payload)
}
return (
<div style={{
position: 'absolute', inset: 0,
background: 'var(--bg-dialog)',
display: 'flex', flexDirection: 'column',
fontFamily: 'var(--font)', color: 'var(--text-primary)',
overflow: 'hidden',
}}>
{/* Body */}
<div style={{ flex: 1, overflowY: 'auto', padding: '12px 16px',
display: 'flex', flexDirection: 'column', gap: 14 }}>
{!editing && (
<Field label="Name">
<input
type="text" value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') submit() }}
placeholder="z.B. Grundriss EG"
autoFocus
style={{ ...pillInput, width: '100%' }}
/>
</Field>
)}
<Field label="Papierformat">
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
{PAPER_SIZES.map(f => (
<BarToggle key={f} label={f}
active={format === f}
onClick={() => setFormat(f)} />
))}
<BarToggle label="Eigene"
active={format === 'custom'}
onClick={() => setFormat('custom')} />
</div>
</Field>
{format === 'custom' ? (
<Field label="Größe (mm)">
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<input
type="text" value={cw}
onChange={(e) => setCw(e.target.value)}
placeholder="Breite"
style={{ ...pillInput, flex: 1,
fontFamily: 'DM Mono, monospace',
textAlign: 'right' }}
/>
<span style={{ color: 'var(--text-muted)', fontSize: 11 }}>×</span>
<input
type="text" value={ch}
onChange={(e) => setCh(e.target.value)}
placeholder="Höhe"
style={{ ...pillInput, flex: 1,
fontFamily: 'DM Mono, monospace',
textAlign: 'right' }}
/>
<span style={{ color: 'var(--text-muted)', fontSize: 10, width: 22 }}>mm</span>
</div>
</Field>
) : (
<Field label="Ausrichtung">
<div style={{ display: 'flex', gap: 6 }}>
<BarToggle icon="crop_landscape" label="Quer"
active={landscape}
onClick={() => setLandscape(true)} />
<BarToggle icon="crop_portrait" label="Hoch"
active={!landscape}
onClick={() => setLandscape(false)} />
</div>
</Field>
)}
{/* Preview */}
<FormatPreview format={format} landscape={landscape}
customW={parseFloat(cw)} customH={parseFloat(ch)} />
</div>
{/* Footer */}
<div style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '10px 14px',
borderTop: '1px solid var(--border)',
background: 'var(--bg-section)',
}}>
<div style={{ flex: 1 }} />
<BarToggle label="Abbrechen" onClick={() => send('CANCEL', {})} />
<BarToggle label={editing ? 'Anwenden' : 'Erstellen'}
active
disabled={!editing && !name.trim()}
title={!editing && !name.trim() ? 'Erst einen Namen eingeben' : ''}
onClick={submit} />
</div>
</div>
)
}
function Field({ label, children }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<span className="label-xs">{label}</span>
{children}
</div>
)
}
const PAPER_DIMS = {
A0: [841, 1189], A1: [594, 841], A2: [420, 594], A3: [297, 420],
A4: [210, 297], Letter: [216, 279],
}
function FormatPreview({ format, landscape, customW, customH }) {
let w, h
if (format === 'custom') { w = customW; h = customH }
else {
const dims = PAPER_DIMS[format]
if (!dims) return null
w = dims[0]; h = dims[1]
if (landscape) { w = dims[1]; h = dims[0] }
}
if (!(w > 0) || !(h > 0)) return null
const MAX = 120
const scale = Math.min(MAX / w, MAX / h)
const pw = w * scale, ph = h * scale
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, marginTop: 6 }}>
<div style={{
width: pw, height: ph,
background: 'var(--bg-input)',
border: '1.5px solid var(--accent)',
boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
transition: 'width 0.2s, height 0.2s',
}} />
<span style={{ fontSize: 10, color: 'var(--text-muted)',
fontFamily: 'DM Mono, monospace' }}>
{Math.round(w)} × {Math.round(h)} mm
</span>
</div>
)
}