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)
This commit is contained in:
+171
-34
@@ -1,54 +1,191 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function Row({ icon, label, children }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '8px 0',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
}}>
|
||||
<Icon name={icon} size={14}
|
||||
style={{ color: 'var(--text-muted)', flexShrink: 0 }} />
|
||||
<span style={{
|
||||
fontSize: 10, color: 'var(--text-muted)',
|
||||
textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
fontWeight: 600, width: 70, flexShrink: 0,
|
||||
}}>{label}</span>
|
||||
<span style={{ flex: 1, fontSize: 12, minWidth: 0,
|
||||
overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{children}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function VersionPill({ label, version }) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||||
height: 26, padding: '0 12px 0 10px',
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: 9, color: 'var(--text-muted)',
|
||||
textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
fontWeight: 600,
|
||||
}}>{label}</span>
|
||||
<span style={{
|
||||
fontFamily: 'DM Mono, monospace',
|
||||
fontSize: 11, fontWeight: 500,
|
||||
color: 'var(--accent-light)',
|
||||
}}>v{version}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AboutApp() {
|
||||
useEffect(() => { notifyReady() }, [])
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
padding: '28px 32px',
|
||||
padding: '36px 36px 28px',
|
||||
fontFamily: 'var(--font)', color: 'var(--text-primary)',
|
||||
background: 'var(--bg-panel)', minHeight: '100vh',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex', flexDirection: 'column', gap: 28,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8,
|
||||
marginBottom: 4 }}>
|
||||
<span style={{
|
||||
fontFamily: "Krungthep, 'Archivo Black', sans-serif",
|
||||
fontSize: 32, letterSpacing: '-0.02em', lineHeight: 1,
|
||||
{/* Logo + Tagline */}
|
||||
<div>
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'baseline', gap: 8,
|
||||
marginBottom: 6,
|
||||
}}>
|
||||
DOSSIER<span style={{ color: 'var(--accent)' }}>.</span>
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.06em', textTransform: 'uppercase',
|
||||
marginBottom: 22 }}>
|
||||
Teil von OpenStudio
|
||||
<span style={{
|
||||
fontFamily: "Krungthep, 'Archivo Black', sans-serif",
|
||||
fontSize: 38, letterSpacing: '-0.02em', lineHeight: 1,
|
||||
}}>
|
||||
DOSSIER<span style={{ color: 'var(--accent)' }}>.</span>
|
||||
</span>
|
||||
</div>
|
||||
<div style={{
|
||||
fontFamily: "'Playfair Display', Georgia, serif",
|
||||
fontStyle: 'italic',
|
||||
fontSize: 14, color: 'var(--text-secondary)',
|
||||
lineHeight: 1.4,
|
||||
}}>
|
||||
Architektur-Studio für Rhino 8
|
||||
</div>
|
||||
<div style={{
|
||||
marginTop: 10,
|
||||
fontSize: 10, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.1em', textTransform: 'uppercase',
|
||||
}}>
|
||||
Teil von <a href="https://openbureau.ch" target="_blank" rel="noreferrer"
|
||||
style={{ color: 'var(--accent)', textDecoration: 'none' }}>
|
||||
openbureau
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'auto 1fr',
|
||||
gap: '8px 16px', fontSize: 12, marginBottom: 22 }}>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Launcher</span>
|
||||
<span style={{ fontFamily: 'DM Mono, monospace' }}>v{__LAUNCHER_VERSION__}</span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Plugin</span>
|
||||
<span style={{ fontFamily: 'DM Mono, monospace' }}>v{__APP_VERSION__}</span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Autor</span>
|
||||
<span>Karim Gabriele Varano</span>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Website</span>
|
||||
<a href="https://gabrielevarano.ch" target="_blank" rel="noreferrer"
|
||||
style={{ color: 'var(--accent)', textDecoration: 'none' }}>
|
||||
gabrielevarano.ch
|
||||
</a>
|
||||
<span style={{ color: 'var(--text-muted)' }}>Lizenz</span>
|
||||
<span>Proprietär — © 2026 Karim Gabriele Varano</span>
|
||||
{/* Versions */}
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<VersionPill label="Plugin" version={__APP_VERSION__} />
|
||||
<VersionPill label="Launcher" version={__LAUNCHER_VERSION__} />
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 11, color: 'var(--text-muted)',
|
||||
lineHeight: 1.5,
|
||||
paddingTop: 14,
|
||||
borderTop: '1px solid var(--border-light)' }}>
|
||||
Rhino 8 Plugin für architektonische Workflows — Wände, Decken,
|
||||
Öffnungen, Räume, SIA 416, Plan-Layouts. Schwester-App: Rapport.
|
||||
{/* Meta-Rows */}
|
||||
<div>
|
||||
<Row icon="person" label="Autor">
|
||||
Karim Gabriele Varano
|
||||
</Row>
|
||||
<Row icon="public" label="Web">
|
||||
<a href="https://dossier.openbureau.ch" target="_blank" rel="noreferrer"
|
||||
style={{ color: 'var(--accent)', textDecoration: 'none' }}>
|
||||
dossier.openbureau.ch
|
||||
</a>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
{/* Lizenz — Dual: AGPL-3.0 + Commercial */}
|
||||
<div style={{
|
||||
padding: '14px 16px',
|
||||
background: 'var(--bg-section)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderLeft: '3px solid var(--accent)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8,
|
||||
}}>
|
||||
<Icon name="copyright" size={13} style={{ color: 'var(--accent)' }} />
|
||||
<span style={{
|
||||
fontSize: 10, color: 'var(--text-muted)',
|
||||
textTransform: 'uppercase', letterSpacing: '0.08em',
|
||||
fontWeight: 600,
|
||||
}}>Lizenz · Dual</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8,
|
||||
marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600 }}>AGPL-3.0</span>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
· Source available
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||
lineHeight: 1.55, marginBottom: 10 }}>
|
||||
Frei nutzbar, modifizierbar und weitergebbar unter den Bedingungen
|
||||
der <a href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
target="_blank" rel="noreferrer"
|
||||
style={{ color: 'var(--accent)', textDecoration: 'none' }}>
|
||||
GNU Affero General Public License v3
|
||||
</a>. Abgeleitete Werke müssen unter derselben Lizenz veröffentlicht
|
||||
werden.
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
height: 1, background: 'var(--border-light)', margin: '6px 0 10px',
|
||||
}} />
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8,
|
||||
marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600 }}>Kommerzielle Lizenz</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||
lineHeight: 1.55 }}>
|
||||
Für proprietäre Integrationen, geschlossene Forks oder Nutzungen die
|
||||
nicht mit AGPL-3.0 vereinbar sind, ist eine kommerzielle Lizenz
|
||||
erforderlich. Kontakt:{' '}
|
||||
<a href="mailto:karim@gabrielevarano.ch"
|
||||
style={{ color: 'var(--accent)', textDecoration: 'none' }}>
|
||||
karim@gabrielevarano.ch
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||
marginTop: 10, paddingTop: 8,
|
||||
borderTop: '1px solid var(--border-light)' }}>
|
||||
© 2026 Karim Gabriele Varano
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div style={{
|
||||
marginTop: 'auto',
|
||||
paddingTop: 16,
|
||||
borderTop: '1px solid var(--border-light)',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
fontSize: 9, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.05em',
|
||||
}}>
|
||||
<Icon name="favorite" size={10} style={{ color: 'var(--accent)' }} />
|
||||
<span>made in Switzerland</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import EbenenManager from './components/EbenenManager'
|
||||
import {
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
// 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, BarCombo, BAR_H } from './components/BarControls'
|
||||
|
||||
function send(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[AusschnittSettings] →', type, payload); return }
|
||||
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
function Field({ label, hint, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, padding: '6px 0' }}>
|
||||
@@ -85,42 +100,38 @@ export default function AusschnittSettingsApp() {
|
||||
value={snap.scale || ''}
|
||||
onChange={(ev) => set({ scale: ev.target.value })}
|
||||
placeholder="1:50"
|
||||
style={{ flex: 1, fontSize: 11, fontFamily: 'var(--font-mono)', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, fontFamily: 'var(--font-mono)', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label="DARSTELLUNG"
|
||||
hint="SIA-400 Detaillierungsgrad fuer diesen Ausschnitt — leer = beim Restore nicht aendern">
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={snap.darstellung || ''}
|
||||
onChange={(ev) => set({ darstellung: ev.target.value })}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
>
|
||||
onChange={(v) => set({ darstellung: v })}>
|
||||
<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>
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Field>
|
||||
|
||||
<Field label="BILDSCHIRMMODUS"
|
||||
hint="Display-Mode des Viewports beim Wiederherstellen">
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={snap.displayMode || ''}
|
||||
onChange={(ev) => {
|
||||
const dm = displayModes.find(d => d.id === ev.target.value)
|
||||
onChange={(v) => {
|
||||
const dm = displayModes.find(d => d.id === v)
|
||||
set({
|
||||
displayMode: ev.target.value || null,
|
||||
displayMode: v || null,
|
||||
displayModeName: dm ? dm.name : null,
|
||||
})
|
||||
}}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
>
|
||||
}}>
|
||||
<option value="">— unverändert —</option>
|
||||
{displayModes.map(dm => (
|
||||
<option key={dm.id} value={dm.id}>{dm.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Field>
|
||||
|
||||
<SectionLabel>Grafische Overrides</SectionLabel>
|
||||
@@ -142,29 +153,25 @@ export default function AusschnittSettingsApp() {
|
||||
{snap.applyOverrides && (
|
||||
<>
|
||||
<Field label="OVERRIDES STATUS">
|
||||
<select
|
||||
value={snap.overridesEnabled ? 'on' : 'off'}
|
||||
onChange={(ev) => set({ overridesEnabled: ev.target.value === 'on' })}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
>
|
||||
<option value="on">AN</option>
|
||||
<option value="off">AUS</option>
|
||||
</select>
|
||||
<BarToggle label="AN"
|
||||
active={!!snap.overridesEnabled}
|
||||
onClick={() => set({ overridesEnabled: true })} />
|
||||
<BarToggle label="AUS"
|
||||
active={!snap.overridesEnabled}
|
||||
onClick={() => set({ overridesEnabled: false })} />
|
||||
</Field>
|
||||
|
||||
<Field label="OVERRIDES PRESET"
|
||||
hint="Leer = kein Preset (Doc-Rules bleiben)">
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={snap.overridesPreset || ''}
|
||||
onChange={(ev) => set({ overridesPreset: ev.target.value })}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
disabled={!snap.overridesEnabled}
|
||||
>
|
||||
onChange={(v) => set({ overridesPreset: v })}
|
||||
disabled={!snap.overridesEnabled}>
|
||||
<option value="">— kein Preset —</option>
|
||||
{overridesPresets.map(name => (
|
||||
<option key={name} value={name}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
@@ -173,16 +180,14 @@ export default function AusschnittSettingsApp() {
|
||||
|
||||
<Field label="KOMBI"
|
||||
hint='"Eigene" = die per Snap gespeicherte Sichtbarkeit. Ein Preset überschreibt diese beim Wiederherstellen.'>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={snap.layerCombination || ''}
|
||||
onChange={(ev) => set({ layerCombination: ev.target.value })}
|
||||
style={{ flex: 1, fontSize: 11, minWidth: 0 }}
|
||||
>
|
||||
onChange={(v) => set({ layerCombination: v })}>
|
||||
<option value="">— Eigene Sichtbarkeit —</option>
|
||||
{layerKombis.map(name => (
|
||||
<option key={name} value={name}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
@@ -194,8 +199,8 @@ export default function AusschnittSettingsApp() {
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button className="btn-text" onClick={() => send('CANCEL', {})}>Abbrechen</button>
|
||||
<button className="btn-contained" onClick={saveAndClose}>Übernehmen</button>
|
||||
<BarToggle label="Abbrechen" onClick={() => send('CANCEL', {})} />
|
||||
<BarToggle label="Übernehmen" active onClick={saveAndClose} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+9
-11
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import ContextMenu from './components/ContextMenu'
|
||||
@@ -103,13 +105,13 @@ function OrientationBadge({ orientation }) {
|
||||
title={variant.title}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: 24, height: 24, flexShrink: 0,
|
||||
width: 20, height: 20, flexShrink: 0,
|
||||
borderRadius: 999,
|
||||
background: 'var(--bg-input)',
|
||||
color: variant.color,
|
||||
}}
|
||||
>
|
||||
<Icon name={variant.icon} size={14} />
|
||||
<Icon name={variant.icon} size={12} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -123,13 +125,13 @@ function AusschnittCard({ snap, onClick, onContextMenu, onMenuClick, onRename, o
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 8px',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '2px 8px 2px 4px',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 999,
|
||||
background: 'var(--bg-input)',
|
||||
cursor: 'grab', userSelect: 'none',
|
||||
marginBottom: 4,
|
||||
marginBottom: 3,
|
||||
opacity: dragging ? 0.4 : 1,
|
||||
transition: 'background 0.14s, border-color 0.14s, opacity 0.14s',
|
||||
}}
|
||||
@@ -364,13 +366,9 @@ export default function AusschnitteApp() {
|
||||
position: 'relative',
|
||||
}}>
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: 8 }}>
|
||||
{/* Save-Bar als Card */}
|
||||
{/* Save-Bar — kein Outer-Border mehr, nur das Pill-Input + Add-Button. */}
|
||||
<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,
|
||||
}}>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BarButton } from './components/BarControls'
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import EbenenSettingsDialog from './components/EbenenSettingsDialog'
|
||||
import { notifyReady, onMessage } from './lib/rhinoBridge'
|
||||
|
||||
function bridgeSend(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
||||
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||
}
|
||||
import { notifyReady, onMessage, send as bridgeSend } from './lib/rhinoBridge'
|
||||
|
||||
export default function EbenenSettingsApp() {
|
||||
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BarButton, BarCombo } from './components/BarControls'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BarButton } from './components/BarControls'
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect } from 'react'
|
||||
import GeschossDialog from './components/GeschossDialog'
|
||||
import { notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function bridgeSend(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
||||
const json = JSON.stringify({ type, payload })
|
||||
document.title = 'RHINOMSG::' + json
|
||||
}
|
||||
import { notifyReady, send as bridgeSend } from './lib/rhinoBridge'
|
||||
|
||||
// recalcOkff direkt hier — gleiche Logik wie in ZeichnungsebenenApp.jsx,
|
||||
// damit der Dialog die OKFF-Werte beim Editieren live updaten kann.
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect } from 'react'
|
||||
import GeschossSettingsDialog from './components/GeschossSettingsDialog'
|
||||
import { notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function bridgeSend(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
||||
const json = JSON.stringify({ type, payload })
|
||||
document.title = 'RHINOMSG::' + json
|
||||
}
|
||||
import { notifyReady, send as bridgeSend } from './lib/rhinoBridge'
|
||||
|
||||
export default function GeschossSettingsApp() {
|
||||
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
||||
|
||||
+113
-19
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarCombo, BarToggle, BarButton } from './components/BarControls'
|
||||
@@ -311,14 +313,20 @@ function HatchEditor({ sel, enabled, source, color, pattern, scale, rotation,
|
||||
dropdownOptions.push({ value: currentValue, label: currentValue })
|
||||
}
|
||||
const applyPattern = (newValue) => {
|
||||
// Pattern-Pick: KEIN color senden (null) damit Backend per-Object die
|
||||
// Layer-Farbe nimmt. Color-Override greift nur wenn User explizit den
|
||||
// ColorBar anklickt → apply({color: ...}).
|
||||
if (newValue === '__layer__') setter(true, 'layer', null, null, null, null)
|
||||
else if (newValue === 'None') setter(false, source, null, null, scale, rotation)
|
||||
else setter(true, 'object', color, newValue, scale, rotation)
|
||||
else setter(true, 'object', null, newValue, scale, rotation)
|
||||
}
|
||||
const apply = (over) => setter(
|
||||
true,
|
||||
over.source ?? (source === 'layer' ? 'object' : source),
|
||||
(over.source ?? source) === 'layer' ? null : (over.color ?? color),
|
||||
// Layer-Source: null. Object-Source: nur expliziter Override-Color senden,
|
||||
// sonst null (= Backend nimmt Layer-Farbe).
|
||||
(over.source ?? source) === 'layer' ? null
|
||||
: (over.color !== undefined ? over.color : null),
|
||||
over.pattern ?? (objectPat === 'None' ? 'Solid' : objectPat),
|
||||
over.scale ?? scale,
|
||||
over.rotation ?? rotation,
|
||||
@@ -399,18 +407,107 @@ function FillBlock({ sel }) {
|
||||
|
||||
function SectionBlock({ sel }) {
|
||||
const color = sel.sectionColor || sel.layerColor || '#cccccc'
|
||||
return <HatchEditor
|
||||
sel={sel}
|
||||
enabled={sel.sectionEnabled === true}
|
||||
source={sel.sectionSource || 'layer'}
|
||||
color={color}
|
||||
pattern={sel.sectionPattern || 'Solid'}
|
||||
scale={sel.sectionScale ?? 1.0}
|
||||
rotation={sel.sectionRotation ?? 0.0}
|
||||
patternList={sel.hatchPatterns || ['Solid']}
|
||||
layerHint="Pattern, Skalierung & Farbe folgen Layer-SectionStyle"
|
||||
setter={setSectionStyle}
|
||||
/>
|
||||
const enabled = sel.sectionEnabled === true
|
||||
const isLayerSource = (sel.sectionSource || 'layer') === 'layer'
|
||||
// Boundary + Background Helper: setter mit allen aktuellen Werten,
|
||||
// overrides als opts.
|
||||
const callSetter = (over = {}) => {
|
||||
const opts = {
|
||||
boundaryVisible: over.boundaryVisible !== undefined
|
||||
? over.boundaryVisible : (sel.sectionBoundaryVisible !== false),
|
||||
boundaryWidthScale: over.boundaryWidthScale !== undefined
|
||||
? over.boundaryWidthScale : (sel.sectionBoundaryWidthScale ?? 1.0),
|
||||
boundaryColor: over.boundaryColor !== undefined
|
||||
? over.boundaryColor : sel.sectionBoundaryColor,
|
||||
backgroundColor: over.backgroundColor !== undefined
|
||||
? over.backgroundColor : sel.sectionBackgroundColor,
|
||||
}
|
||||
setSectionStyle(
|
||||
enabled, sel.sectionSource || 'layer', sel.sectionColor || null,
|
||||
sel.sectionPattern || 'Solid',
|
||||
sel.sectionScale ?? 1.0, sel.sectionRotation ?? 0.0,
|
||||
opts)
|
||||
}
|
||||
const boundaryVisible = sel.sectionBoundaryVisible !== false
|
||||
const boundaryColor = sel.sectionBoundaryColor || '#000000'
|
||||
const boundaryWidthScale = sel.sectionBoundaryWidthScale ?? 1.0
|
||||
const backgroundColor = sel.sectionBackgroundColor || null
|
||||
return <>
|
||||
{/* Schnittkante (Boundary) — Outline des Section-Cut, oben weil
|
||||
visuell zuerst wahrgenommen */}
|
||||
<SectionHead title="Schnittkante" />
|
||||
{!isLayerSource && enabled ? (
|
||||
<div style={{ padding: '0 14px 8px', display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<BarToggle label={boundaryVisible ? 'Sichtbar' : 'Versteckt'}
|
||||
icon={boundaryVisible ? 'visibility' : 'visibility_off'}
|
||||
active={boundaryVisible}
|
||||
onClick={() => callSetter({ boundaryVisible: !boundaryVisible })} />
|
||||
{boundaryVisible && (
|
||||
<>
|
||||
<Icon name="line_weight" size={14}
|
||||
style={{ color: 'var(--text-muted)', flexShrink: 0 }}
|
||||
title="Schnittkanten-Breite" />
|
||||
<NumInput
|
||||
value={boundaryWidthScale} step={0.5} min={0.1}
|
||||
onCommit={(v) => callSetter({ boundaryWidthScale: v })}
|
||||
width={56}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{boundaryVisible && (
|
||||
<ColorBar
|
||||
color={boundaryColor}
|
||||
onChange={(c) => callSetter({ boundaryColor: c })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: '0 14px 8px', fontSize: 10, color: 'var(--text-muted)',
|
||||
fontStyle: 'italic' }}>
|
||||
Aktiviere ein Pattern damit die Schnittkante eingestellt werden kann.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section Style (Hatch-Fill) */}
|
||||
<SectionHead title="Section Style" />
|
||||
<HatchEditor
|
||||
sel={sel}
|
||||
enabled={enabled}
|
||||
source={sel.sectionSource || 'layer'}
|
||||
color={color}
|
||||
pattern={sel.sectionPattern || 'Solid'}
|
||||
scale={sel.sectionScale ?? 1.0}
|
||||
rotation={sel.sectionRotation ?? 0.0}
|
||||
patternList={sel.hatchPatterns || ['Solid']}
|
||||
layerHint="Pattern, Skalierung & Farbe folgen Layer-SectionStyle"
|
||||
setter={setSectionStyle}
|
||||
/>
|
||||
|
||||
{/* Hintergrund (Solid-Fill hinter dem Hatch-Pattern) */}
|
||||
{!isLayerSource && enabled && (
|
||||
<>
|
||||
<SectionHead title="Solid-Fill" />
|
||||
<div style={{ padding: '0 14px 12px', display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<BarToggle label={backgroundColor ? 'Solid' : 'Transparent'}
|
||||
icon={backgroundColor ? 'format_color_fill' : 'check_box_outline_blank'}
|
||||
active={!!backgroundColor}
|
||||
onClick={() => callSetter({
|
||||
backgroundColor: backgroundColor ? null : '#ffffff'
|
||||
})} />
|
||||
</div>
|
||||
{backgroundColor && (
|
||||
<ColorBar
|
||||
color={backgroundColor}
|
||||
onChange={(c) => callSetter({ backgroundColor: c })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@@ -449,7 +546,7 @@ export default function GestaltungApp() {
|
||||
const kind = sel.geometryKind || 'curve'
|
||||
const showFill = kind === 'curve'
|
||||
const showSection = kind === '3d'
|
||||
const penLabel = (kind === '3d') ? 'Boundary' : 'Pen'
|
||||
const penLabel = (kind === '3d') ? 'Background' : 'Pen'
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
@@ -472,10 +569,7 @@ export default function GestaltungApp() {
|
||||
<PenBlock sel={sel} />
|
||||
|
||||
{showSection && (
|
||||
<>
|
||||
<SectionHead title="Section Style" />
|
||||
<SectionBlock sel={sel} />
|
||||
</>
|
||||
<SectionBlock sel={sel} />
|
||||
)}
|
||||
|
||||
<SectionHead title="Effects" />
|
||||
|
||||
+49
-77
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './components/BarControls'
|
||||
import {
|
||||
onMessage, notifyReady,
|
||||
setKameraViewport, setKameraProjection, setKameraIso,
|
||||
@@ -13,6 +16,18 @@ const labelXs = {
|
||||
fontWeight: 600,
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
function NumberField({ label, value, onCommit, suffix, step = 0.1 }) {
|
||||
const [draft, setDraft] = useState(value != null ? value.toFixed(3) : '')
|
||||
useEffect(() => {
|
||||
@@ -37,8 +52,7 @@ function NumberField({ label, value, onCommit, suffix, step = 0.1 }) {
|
||||
if (ev.key === 'Enter') commit()
|
||||
if (ev.key === 'Escape') setDraft(value != null ? value.toFixed(3) : '')
|
||||
}}
|
||||
style={{ flex: 1, fontSize: 11, padding: '4px 8px',
|
||||
fontFamily: 'var(--font-mono)' }}
|
||||
style={{ ...pillInput, flex: 1, fontFamily: 'var(--font-mono)' }}
|
||||
/>
|
||||
{suffix && (
|
||||
<span style={{ fontSize: 9, color: 'var(--text-muted)', minWidth: 18 }}>
|
||||
@@ -123,32 +137,19 @@ export default function KameraApp() {
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 600 }}>{vp.name || 'Unnamed'}</span>
|
||||
<div style={{ flex: 1 }} />
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
borderRadius: 999,
|
||||
border: '1px solid var(--border)',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setKameraProjection(false)}
|
||||
className={!isPar ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '4px 12px', fontSize: 10, border: 'none',
|
||||
borderRadius: 0 }}
|
||||
>Perspektive</button>
|
||||
<button
|
||||
onClick={() => setKameraProjection(true)}
|
||||
className={isPar ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '4px 12px', fontSize: 10, border: 'none',
|
||||
borderRadius: 0 }}
|
||||
>Parallel</button>
|
||||
</div>
|
||||
<BarToggle label="Perspektive"
|
||||
active={!isPar}
|
||||
onClick={() => setKameraProjection(false)} />
|
||||
<BarToggle label="Parallel"
|
||||
active={isPar}
|
||||
onClick={() => setKameraProjection(true)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plan-Norden — Rotations-Winkel im Uhrzeigersinn von +Y */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<span style={labelXs}>Plan-Norden (Rotation von +Y, im Uhrzeigersinn)</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<input
|
||||
type="number" min={0} max={360} step={0.5}
|
||||
value={northAngle.toFixed(1)}
|
||||
@@ -159,16 +160,12 @@ export default function KameraApp() {
|
||||
setKameraNorthAngle(a)
|
||||
}
|
||||
}}
|
||||
style={{ flex: 1, fontSize: 12, padding: '4px 8px',
|
||||
fontFamily: 'DM Mono, monospace' }}
|
||||
style={{ ...pillInput, flex: 1, fontFamily: 'DM Mono, monospace' }}
|
||||
/>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>°</span>
|
||||
<button
|
||||
<BarToggle label="Reset"
|
||||
onClick={() => { setNorthAngleState(0); setKameraNorthAngle(0) }}
|
||||
className="btn-outlined"
|
||||
style={{ padding: '4px 10px', fontSize: 10 }}
|
||||
title="Norden zurueck auf +Y (0°)"
|
||||
>Reset</button>
|
||||
title="Norden zurueck auf +Y (0°)" />
|
||||
</div>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', lineHeight: 1.4 }}>
|
||||
Norden = +Y bei 0°. Bei rotierten Projekten (z.B. swissBUILDINGS in
|
||||
@@ -180,20 +177,12 @@ export default function KameraApp() {
|
||||
{/* Iso-Quick-Picker */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<span style={labelXs}>Isometrie (Standard, true-iso 35°/45°)</span>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 4 }}>
|
||||
{[
|
||||
{ v: 'NW', label: 'NW' },
|
||||
{ v: 'NE', label: 'NE' },
|
||||
{ v: 'SE', label: 'SE' },
|
||||
{ v: 'SW', label: 'SW' },
|
||||
].map(o => (
|
||||
<button
|
||||
key={o.v}
|
||||
onClick={() => setKameraIso(o.v)}
|
||||
className="btn-outlined"
|
||||
style={{ padding: '6px 0', fontSize: 11 }}
|
||||
title={`Isometrie aus ${o.label} (Kamera blickt Richtung Szene)`}
|
||||
>{o.label}</button>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{['NW', 'NE', 'SE', 'SW'].map(v => (
|
||||
<BarToggle key={v} label={v}
|
||||
onClick={() => setKameraIso(v)}
|
||||
title={`Isometrie aus ${v} (Kamera blickt Richtung Szene)`}
|
||||
minWidth={48} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -205,9 +194,9 @@ export default function KameraApp() {
|
||||
{/* Distance read-only */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 10px',
|
||||
padding: '6px 12px',
|
||||
background: 'var(--bg-section)',
|
||||
borderRadius: 6,
|
||||
borderRadius: 999,
|
||||
border: '1px solid var(--border-light)',
|
||||
}}>
|
||||
<span style={labelXs}>Distanz</span>
|
||||
@@ -236,19 +225,13 @@ export default function KameraApp() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => kameraZoomExtents()}
|
||||
className="btn-outlined"
|
||||
style={{ padding: '6px 12px', fontSize: 11 }}
|
||||
>
|
||||
<Icon name="zoom_out_map" size={13} />
|
||||
<span style={{ marginLeft: 6 }}>Zoom Extents</span>
|
||||
</button>
|
||||
<BarToggle icon="zoom_out_map" label="Zoom Extents"
|
||||
onClick={() => kameraZoomExtents()} />
|
||||
|
||||
{/* Presets */}
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 6,
|
||||
padding: 10, borderRadius: 6,
|
||||
padding: 10, borderRadius: 'var(--r-lg)',
|
||||
background: 'var(--bg-section)',
|
||||
border: '1px solid var(--border-light)',
|
||||
}}>
|
||||
@@ -260,41 +243,34 @@ export default function KameraApp() {
|
||||
value={presetName}
|
||||
onChange={(ev) => setPresetName(ev.target.value)}
|
||||
onKeyDown={(ev) => { if (ev.key === 'Enter') saveCurrent() }}
|
||||
style={{ flex: 1, fontSize: 11, padding: '4px 8px' }}
|
||||
style={{ ...pillInput, flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
onClick={saveCurrent}
|
||||
<BarToggle label="Speichern"
|
||||
active={!!presetName.trim()}
|
||||
disabled={!presetName.trim()}
|
||||
className="btn-contained"
|
||||
style={{ padding: '4px 12px', fontSize: 11 }}
|
||||
>Aktuelle speichern</button>
|
||||
onClick={saveCurrent} />
|
||||
</div>
|
||||
{presets.length === 0 ? (
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
||||
Keine Presets gespeichert.
|
||||
</span>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{presets.map(p => (
|
||||
<div
|
||||
key={p.id}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '4px 6px',
|
||||
borderRadius: 4,
|
||||
padding: '2px 6px 2px 4px',
|
||||
borderRadius: 999,
|
||||
background: 'var(--bg-item)',
|
||||
border: '1px solid transparent',
|
||||
border: '1px solid var(--border-light)',
|
||||
fontSize: 11,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
<BarButton icon="play_arrow"
|
||||
onClick={() => applyKameraPreset(p.id)}
|
||||
className="btn-outlined"
|
||||
style={{ padding: '2px 8px', fontSize: 10 }}
|
||||
title="Anwenden"
|
||||
>
|
||||
<Icon name="play_arrow" size={11} />
|
||||
</button>
|
||||
title="Anwenden" />
|
||||
<span style={{ flex: 1, minWidth: 0,
|
||||
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap' }}>{p.name}</span>
|
||||
@@ -302,13 +278,9 @@ export default function KameraApp() {
|
||||
fontFamily: 'var(--font-mono)' }}>
|
||||
{p.parallel ? 'Par' : 'Persp'}
|
||||
</span>
|
||||
<button
|
||||
<BarButton icon="close"
|
||||
onClick={() => deleteKameraPreset(p.id)}
|
||||
className="btn-icon-xs"
|
||||
title="Loeschen"
|
||||
>
|
||||
<Icon name="close" size={11} />
|
||||
</button>
|
||||
title="Loeschen" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import AusschnittLayerDialog from './components/AusschnittLayerDialog'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
+40
-40
@@ -1,6 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
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 }
|
||||
@@ -9,6 +11,18 @@ function send(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')
|
||||
@@ -70,7 +84,7 @@ export default function LayoutDialogApp() {
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') submit() }}
|
||||
placeholder="z.B. Grundriss EG"
|
||||
autoFocus
|
||||
style={{ width: '100%', fontSize: 12, padding: '6px 8px' }}
|
||||
style={{ ...pillInput, width: '100%' }}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
@@ -78,19 +92,13 @@ export default function LayoutDialogApp() {
|
||||
<Field label="Papierformat">
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{PAPER_SIZES.map(f => (
|
||||
<button key={f}
|
||||
onClick={() => setFormat(f)}
|
||||
className={format === f ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '5px 12px', fontSize: 11 }}>
|
||||
{f}
|
||||
</button>
|
||||
<BarToggle key={f} label={f}
|
||||
active={format === f}
|
||||
onClick={() => setFormat(f)} />
|
||||
))}
|
||||
<button
|
||||
onClick={() => setFormat('custom')}
|
||||
className={format === 'custom' ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '5px 12px', fontSize: 11 }}>
|
||||
Eigene
|
||||
</button>
|
||||
<BarToggle label="Eigene"
|
||||
active={format === 'custom'}
|
||||
onClick={() => setFormat('custom')} />
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
@@ -101,16 +109,18 @@ export default function LayoutDialogApp() {
|
||||
type="text" value={cw}
|
||||
onChange={(e) => setCw(e.target.value)}
|
||||
placeholder="Breite"
|
||||
style={{ flex: 1, fontFamily: 'DM Mono, monospace',
|
||||
fontSize: 12, textAlign: 'right', padding: '6px 8px' }}
|
||||
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={{ flex: 1, fontFamily: 'DM Mono, monospace',
|
||||
fontSize: 12, textAlign: 'right', padding: '6px 8px' }}
|
||||
style={{ ...pillInput, flex: 1,
|
||||
fontFamily: 'DM Mono, monospace',
|
||||
textAlign: 'right' }}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 10, width: 22 }}>mm</span>
|
||||
</div>
|
||||
@@ -118,22 +128,12 @@ export default function LayoutDialogApp() {
|
||||
) : (
|
||||
<Field label="Ausrichtung">
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<button
|
||||
onClick={() => setLandscape(true)}
|
||||
className={landscape ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ flex: 1, padding: '8px 12px', fontSize: 11,
|
||||
display: 'flex', gap: 6, alignItems: 'center',
|
||||
justifyContent: 'center' }}>
|
||||
<Icon name="crop_landscape" size={16} /> Quer
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setLandscape(false)}
|
||||
className={!landscape ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ flex: 1, padding: '8px 12px', fontSize: 11,
|
||||
display: 'flex', gap: 6, alignItems: 'center',
|
||||
justifyContent: 'center' }}>
|
||||
<Icon name="crop_portrait" size={16} /> Hoch
|
||||
</button>
|
||||
<BarToggle icon="crop_landscape" label="Quer"
|
||||
active={landscape}
|
||||
onClick={() => setLandscape(true)} />
|
||||
<BarToggle icon="crop_portrait" label="Hoch"
|
||||
active={!landscape}
|
||||
onClick={() => setLandscape(false)} />
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
@@ -151,12 +151,12 @@ export default function LayoutDialogApp() {
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button className="btn-text" onClick={() => send('CANCEL', {})}>Abbrechen</button>
|
||||
<button className="btn-contained" onClick={submit}
|
||||
disabled={!editing && !name.trim()}
|
||||
title={!editing && !name.trim() ? 'Erst einen Namen eingeben' : ''}>
|
||||
{editing ? 'Anwenden' : 'Erstellen'}
|
||||
</button>
|
||||
<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>
|
||||
)
|
||||
|
||||
+34
-79
@@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import ContextMenu from './components/ContextMenu'
|
||||
import { BarButton, BarCombo, BAR_H } from './components/BarControls'
|
||||
import {
|
||||
onMessage, notifyReady,
|
||||
listLayouts, deleteLayout, renameLayout, activateLayout,
|
||||
@@ -37,12 +40,12 @@ function OrientationBadge({ landscape }) {
|
||||
<span title={landscape ? 'Querformat' : 'Hochformat'}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: 24, height: 24, flexShrink: 0,
|
||||
width: 20, height: 20, flexShrink: 0,
|
||||
borderRadius: 999,
|
||||
background: 'var(--bg-input)',
|
||||
color: 'var(--accent)',
|
||||
}}>
|
||||
<Icon name={landscape ? 'crop_landscape' : 'crop_portrait'} size={14} />
|
||||
<Icon name={landscape ? 'crop_landscape' : 'crop_portrait'} size={12} />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -346,16 +349,12 @@ export default function LayoutsApp() {
|
||||
{folderName}
|
||||
</span>
|
||||
<span className="chip" style={{ fontSize: 8 }}>{items.length}</span>
|
||||
<button
|
||||
className="btn-icon-sm"
|
||||
<BarButton icon="more_vert"
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation()
|
||||
setCtxMenu({ x: ev.clientX, y: ev.clientY, kind: 'folder', id: folderName })
|
||||
}}
|
||||
title="Ordner-Aktionen"
|
||||
>
|
||||
<Icon name="more_vert" size={14} />
|
||||
</button>
|
||||
title="Ordner-Aktionen" />
|
||||
</div>
|
||||
{!isCollapsed && (
|
||||
<div style={{ marginTop: 6, display: 'flex', flexDirection: 'column' }}>
|
||||
@@ -461,26 +460,17 @@ export default function LayoutsApp() {
|
||||
{/* Details des aktuell gewaehlten Layouts */}
|
||||
{selected && (
|
||||
<div style={cardStyle}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 6 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 6 }}>
|
||||
<span style={{ ...labelXs, flex: 1 }}>
|
||||
Details · {selected.name}
|
||||
</span>
|
||||
<button
|
||||
<BarButton icon="add"
|
||||
onClick={() => addDetail(selected.id, null)}
|
||||
className="btn-icon-tonal"
|
||||
title="Neues Detail (zentriert auf Seite)"
|
||||
style={{ marginRight: 4 }}
|
||||
>
|
||||
<Icon name="add" size={13} />
|
||||
</button>
|
||||
<button
|
||||
title="Neues Detail (zentriert auf Seite)" />
|
||||
<BarButton icon="sync"
|
||||
onClick={() => syncLayout(selected.id)}
|
||||
className="btn-icon-tonal"
|
||||
disabled={details.length === 0}
|
||||
title="Alle Details mit ihren Ausschnitten neu synchronisieren"
|
||||
>
|
||||
<Icon name="sync" size={13} />
|
||||
</button>
|
||||
title="Alle Details mit ihren Ausschnitten neu synchronisieren" />
|
||||
</div>
|
||||
|
||||
{details.length === 0 ? (
|
||||
@@ -505,7 +495,7 @@ export default function LayoutsApp() {
|
||||
padding: '6px 8px',
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span className="chip" style={{ flexShrink: 0 }}>#{i + 1}</span>
|
||||
@@ -518,38 +508,28 @@ export default function LayoutsApp() {
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', fontFamily: 'DM Mono, monospace' }}>
|
||||
{Math.round(d.width)}×{Math.round(d.height)}
|
||||
</span>
|
||||
<button
|
||||
className="btn-icon-sm btn-icon-danger"
|
||||
<BarButton icon="delete"
|
||||
onClick={() => {
|
||||
if (window.confirm('Detail loeschen?')) deleteDetail(selected.id, d.id)
|
||||
}}
|
||||
title="Detail loeschen"
|
||||
>
|
||||
<Icon name="delete" size={12} />
|
||||
</button>
|
||||
title="Detail loeschen" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={d.boundAusschnitt || ''}
|
||||
onChange={(e) => bindAusschnitt(selected.id, d.id, e.target.value || null)}
|
||||
style={{ flex: 1, fontSize: 11 }}
|
||||
title="Welcher Ausschnitt auf diesem Detail liegt"
|
||||
>
|
||||
onChange={(v) => bindAusschnitt(selected.id, d.id, v || null)}
|
||||
title="Welcher Ausschnitt auf diesem Detail liegt">
|
||||
<option value="">— kein Ausschnitt —</option>
|
||||
{snaps.map(s => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.name}{s.folder ? ` · ${s.folder}` : ''}{s.scale ? ` · ${s.scale}` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
</BarCombo>
|
||||
<BarButton icon="sync"
|
||||
onClick={() => syncDetail(selected.id, d.id)}
|
||||
disabled={!d.boundAusschnitt}
|
||||
className="btn-icon-sm"
|
||||
title="Gebundenen Ausschnitt neu anwenden"
|
||||
>
|
||||
<Icon name="sync" size={12} />
|
||||
</button>
|
||||
title="Gebundenen Ausschnitt neu anwenden" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -573,49 +553,24 @@ export default function LayoutsApp() {
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)', flex: 1 }}>
|
||||
Layouts
|
||||
</span>
|
||||
{/* PDF-Aktionen: feste Breite damit das Auswahl-Counter den Footer
|
||||
nicht horizontal verschiebt. */}
|
||||
<button
|
||||
{/* PDF-Aktionen — Pill-Stil */}
|
||||
<BarButton icon="picture_as_pdf"
|
||||
onClick={handleExportSelection}
|
||||
className="btn-icon-tonal"
|
||||
disabled={checked.size === 0}
|
||||
title={checked.size > 0
|
||||
? `Auswahl (${checked.size}) als ein PDF exportieren`
|
||||
: 'Erst Layouts ankreuzen'}
|
||||
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }}
|
||||
>
|
||||
<Icon name="picture_as_pdf" size={14} />
|
||||
{checked.size > 0 && (
|
||||
<span style={{ fontSize: 9, fontFamily: 'DM Mono, monospace' }}>
|
||||
{checked.size}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
: 'Erst Layouts ankreuzen'} />
|
||||
<BarButton icon="picture_as_pdf"
|
||||
onClick={() => exportPdfAll(300)}
|
||||
className="btn-icon-tonal"
|
||||
disabled={layouts.length === 0}
|
||||
title="Alle Layouts als ein PDF exportieren"
|
||||
style={{ width: 'auto', minWidth: 26, padding: '0 8px', gap: 3 }}
|
||||
>
|
||||
<Icon name="picture_as_pdf" size={14} />
|
||||
<span style={{ fontSize: 9 }}>·∗</span>
|
||||
</button>
|
||||
title="Alle Layouts als ein PDF exportieren" />
|
||||
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
|
||||
<button
|
||||
<BarButton icon="create_new_folder"
|
||||
onClick={handleNewFolder}
|
||||
className="btn-icon-tonal"
|
||||
title="Neuer Ordner"
|
||||
>
|
||||
<Icon name="create_new_folder" size={14} />
|
||||
</button>
|
||||
<button
|
||||
title="Neuer Ordner" />
|
||||
<BarButton icon="refresh"
|
||||
onClick={() => listLayouts()}
|
||||
className="btn-icon-tonal"
|
||||
title="Aktualisieren"
|
||||
>
|
||||
<Icon name="refresh" size={14} />
|
||||
</button>
|
||||
title="Aktualisieren" />
|
||||
<button
|
||||
onClick={() => openLayoutDialog('new', null)}
|
||||
className="btn-add"
|
||||
@@ -660,14 +615,14 @@ function LayoutRow({ l, active, checked, dragging, forceEditName,
|
||||
onDoubleClick={onActivate}
|
||||
onContextMenu={onContextMenu}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 8px',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '2px 8px 2px 6px',
|
||||
border: '1px solid ' + (active ? 'var(--accent)' : 'var(--border-light)'),
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 999,
|
||||
background: active ? 'var(--bg-item-active)' : 'var(--bg-input)',
|
||||
cursor: 'grab',
|
||||
userSelect: 'none',
|
||||
marginBottom: 4,
|
||||
marginBottom: 3,
|
||||
opacity: dragging ? 0.4 : 1,
|
||||
transition: 'background 0.14s, border-color 0.14s, opacity 0.14s',
|
||||
}}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import LibraryBrowser from './components/LibraryBrowser'
|
||||
import { notifyReady, onMessage, send } from './lib/rhinoBridge'
|
||||
|
||||
+34
-33
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useState } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarButton, BarCombo, BAR_H } from './components/BarControls'
|
||||
import { onMessage, notifyReady,
|
||||
masseSetActive as setActive,
|
||||
masseSavePreset as savePreset,
|
||||
@@ -19,6 +22,18 @@ const labelXs = {
|
||||
fontWeight: 600,
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
function Row({ label, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
@@ -86,35 +101,27 @@ export default function MasseSettingsApp() {
|
||||
{/* Picker + Aktionen */}
|
||||
<Row label="Aktiv">
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={activeId || (active?.id || '')}
|
||||
onChange={(e) => setActive(e.target.value)}
|
||||
style={{ flex: 1, fontSize: 12 }}
|
||||
>
|
||||
onChange={(v) => setActive(v)}>
|
||||
{presets.map(p => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
</BarCombo>
|
||||
<BarButton icon="add"
|
||||
onClick={addNew}
|
||||
className="btn-outlined"
|
||||
style={{ padding: '4px 8px' }}
|
||||
title="Neues Mass anlegen (mit aktuellen Werten als Vorlage)"
|
||||
><Icon name="add" size={13} /></button>
|
||||
<button
|
||||
title="Neues Mass anlegen (mit aktuellen Werten als Vorlage)" />
|
||||
<BarButton icon="delete"
|
||||
onClick={remove}
|
||||
className="btn-outlined"
|
||||
style={{ padding: '4px 8px' }}
|
||||
title="Aktives Mass löschen"
|
||||
disabled={presets.length <= 1}
|
||||
><Icon name="delete" size={13} /></button>
|
||||
title="Aktives Mass löschen" />
|
||||
</div>
|
||||
</Row>
|
||||
|
||||
{active && (
|
||||
<div style={{
|
||||
display: 'flex', flexDirection: 'column', gap: 10,
|
||||
padding: 12, borderRadius: 6,
|
||||
padding: 12, borderRadius: 'var(--r-lg)',
|
||||
background: 'var(--bg-section)',
|
||||
border: '1px solid var(--border-light)',
|
||||
}}>
|
||||
@@ -123,44 +130,38 @@ export default function MasseSettingsApp() {
|
||||
type="text"
|
||||
value={active.name}
|
||||
onChange={(e) => update({ name: e.target.value })}
|
||||
style={{ width: '100%', fontSize: 12, padding: '4px 8px' }}
|
||||
style={{ ...pillInput, width: '100%' }}
|
||||
/>
|
||||
</Row>
|
||||
|
||||
<Row label="Raum-Rundung">
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={active.raumRundung}
|
||||
onChange={(e) => update({ raumRundung: e.target.value })}
|
||||
style={{ width: '100%', fontSize: 12 }}
|
||||
>
|
||||
onChange={(v) => update({ raumRundung: v })}>
|
||||
{Object.entries(RAUM_RUNDUNGS_LABELS).map(([v, l]) => (
|
||||
<option key={v} value={v}>{l}</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Row>
|
||||
|
||||
<Row label="Mass-Dezimalstellen">
|
||||
<select
|
||||
value={active.dimDezimalstellen}
|
||||
onChange={(e) => update({ dimDezimalstellen: parseInt(e.target.value, 10) })}
|
||||
style={{ width: '100%', fontSize: 12 }}
|
||||
>
|
||||
<BarCombo stretch
|
||||
value={String(active.dimDezimalstellen)}
|
||||
onChange={(v) => update({ dimDezimalstellen: parseInt(v, 10) })}>
|
||||
{[0, 1, 2, 3, 4].map(n => (
|
||||
<option key={n} value={n}>{n} {n === 1 ? 'Stelle' : 'Stellen'}</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Row>
|
||||
|
||||
<Row label="Mass-Einheit">
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={active.dimEinheit}
|
||||
onChange={(e) => update({ dimEinheit: e.target.value })}
|
||||
style={{ width: '100%', fontSize: 12 }}
|
||||
>
|
||||
onChange={(v) => update({ dimEinheit: v })}>
|
||||
<option value="m">m (Meter)</option>
|
||||
<option value="cm">cm (Zentimeter)</option>
|
||||
<option value="mm">mm (Millimeter)</option>
|
||||
</select>
|
||||
</BarCombo>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+44
-68
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BarButton, BarCombo, BAR_H } from './components/BarControls'
|
||||
import {
|
||||
onMessage, notifyReady,
|
||||
requestMassstab, setMassstab,
|
||||
@@ -50,6 +53,18 @@ function parseScale(input) {
|
||||
return n > 0 ? n : null
|
||||
}
|
||||
|
||||
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 MassstabApp() {
|
||||
@@ -124,20 +139,6 @@ export default function MassstabApp() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Style-Bausteine ------------------------------------------------------
|
||||
const cellBtn = {
|
||||
fontSize: 11, padding: '0 8px', height: 24,
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 4,
|
||||
background: 'var(--bg-item)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', color: 'var(--text-primary)', cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
}
|
||||
const cellInput = {
|
||||
fontSize: 11, padding: '0 6px', height: 24, minWidth: 0,
|
||||
background: 'var(--bg-input)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', color: 'var(--text-primary)',
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%', height: '100%',
|
||||
@@ -163,13 +164,12 @@ export default function MassstabApp() {
|
||||
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
|
||||
|
||||
{/* Skala-Dropdown */}
|
||||
<select
|
||||
<BarCombo
|
||||
disabled={isPerspective}
|
||||
value={dropdownValue}
|
||||
onChange={(e) => applyDropdown(e.target.value)}
|
||||
style={{ ...cellInput, width: 80 }}
|
||||
title="Massstab wählen"
|
||||
>
|
||||
onChange={applyDropdown}
|
||||
width={84}
|
||||
title="Massstab wählen">
|
||||
<option value="__none__">1:?</option>
|
||||
{PRESETS.map(p => (
|
||||
<option key={p.value} value={String(p.value)}>{p.label}</option>
|
||||
@@ -177,7 +177,7 @@ export default function MassstabApp() {
|
||||
{appliedScale != null && !PRESETS.some(p => p.value === appliedScale) && (
|
||||
<option value={String(appliedScale)}>1:{appliedScale}</option>
|
||||
)}
|
||||
</select>
|
||||
</BarCombo>
|
||||
|
||||
{/* Freitext */}
|
||||
<input
|
||||
@@ -188,49 +188,37 @@ export default function MassstabApp() {
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') applyDraft() }}
|
||||
onBlur={() => { if (draft) applyDraft() }}
|
||||
style={{ ...cellInput, width: 64 }}
|
||||
style={{ ...pillInput, width: 64 }}
|
||||
title="Eigenen Massstab eingeben (Enter)"
|
||||
/>
|
||||
|
||||
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
|
||||
|
||||
{/* Aktions-Buttons */}
|
||||
<button
|
||||
<BarToggle label="100%"
|
||||
disabled={isPerspective || !appliedScale}
|
||||
onClick={apply100}
|
||||
style={cellBtn}
|
||||
title={appliedScale
|
||||
? `Zoom auf eingestellten Massstab snappen (1:${appliedScale >= 10 ? Math.round(appliedScale) : appliedScale})`
|
||||
: 'Erst einen Massstab wählen'}
|
||||
>100%</button>
|
||||
<button onClick={zoomExtents} style={cellBtn} title="Auf gesamten Inhalt zoomen">
|
||||
<Icon name="fit_screen" size={14} />
|
||||
</button>
|
||||
<button onClick={zoomSelection} style={cellBtn} title="Auf Selektion zoomen">
|
||||
<Icon name="center_focus_strong" size={14} />
|
||||
</button>
|
||||
: 'Erst einen Massstab wählen'} />
|
||||
<BarButton icon="fit_screen"
|
||||
onClick={zoomExtents}
|
||||
title="Auf gesamten Inhalt zoomen" />
|
||||
<BarButton icon="center_focus_strong"
|
||||
onClick={zoomSelection}
|
||||
title="Auf Selektion zoomen" />
|
||||
|
||||
<div style={{ width: 1, height: 18, background: 'var(--border)' }} />
|
||||
|
||||
{/* Print-View / Strichstaerken-Toggle
|
||||
Beide Icons werden permanent gerendert; nur display: none togglet,
|
||||
damit die Font-Ligatur nicht neu aufgeloest wird (sonst Flackern). */}
|
||||
<button
|
||||
{/* Print-View / Strichstaerken-Toggle */}
|
||||
<BarToggle
|
||||
icon={state.showLineweights ? 'print' : 'edit'}
|
||||
label={state.showLineweights ? 'Print' : 'Edit'}
|
||||
active={state.showLineweights}
|
||||
onClick={() => setShowLineweights(!state.showLineweights)}
|
||||
style={{
|
||||
...cellBtn,
|
||||
background: state.showLineweights ? 'var(--accent)' : 'var(--bg-item)',
|
||||
color: state.showLineweights ? '#fff' : 'var(--text-primary)',
|
||||
borderColor: state.showLineweights ? 'var(--accent)' : 'var(--border)',
|
||||
}}
|
||||
title={state.showLineweights
|
||||
? 'Strichstärken werden angezeigt (Print-View) — klicken zum Ausschalten'
|
||||
: 'Strichstärken als Hairlines (Edit-View) — klicken um Print-View zu zeigen'}
|
||||
>
|
||||
<Icon name="edit" size={14} style={{ display: state.showLineweights ? 'none' : 'inline-block' }} />
|
||||
<Icon name="print" size={14} style={{ display: state.showLineweights ? 'inline-block' : 'none' }} />
|
||||
<span style={{ fontSize: 10 }}>{state.showLineweights ? 'Print' : 'Edit'}</span>
|
||||
</button>
|
||||
: 'Strichstärken als Hairlines (Edit-View) — klicken um Print-View zu zeigen'} />
|
||||
|
||||
{/* Spacer */}
|
||||
<div style={{ flex: 1 }} />
|
||||
@@ -246,24 +234,16 @@ export default function MassstabApp() {
|
||||
|
||||
{/* DPI-Popover */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<button
|
||||
<BarToggle
|
||||
label={`${Math.round(state.dpi || 96)}dpi${state.dpiSource === 'auto' ? ' · auto' : ''}`}
|
||||
active={state.dpiSource === 'auto'}
|
||||
onClick={() => { setDpiDraft(String(state.dpi || 96)); setDpiOpen(o => !o) }}
|
||||
style={{ ...cellBtn, fontSize: 10, padding: '0 6px', height: 22,
|
||||
color: state.dpiSource === 'auto' ? 'var(--accent)'
|
||||
: state.dpiSource === 'manual' ? 'var(--text-primary)'
|
||||
: 'var(--text-muted)' }}
|
||||
title={`DPI Kalibrierung — aktuell ${Math.round(state.dpi || 96)} dpi (${state.dpiSource || 'default'})`}
|
||||
>
|
||||
{Math.round(state.dpi || 96)}dpi
|
||||
{state.dpiSource === 'auto' && (
|
||||
<span style={{ marginLeft: 3, fontSize: 8, opacity: 0.8 }}>auto</span>
|
||||
)}
|
||||
</button>
|
||||
title={`DPI Kalibrierung — aktuell ${Math.round(state.dpi || 96)} dpi (${state.dpiSource || 'default'})`} />
|
||||
{dpiOpen && (
|
||||
<div style={{
|
||||
position: 'absolute', right: 0, bottom: '100%', marginBottom: 4,
|
||||
background: 'var(--bg-panel)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', padding: 8, display: 'flex',
|
||||
borderRadius: 'var(--r-lg)', padding: 8, display: 'flex',
|
||||
flexDirection: 'column', gap: 6,
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)', zIndex: 10, minWidth: 220,
|
||||
}}>
|
||||
@@ -277,17 +257,13 @@ export default function MassstabApp() {
|
||||
onChange={(e) => setDpiDraft(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') commitDpi() }}
|
||||
autoFocus
|
||||
style={{ ...cellInput, flex: 1 }}
|
||||
style={{ ...pillInput, flex: 1 }}
|
||||
/>
|
||||
<button onClick={commitDpi} style={{ ...cellBtn, fontSize: 10 }}>OK</button>
|
||||
<BarToggle label="OK" active onClick={commitDpi} />
|
||||
</div>
|
||||
<button
|
||||
<BarToggle icon="auto_fix_high" label="Auto-Detect (EDID)"
|
||||
onClick={() => { detectMassstabDpi(); setDpiOpen(false) }}
|
||||
style={{ ...cellBtn, fontSize: 10, justifyContent: 'flex-start' }}
|
||||
title="DPI automatisch über EDID des Bildschirms ermitteln"
|
||||
>
|
||||
<Icon name="auto_fix_high" size={12} /> Auto-Detect (EDID)
|
||||
</button>
|
||||
title="DPI automatisch über EDID des Bildschirms ermitteln" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarCombo, BarButton, BAR_H } from './components/BarControls'
|
||||
|
||||
+32
-27
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BAR_H } from './components/BarControls'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function send(type, payload = {}) {
|
||||
@@ -7,6 +10,18 @@ function send(type, payload = {}) {
|
||||
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
function Field({ label, hint, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, padding: '6px 0' }}>
|
||||
@@ -35,13 +50,9 @@ function Radio({ value, options, onChange }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{options.map(o => (
|
||||
<button key={o.value}
|
||||
onClick={() => onChange(o.value)}
|
||||
className={value === o.value ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '4px 10px', fontSize: 10 }}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
<BarToggle key={o.value} label={o.label}
|
||||
active={value === o.value}
|
||||
onClick={() => onChange(o.value)} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -168,36 +179,31 @@ export default function OsmApp() {
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleSearch() }}
|
||||
placeholder="Adresse oder Ortsname"
|
||||
style={{ flex: 1, fontSize: 11, padding: '5px 8px' }}
|
||||
style={{ ...pillInput, flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
className="btn-outlined"
|
||||
<BarToggle label={searching ? '…' : 'Suchen'}
|
||||
onClick={handleSearch}
|
||||
disabled={searching || !searchText.trim()}
|
||||
style={{ padding: '4px 10px', fontSize: 11 }}
|
||||
>
|
||||
{searching ? '…' : 'Suchen'}
|
||||
</button>
|
||||
disabled={searching || !searchText.trim()} />
|
||||
</Field>
|
||||
|
||||
<Field label="ODER LV95-KOORDS (E / N)"
|
||||
hint="Falls aus Swisstopo-Import übernommen">
|
||||
<input placeholder="E"
|
||||
onChange={(e) => handleManualCoords(e.target.value, center?.n || '')}
|
||||
style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }} />
|
||||
style={{ ...pillInput, width: 110, fontFamily: 'DM Mono, monospace' }} />
|
||||
<span style={{ color: 'var(--text-muted)' }}>/</span>
|
||||
<input placeholder="N"
|
||||
onChange={(e) => handleManualCoords(center?.e || '', e.target.value)}
|
||||
style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }} />
|
||||
style={{ ...pillInput, width: 110, fontFamily: 'DM Mono, monospace' }} />
|
||||
</Field>
|
||||
|
||||
{center && (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '8px 10px',
|
||||
padding: '8px 12px',
|
||||
background: 'var(--accent-dim)',
|
||||
border: '1px solid var(--accent-border)',
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 999,
|
||||
marginTop: 4,
|
||||
}}>
|
||||
<Icon name="location_on" size={14} style={{ color: 'var(--accent)' }} />
|
||||
@@ -276,7 +282,7 @@ export default function OsmApp() {
|
||||
fontFamily: 'DM Mono, monospace',
|
||||
background: 'var(--bg-base)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
color: 'var(--text-secondary)',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}>
|
||||
@@ -296,13 +302,12 @@ export default function OsmApp() {
|
||||
<div style={{ fontSize: 9, color: 'var(--text-muted)', flex: 1 }}>
|
||||
Quelle: Overpass-API · © OpenStreetMap-Mitwirkende (ODbL)
|
||||
</div>
|
||||
<button className="btn-text" onClick={() => send('CANCEL')}>Abbrechen</button>
|
||||
<button className="btn-contained"
|
||||
onClick={handleImport}
|
||||
disabled={running || !center}>
|
||||
<Icon name="download" size={12} />
|
||||
{running ? 'Lädt…' : 'Importieren'}
|
||||
</button>
|
||||
<BarToggle label="Abbrechen" onClick={() => send('CANCEL')} />
|
||||
<BarToggle icon="download"
|
||||
label={running ? 'Lädt…' : 'Importieren'}
|
||||
active
|
||||
disabled={running || !center}
|
||||
onClick={handleImport} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+101
-111
@@ -1,6 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import ContextMenu from './components/ContextMenu'
|
||||
import { BarToggle, BarButton, BarCombo, BAR_H } from './components/BarControls'
|
||||
import {
|
||||
onMessage, notifyReady,
|
||||
setOverridesEnabled, addRule, updateRule, deleteRule,
|
||||
@@ -29,6 +32,18 @@ const labelXs = {
|
||||
fontWeight: 600,
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ConditionLeaf({ cond, layers, onChange, onRemove, canRemove }) {
|
||||
@@ -42,18 +57,14 @@ function ConditionLeaf({ cond, layers, onChange, onRemove, canRemove }) {
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={t}
|
||||
onChange={(e) => onChange({ ...cond, type: e.target.value })}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
onChange={(v) => onChange({ ...cond, type: v })}>
|
||||
{COND_TYPES.map(c => <option key={c.value} value={c.value}>{c.label}</option>)}
|
||||
</select>
|
||||
</BarCombo>
|
||||
{canRemove && (
|
||||
<button onClick={onRemove} className="btn-icon-danger"
|
||||
title="Diese Bedingung entfernen">
|
||||
<Icon name="close" size={14} />
|
||||
</button>
|
||||
<BarButton icon="close" onClick={onRemove}
|
||||
title="Diese Bedingung entfernen" />
|
||||
)}
|
||||
</div>
|
||||
{t === 'user_string' && (
|
||||
@@ -61,34 +72,31 @@ function ConditionLeaf({ cond, layers, onChange, onRemove, canRemove }) {
|
||||
type="text" placeholder="Key"
|
||||
value={cond?.key || ''}
|
||||
onChange={(e) => onChange({ ...cond, key: e.target.value })}
|
||||
style={{ width: '100%' }}
|
||||
style={{ ...pillInput, width: '100%' }}
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<select
|
||||
<BarCombo
|
||||
value={op}
|
||||
onChange={(e) => onChange({ ...cond, operator: e.target.value })}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
onChange={(v) => onChange({ ...cond, operator: v })}
|
||||
width={100}>
|
||||
{OPS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
</BarCombo>
|
||||
{t === 'layer_name' ? (
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={cond?.value || ''}
|
||||
onChange={(e) => onChange({ ...cond, value: e.target.value })}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
onChange={(v) => onChange({ ...cond, value: v })}>
|
||||
<option value="">—</option>
|
||||
{(layers || []).map(l => (
|
||||
<option key={l.fullPath} value={l.fullPath}>{l.fullPath}</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
) : (
|
||||
<input
|
||||
type="text" placeholder="Wert"
|
||||
value={cond?.value || ''}
|
||||
onChange={(e) => onChange({ ...cond, value: e.target.value })}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, minWidth: 0 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -124,16 +132,14 @@ function ConditionsEditor({ rule, layers, onChange }) {
|
||||
{conds.length > 1 && (
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>Logik:</span>
|
||||
<button
|
||||
<BarToggle label="AND"
|
||||
active={logic === 'and'}
|
||||
onClick={() => setLogic('and')}
|
||||
className={logic === 'and' ? 'btn-contained' : 'btn-outlined'}
|
||||
title="Alle Bedingungen müssen zutreffen"
|
||||
>AND</button>
|
||||
<button
|
||||
title="Alle Bedingungen müssen zutreffen" />
|
||||
<BarToggle label="OR"
|
||||
active={logic === 'or'}
|
||||
onClick={() => setLogic('or')}
|
||||
className={logic === 'or' ? 'btn-contained' : 'btn-outlined'}
|
||||
title="Mindestens eine Bedingung muss zutreffen"
|
||||
>OR</button>
|
||||
title="Mindestens eine Bedingung muss zutreffen" />
|
||||
</div>
|
||||
)}
|
||||
{conds.map((c, i) => (
|
||||
@@ -146,11 +152,11 @@ function ConditionsEditor({ rule, layers, onChange }) {
|
||||
canRemove={conds.length > 1}
|
||||
/>
|
||||
))}
|
||||
<button onClick={add} className="btn-outlined" style={{ alignSelf: 'flex-start' }}
|
||||
title="Weitere Bedingung hinzufügen">
|
||||
<Icon name="add" size={14} />
|
||||
<span>Bedingung</span>
|
||||
</button>
|
||||
<div style={{ alignSelf: 'flex-start' }}>
|
||||
<BarToggle icon="add" label="Bedingung"
|
||||
onClick={add}
|
||||
title="Weitere Bedingung hinzufügen" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -200,14 +206,16 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
|
||||
type="color"
|
||||
value={a.color || '#888888'}
|
||||
onChange={(e) => setProp('color', e.target.value)}
|
||||
style={{ width: 36, height: 26, padding: 2, flexShrink: 0 }}
|
||||
style={{ width: 32, height: BAR_H, padding: 0, flexShrink: 0,
|
||||
border: '1px solid var(--border)', borderRadius: 999,
|
||||
background: 'var(--bg-input)' }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={a.color || ''}
|
||||
placeholder="#rrggbb"
|
||||
onChange={(e) => setProp('color', e.target.value)}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, minWidth: 0, fontFamily: 'DM Mono, monospace' }}
|
||||
/>
|
||||
</ActionRow>
|
||||
|
||||
@@ -220,7 +228,7 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
|
||||
type="number" step={0.05} min={0}
|
||||
value={a.lineweight ?? ''}
|
||||
onChange={(e) => setProp('lineweight', parseFloat(e.target.value) || 0)}
|
||||
style={{ width: 80 }}
|
||||
style={{ ...pillInput, width: 80, fontFamily: 'DM Mono, monospace' }}
|
||||
/>
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>mm</span>
|
||||
</ActionRow>
|
||||
@@ -230,14 +238,12 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
|
||||
active={'linetype' in a}
|
||||
onToggle={(e) => setProp('linetype', e.target.checked ? (a.linetype || 'Continuous') : '')}
|
||||
>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={a.linetype || ''}
|
||||
onChange={(e) => setProp('linetype', e.target.value)}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
onChange={(v) => setProp('linetype', v)}>
|
||||
<option value="">—</option>
|
||||
{(linetypes || []).map(lt => <option key={lt} value={lt}>{lt}</option>)}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</ActionRow>
|
||||
|
||||
<ActionRow
|
||||
@@ -245,14 +251,12 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
|
||||
active={'hatchPattern' in a}
|
||||
onToggle={(e) => setProp('hatchPattern', e.target.checked ? (a.hatchPattern || 'Solid') : '')}
|
||||
>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={a.hatchPattern || ''}
|
||||
onChange={(e) => setProp('hatchPattern', e.target.value)}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
onChange={(v) => setProp('hatchPattern', v)}>
|
||||
<option value="">—</option>
|
||||
{(hatchPatterns || []).map(hp => <option key={hp} value={hp}>{hp}</option>)}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</ActionRow>
|
||||
|
||||
<ActionRow
|
||||
@@ -264,7 +268,7 @@ function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) {
|
||||
type="number" step={0.1} min={0.001}
|
||||
value={a.hatchScale ?? ''}
|
||||
onChange={(e) => setProp('hatchScale', parseFloat(e.target.value) || 1.0)}
|
||||
style={{ width: 80 }}
|
||||
style={{ ...pillInput, width: 80, fontFamily: 'DM Mono, monospace' }}
|
||||
/>
|
||||
</ActionRow>
|
||||
|
||||
@@ -322,12 +326,11 @@ function RuleCard({ rule, index, total, layers, linetypes, hatchPatterns, onPatc
|
||||
value={rule.name || ''}
|
||||
placeholder="Regel-Name"
|
||||
onChange={(e) => onPatch({ ...rule, name: e.target.value })}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, minWidth: 0 }}
|
||||
/>
|
||||
<button onClick={() => setOpen(!open)} className="btn-icon"
|
||||
title={open ? 'Einklappen' : 'Bearbeiten'}>
|
||||
<Icon name={open ? 'expand_less' : 'edit'} size={14} />
|
||||
</button>
|
||||
<BarButton icon={open ? 'expand_less' : 'edit'}
|
||||
onClick={() => setOpen(!open)}
|
||||
title={open ? 'Einklappen' : 'Bearbeiten'} />
|
||||
</div>
|
||||
|
||||
{!open && (
|
||||
@@ -339,23 +342,22 @@ function RuleCard({ rule, index, total, layers, linetypes, hatchPatterns, onPatc
|
||||
|
||||
{open && (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 2, marginTop: 8 }}>
|
||||
<button onClick={() => onMoveUp()} disabled={index === 0}
|
||||
className="btn-icon-sm" title="Prio höher (nach oben)">
|
||||
<Icon name="arrow_upward" size={14} />
|
||||
</button>
|
||||
<button onClick={() => onMoveDown()} disabled={index === total - 1}
|
||||
className="btn-icon-sm" title="Prio tiefer (nach unten)">
|
||||
<Icon name="arrow_downward" size={14} />
|
||||
</button>
|
||||
<button onClick={() => onDuplicate()} className="btn-icon-sm" title="Duplizieren">
|
||||
<Icon name="content_copy" size={14} />
|
||||
</button>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 8 }}>
|
||||
<BarButton icon="arrow_upward"
|
||||
onClick={() => onMoveUp()}
|
||||
disabled={index === 0}
|
||||
title="Prio höher (nach oben)" />
|
||||
<BarButton icon="arrow_downward"
|
||||
onClick={() => onMoveDown()}
|
||||
disabled={index === total - 1}
|
||||
title="Prio tiefer (nach unten)" />
|
||||
<BarButton icon="content_copy"
|
||||
onClick={() => onDuplicate()}
|
||||
title="Duplizieren" />
|
||||
<div style={{ flex: 1 }} />
|
||||
<button onClick={() => { if (confirm(`Regel "${rule.name}" löschen?`)) onDelete() }}
|
||||
className="btn-icon-danger" title="Löschen">
|
||||
<Icon name="delete" size={14} />
|
||||
</button>
|
||||
<BarButton icon="delete"
|
||||
onClick={() => { if (confirm(`Regel "${rule.name}" löschen?`)) onDelete() }}
|
||||
title="Löschen" />
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
@@ -476,10 +478,9 @@ export default function OverridesApp() {
|
||||
borderRadius: 'var(--r-lg)',
|
||||
}}>
|
||||
<span style={labelXs}>Override-Kombinationen</span>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={selectedPreset}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value
|
||||
onChange={(v) => {
|
||||
setSelectedPreset(v)
|
||||
if (v) {
|
||||
loadPreset(v, 'replace')
|
||||
@@ -487,40 +488,36 @@ export default function OverridesApp() {
|
||||
clearOverrideRules()
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
title="Kombination zum Bearbeiten oeffnen"
|
||||
>
|
||||
title="Kombination zum Bearbeiten oeffnen">
|
||||
<option value="">— neu / keine —</option>
|
||||
{(state.presets || []).map(p => (
|
||||
<option key={p.name} value={p.name}>
|
||||
{p.name} ({p.ruleCount})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (selectedPreset) { savePreset(selectedPreset); return }
|
||||
const existing = (state.presets || []).map(p => p.name)
|
||||
const def = `Kombination ${existing.length + 1}`
|
||||
const name = window.prompt('Name für neue Kombination:', def)
|
||||
if (!name || !name.trim()) return
|
||||
const t = name.trim()
|
||||
if (existing.includes(t) && !window.confirm(`Kombination "${t}" überschreiben?`)) return
|
||||
savePreset(t)
|
||||
setSelectedPreset(t)
|
||||
}}
|
||||
disabled={state.rules.length === 0}
|
||||
className="btn-outlined"
|
||||
style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}
|
||||
title={selectedPreset
|
||||
? `Änderungen in "${selectedPreset}" speichern`
|
||||
: 'Aktuelle Regeln als neue Kombination speichern'}
|
||||
>
|
||||
<Icon name="save" size={14} />
|
||||
<span>{selectedPreset ? 'Speichern' : 'Als Kombination speichern…'}</span>
|
||||
</button>
|
||||
<button
|
||||
<div style={{ flex: 1 }}>
|
||||
<BarToggle icon="save"
|
||||
label={selectedPreset ? 'Speichern' : 'Als Kombination speichern…'}
|
||||
active={state.rules.length > 0}
|
||||
disabled={state.rules.length === 0}
|
||||
onClick={() => {
|
||||
if (selectedPreset) { savePreset(selectedPreset); return }
|
||||
const existing = (state.presets || []).map(p => p.name)
|
||||
const def = `Kombination ${existing.length + 1}`
|
||||
const name = window.prompt('Name für neue Kombination:', def)
|
||||
if (!name || !name.trim()) return
|
||||
const t = name.trim()
|
||||
if (existing.includes(t) && !window.confirm(`Kombination "${t}" überschreiben?`)) return
|
||||
savePreset(t)
|
||||
setSelectedPreset(t)
|
||||
}}
|
||||
title={selectedPreset
|
||||
? `Änderungen in "${selectedPreset}" speichern`
|
||||
: 'Aktuelle Regeln als neue Kombination speichern'} />
|
||||
</div>
|
||||
<BarButton icon="delete"
|
||||
onClick={() => {
|
||||
if (!selectedPreset) return
|
||||
if (!window.confirm(`Kombination "${selectedPreset}" dauerhaft loeschen?`)) return
|
||||
@@ -528,11 +525,7 @@ export default function OverridesApp() {
|
||||
setSelectedPreset('')
|
||||
}}
|
||||
disabled={!selectedPreset}
|
||||
className="btn-icon-danger"
|
||||
title="Gewaehlte Kombination dauerhaft loeschen"
|
||||
>
|
||||
<Icon name="delete" size={14} />
|
||||
</button>
|
||||
title="Gewaehlte Kombination dauerhaft loeschen" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -551,10 +544,9 @@ export default function OverridesApp() {
|
||||
>
|
||||
<Icon name="add" size={16} />
|
||||
</button>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={selectedTemplate}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value
|
||||
onChange={(v) => {
|
||||
if (!v) { setSelectedTemplate(''); return }
|
||||
if (v === '__delete__') {
|
||||
if (!selectedTemplate) return
|
||||
@@ -566,9 +558,7 @@ export default function OverridesApp() {
|
||||
addFromTemplate(v)
|
||||
setSelectedTemplate(v)
|
||||
}}
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
title="Regel aus Vorlage einfuegen"
|
||||
>
|
||||
title="Regel aus Vorlage einfuegen">
|
||||
<option value="">+ Aus Vorlage…</option>
|
||||
{(state.ruleTemplates || []).map(t => (
|
||||
<option key={t.name} value={t.name}>{t.name}</option>
|
||||
@@ -579,7 +569,7 @@ export default function OverridesApp() {
|
||||
<option value="__delete__">🗑 "{selectedTemplate}" loeschen</option>
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
</BarCombo>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic', lineHeight: 1.4 }}>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect } from 'react'
|
||||
import ProjectSettingsDialog from './components/ProjectSettingsDialog'
|
||||
import { notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function bridgeSend(type, payload = {}) {
|
||||
if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return }
|
||||
const json = JSON.stringify({ type, payload })
|
||||
document.title = 'RHINOMSG::' + json
|
||||
}
|
||||
import { notifyReady, send as bridgeSend } from './lib/rhinoBridge'
|
||||
|
||||
export default function ProjectSettingsApp() {
|
||||
const initial = (typeof window !== 'undefined' && window.PANEL_PARAMS) || {}
|
||||
|
||||
+35
-30
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle, BAR_H } from './components/BarControls'
|
||||
import { onMessage, notifyReady } from './lib/rhinoBridge'
|
||||
|
||||
function send(type, payload = {}) {
|
||||
@@ -7,6 +10,18 @@ function send(type, payload = {}) {
|
||||
document.title = 'RHINOMSG::' + JSON.stringify({ type, payload })
|
||||
}
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
function Field({ label, hint, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, padding: '6px 0' }}>
|
||||
@@ -35,14 +50,10 @@ function Radio({ value, options, onChange }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{options.map(o => (
|
||||
<button key={o.value}
|
||||
<BarToggle key={o.value} label={o.label}
|
||||
active={value === o.value}
|
||||
onClick={() => onChange(o.value)}
|
||||
className={value === o.value ? 'btn-contained' : 'btn-outlined'}
|
||||
style={{ padding: '4px 10px', fontSize: 10 }}
|
||||
title={o.hint || ''}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
title={o.hint || ''} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
@@ -198,16 +209,11 @@ export default function SwisstopoApp() {
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleSearch() }}
|
||||
placeholder="Adresse oder Ortsname"
|
||||
style={{ flex: 1, fontSize: 11, padding: '5px 8px' }}
|
||||
style={{ ...pillInput, flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
className="btn-outlined"
|
||||
<BarToggle label={searching ? '…' : 'Suchen'}
|
||||
onClick={handleSearch}
|
||||
disabled={searching || !searchText.trim()}
|
||||
style={{ padding: '4px 10px', fontSize: 11 }}
|
||||
>
|
||||
{searching ? '…' : 'Suchen'}
|
||||
</button>
|
||||
disabled={searching || !searchText.trim()} />
|
||||
</Field>
|
||||
|
||||
<Field label="ODER LV95-KOORDS (E / N)"
|
||||
@@ -215,23 +221,23 @@ export default function SwisstopoApp() {
|
||||
<input
|
||||
placeholder="E"
|
||||
onChange={(e) => handleManualCoords(e.target.value, center?.n || '')}
|
||||
style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }}
|
||||
style={{ ...pillInput, width: 110, fontFamily: 'DM Mono, monospace' }}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)' }}>/</span>
|
||||
<input
|
||||
placeholder="N"
|
||||
onChange={(e) => handleManualCoords(center?.e || '', e.target.value)}
|
||||
style={{ width: 110, fontSize: 11, fontFamily: 'DM Mono, monospace', padding: '5px 8px' }}
|
||||
style={{ ...pillInput, width: 110, fontFamily: 'DM Mono, monospace' }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{center && (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '8px 10px',
|
||||
padding: '8px 12px',
|
||||
background: 'var(--accent-dim)',
|
||||
border: '1px solid var(--accent-border)',
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 999,
|
||||
marginTop: 4,
|
||||
}}>
|
||||
<Icon name="location_on" size={14} style={{ color: 'var(--accent)' }} />
|
||||
@@ -395,7 +401,7 @@ export default function SwisstopoApp() {
|
||||
<input type="text"
|
||||
value={terrainVolumeDepth}
|
||||
onChange={(e) => setTerrainVolumeDepth(e.target.value)}
|
||||
style={{ width: 60, textAlign: 'right' }} />
|
||||
style={{ ...pillInput, width: 60, textAlign: 'right' }} />
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
m unter tiefstem Punkt
|
||||
</span>
|
||||
@@ -451,7 +457,7 @@ export default function SwisstopoApp() {
|
||||
fontSize: 10, fontFamily: 'DM Mono, monospace',
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)',
|
||||
borderRadius: 'var(--r-lg)',
|
||||
padding: 8,
|
||||
maxHeight: 140,
|
||||
overflowY: 'auto',
|
||||
@@ -474,15 +480,14 @@ export default function SwisstopoApp() {
|
||||
<div style={{ flex: 1, fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
{center ? `Tiles werden im Projekt-Ordner neben der .3dm gecacht (Fallback: ~/Library/Caches/Dossier/swisstopo/ wenn ungespeichert)` : 'Wähle zuerst einen Standort'}
|
||||
</div>
|
||||
<button className="btn-text" onClick={() => send('CANCEL', {})}
|
||||
disabled={running}>
|
||||
{done ? 'Schliessen' : 'Abbrechen'}
|
||||
</button>
|
||||
<button className="btn-contained" onClick={handleImport}
|
||||
disabled={!center || running}>
|
||||
<Icon name="download" size={13} />
|
||||
<span>{running ? 'Importiere…' : 'Importieren'}</span>
|
||||
</button>
|
||||
<BarToggle label={done ? 'Schliessen' : 'Abbrechen'}
|
||||
onClick={() => send('CANCEL', {})}
|
||||
disabled={running} />
|
||||
<BarToggle icon="download"
|
||||
label={running ? 'Importiere…' : 'Importieren'}
|
||||
active
|
||||
onClick={handleImport}
|
||||
disabled={!center || running} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import SymbolPicker from './components/SymbolPicker'
|
||||
import { notifyReady, onMessage, send } from './lib/rhinoBridge'
|
||||
|
||||
+5
-13
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { BarToggle } from './components/BarControls'
|
||||
import { onMessage, notifyReady, send } from './lib/rhinoBridge'
|
||||
|
||||
const SYMBOL_GROUPS = [
|
||||
@@ -719,19 +722,8 @@ export default function TextEditorApp() {
|
||||
|
||||
{/* Bottom Buttons */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 6 }}>
|
||||
<Pill onClick={onCancel}>Abbrechen</Pill>
|
||||
<button onClick={onCommit}
|
||||
style={{
|
||||
height: BAR_H + 2, padding: '0 14px',
|
||||
background: 'var(--accent)',
|
||||
color: 'var(--bg-panel)',
|
||||
border: '1px solid var(--accent)',
|
||||
borderRadius: 999,
|
||||
fontSize: 11, fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
appearance: 'none', WebkitAppearance: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}}>Einfügen</button>
|
||||
<BarToggle label="Abbrechen" onClick={onCancel} />
|
||||
<BarToggle label="Einfügen" active onClick={onCommit} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+43
-23
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { notifyReady, runRhinoCommand } from './lib/rhinoBridge'
|
||||
@@ -52,34 +54,53 @@ const TOOLS = {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ToolButton({ icon, label, cmd, tip }) {
|
||||
function ToolPill({ icon, label, cmd, tip }) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => runRhinoCommand(cmd)}
|
||||
title={`${tip} (${cmd})`}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--accent)'
|
||||
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'var(--border-light)'
|
||||
e.currentTarget.style.background = 'var(--bg-input)'
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '6px 8px',
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
background: 'var(--bg-item)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', color: 'var(--text-primary)',
|
||||
cursor: 'pointer', textAlign: 'left',
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '5px 10px 5px 8px',
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 999,
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s, border-color 0.1s',
|
||||
fontSize: 11, fontWeight: 500,
|
||||
color: 'var(--text-primary)',
|
||||
whiteSpace: 'nowrap',
|
||||
appearance: 'none', WebkitAppearance: 'none',
|
||||
}}
|
||||
>
|
||||
<Icon name={icon} size={16} style={{ flexShrink: 0 }} />
|
||||
<span style={{ fontSize: 11, fontWeight: 500 }}>{label}</span>
|
||||
<Icon name={icon} size={14} style={{ color: 'var(--accent)', flexShrink: 0 }} />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function GroupLabel({ children }) {
|
||||
function PillGroup({ label, children }) {
|
||||
return (
|
||||
<div style={{
|
||||
fontSize: 9, color: 'var(--text-muted)', textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
padding: '8px 4px 4px',
|
||||
borderTop: '1px solid var(--border)',
|
||||
}}>{children}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||||
<span style={{
|
||||
fontSize: 9, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.08em', textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
{label}
|
||||
</span>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -93,20 +114,19 @@ export default function WerkzeugeApp() {
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%', height: '100%',
|
||||
display: 'flex', flexDirection: 'column', gap: 0,
|
||||
padding: 6,
|
||||
display: 'flex', flexDirection: 'column', gap: 10,
|
||||
padding: 10,
|
||||
fontFamily: 'var(--font)', color: 'var(--text-primary)',
|
||||
background: 'var(--bg-base)',
|
||||
boxSizing: 'border-box',
|
||||
overflowY: 'auto', overflowX: 'hidden',
|
||||
}}>
|
||||
{groups.map(([title, items], gi) => (
|
||||
<div key={title} style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<GroupLabel>{title}</GroupLabel>
|
||||
{groups.map(([title, items]) => (
|
||||
<PillGroup key={title} label={title}>
|
||||
{items.map(([icon, label, cmd, tip]) => (
|
||||
<ToolButton key={cmd} icon={icon} label={label} cmd={cmd} tip={tip} />
|
||||
<ToolPill key={cmd} icon={icon} label={label} cmd={cmd} tip={tip} />
|
||||
))}
|
||||
</div>
|
||||
</PillGroup>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import GeschossManager from './components/GeschossManager'
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BarCombo, BAR_H } from './BarControls'
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
// Erzeugt ein vollstaendiges Draft-Array fuer einen Preset.
|
||||
// Layer die im Preset nicht enthalten sind werden auf Default (visible=true,
|
||||
@@ -31,8 +46,6 @@ export default function AusschnittLayerDialog({
|
||||
const [filter, setFilter] = useState('')
|
||||
const [newName, setNewName] = useState('')
|
||||
|
||||
// Wenn die Layer-Liste (von Backend) sich aendert wegen Doc-Update,
|
||||
// resetten wir den Draft — aber nur wenn nicht dirty.
|
||||
useEffect(() => {
|
||||
if (dirty) return
|
||||
if (selectedPreset === null) {
|
||||
@@ -133,17 +146,17 @@ export default function AusschnittLayerDialog({
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<Icon name="layers" size={16} style={{ color: 'var(--text-secondary)' }} />
|
||||
<Icon name="layers" size={16} style={{ color: 'var(--accent)' }} />
|
||||
<span style={{ flex: 1, fontWeight: 600, fontSize: 12 }}>
|
||||
{snapName}
|
||||
</span>
|
||||
{dirty && (
|
||||
<span style={{ fontSize: 10, color: 'var(--warn)',
|
||||
padding: '2px 6px', borderRadius: 'var(--r)',
|
||||
padding: '2px 6px', borderRadius: 999,
|
||||
background: 'var(--warn-dim)' }}
|
||||
title="Ungespeicherte Änderungen">●</span>
|
||||
)}
|
||||
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px' }}>×</button>
|
||||
<BarButton icon="close" onClick={onClose} title="Schliessen" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -156,11 +169,9 @@ export default function AusschnittLayerDialog({
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span className="label-xs" style={{ flexShrink: 0 }}>Kombination</span>
|
||||
<select
|
||||
<BarCombo stretch
|
||||
value={selectedPreset || ''}
|
||||
onChange={(ev) => pickPreset(ev.target.value || null)}
|
||||
style={{ flex: 1, fontSize: 11 }}
|
||||
>
|
||||
onChange={(v) => pickPreset(v || null)}>
|
||||
<option value="">— Aktueller Zustand —</option>
|
||||
{presets.length > 0 && <option disabled>──────────</option>}
|
||||
{presets.map(p => (
|
||||
@@ -168,31 +179,20 @@ export default function AusschnittLayerDialog({
|
||||
{p.name} ({p.layers?.length || 0} Ebenen)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</BarCombo>
|
||||
{selectedPreset && (
|
||||
<button
|
||||
className="btn-icon-sm"
|
||||
<BarButton icon="delete"
|
||||
onClick={deleteSelected}
|
||||
title="Diese Kombination löschen"
|
||||
style={{ color: 'var(--danger)' }}
|
||||
>
|
||||
<Icon name="delete" size={13} />
|
||||
</button>
|
||||
title="Diese Kombination löschen" />
|
||||
)}
|
||||
</div>
|
||||
{selectedPreset && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<button
|
||||
className="btn-contained"
|
||||
onClick={savePresetChanges}
|
||||
<BarToggle icon="save" label="Speichern"
|
||||
active={dirty}
|
||||
disabled={!dirty}
|
||||
style={{ fontSize: 10, padding: '3px 10px',
|
||||
opacity: dirty ? 1 : 0.5 }}
|
||||
title={dirty ? `Änderungen in "${selectedPreset}" speichern` : 'Keine Änderungen'}
|
||||
>
|
||||
<Icon name="save" size={12} />
|
||||
<span>Speichern</span>
|
||||
</button>
|
||||
onClick={savePresetChanges}
|
||||
title={dirty ? `Änderungen in "${selectedPreset}" speichern` : 'Keine Änderungen'} />
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
Änderungen werden NICHT automatisch gespeichert.
|
||||
</span>
|
||||
@@ -204,17 +204,12 @@ export default function AusschnittLayerDialog({
|
||||
onChange={(ev) => setNewName(ev.target.value)}
|
||||
onKeyDown={(ev) => { if (ev.key === 'Enter') saveAsNew() }}
|
||||
placeholder="Aktuelle Auswahl als neue Kombination speichern…"
|
||||
style={{ flex: 1, fontSize: 10 }}
|
||||
style={{ ...pillInput, flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
className="btn-outlined"
|
||||
<BarToggle icon="add" label="Neu"
|
||||
onClick={saveAsNew}
|
||||
disabled={!newName.trim()}
|
||||
title="Aktuelle Auswahl unter diesem Namen speichern"
|
||||
style={{ fontSize: 10, padding: '3px 8px' }}
|
||||
>
|
||||
<Icon name="add" size={12} /> Neu
|
||||
</button>
|
||||
title="Aktuelle Auswahl unter diesem Namen speichern" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -228,17 +223,17 @@ export default function AusschnittLayerDialog({
|
||||
value={filter}
|
||||
onChange={(ev) => setFilter(ev.target.value)}
|
||||
placeholder="Filter..."
|
||||
style={{ flex: 1, fontSize: 10, padding: '3px 6px' }}
|
||||
style={{ ...pillInput, flex: 1 }}
|
||||
/>
|
||||
<button className="btn-icon-xs" onClick={() => setAll('visible', true)} title="Alle (gefiltert) sichtbar">
|
||||
<Icon name="visibility" size={12} />
|
||||
</button>
|
||||
<button className="btn-icon-xs" onClick={() => setAll('visible', false)} title="Alle (gefiltert) ausblenden">
|
||||
<Icon name="visibility_off" size={12} />
|
||||
</button>
|
||||
<button className="btn-icon-xs" onClick={() => setAll('locked', false)} title="Alle (gefiltert) entsperren">
|
||||
<Icon name="lock_open" size={12} />
|
||||
</button>
|
||||
<BarButton icon="visibility"
|
||||
onClick={() => setAll('visible', true)}
|
||||
title="Alle (gefiltert) sichtbar" />
|
||||
<BarButton icon="visibility_off"
|
||||
onClick={() => setAll('visible', false)}
|
||||
title="Alle (gefiltert) ausblenden" />
|
||||
<BarButton icon="lock_open"
|
||||
onClick={() => setAll('locked', false)}
|
||||
title="Alle (gefiltert) entsperren" />
|
||||
</div>
|
||||
|
||||
{/* Layer-Liste */}
|
||||
@@ -254,7 +249,7 @@ export default function AusschnittLayerDialog({
|
||||
key={l.id}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '4px 14px',
|
||||
padding: '3px 14px',
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
background: 'var(--bg-item)',
|
||||
opacity: l.visible ? 1 : 0.5,
|
||||
@@ -272,16 +267,16 @@ export default function AusschnittLayerDialog({
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
}} title={l.fullPath}>{l.fullPath || l.name}</span>
|
||||
<button
|
||||
className={`btn-icon-xs ${l.visible ? 'is-on' : ''}`}
|
||||
<BarButton
|
||||
icon={l.visible ? 'visibility' : 'visibility_off'}
|
||||
active={l.visible}
|
||||
onClick={() => toggle(l.id, 'visible')}
|
||||
title={l.visible ? 'Ausblenden' : 'Einblenden'}
|
||||
><Icon name={l.visible ? 'visibility' : 'visibility_off'} size={12} /></button>
|
||||
<button
|
||||
className={`btn-icon-xs ${l.locked ? 'is-on' : ''}`}
|
||||
title={l.visible ? 'Ausblenden' : 'Einblenden'} />
|
||||
<BarButton
|
||||
icon={l.locked ? 'lock' : 'lock_open'}
|
||||
active={l.locked}
|
||||
onClick={() => toggle(l.id, 'locked')}
|
||||
title={l.locked ? 'Entsperren' : 'Sperren'}
|
||||
><Icon name={l.locked ? 'lock' : 'lock_open'} size={12} /></button>
|
||||
title={l.locked ? 'Entsperren' : 'Sperren'} />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -297,12 +292,11 @@ export default function AusschnittLayerDialog({
|
||||
<div style={{ flex: 1, fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
{draft.filter(l => l.visible).length} / {draft.length} sichtbar
|
||||
</div>
|
||||
<button className="btn-text" onClick={onClose}>Schliessen</button>
|
||||
<button className="btn-contained" onClick={applyToDoc}
|
||||
title="Aktuelle Auswahl auf das Dokument anwenden">
|
||||
<Icon name="check" size={12} />
|
||||
<span>Auf Doc anwenden</span>
|
||||
</button>
|
||||
<BarToggle label="Schliessen" onClick={onClose} />
|
||||
<BarToggle icon="check" label="Auf Doc anwenden"
|
||||
active
|
||||
onClick={applyToDoc}
|
||||
title="Aktuelle Auswahl auf das Dokument anwenden" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import Icon from './Icon'
|
||||
|
||||
// Gemeinsame Toolbar-Primitiven für Panels im Oberleiste-Stil:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
export default function BottomBar({ onApply, dirty }) {
|
||||
return (
|
||||
<div style={{
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarCombo } from './BarControls'
|
||||
|
||||
export default function ConfirmDeleteEbene({ ebene, otherEbenen, onConfirm, onCancel }) {
|
||||
const [target, setTarget] = useState(otherEbenen[0]?.code ?? '_delete')
|
||||
@@ -42,28 +45,28 @@ export default function ConfirmDeleteEbene({ ebene, otherEbenen, onConfirm, onCa
|
||||
|
||||
<div style={{ padding: '10px 18px 14px', display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<span className="label-xs">Inhalte auf der Ebene</span>
|
||||
<select value={target} onChange={ev => setTarget(ev.target.value)}>
|
||||
<BarCombo stretch
|
||||
value={target}
|
||||
onChange={(v) => setTarget(v)}>
|
||||
{otherEbenen.map(e => (
|
||||
<option key={e.code} value={e.code}>→ Verschieben nach {e.code}_{e.name}</option>
|
||||
))}
|
||||
<option value="_delete">⚠ Inhalte ebenfalls löschen</option>
|
||||
</select>
|
||||
</BarCombo>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
display: 'flex', gap: 4, padding: '10px 14px',
|
||||
display: 'flex', gap: 6, padding: '10px 14px',
|
||||
justifyContent: 'flex-end',
|
||||
borderTop: '1px solid var(--border-light)',
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<button className="btn-text" onClick={onCancel}>Abbrechen</button>
|
||||
<button
|
||||
className="btn-contained"
|
||||
style={isDelete ? { background: 'var(--danger)' } : undefined}
|
||||
<BarToggle label="Abbrechen" onClick={onCancel} />
|
||||
<BarToggle
|
||||
label="Löschen"
|
||||
active
|
||||
onClick={() => onConfirm(isDelete ? null : target)}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useRef, useMemo, useEffect } from 'react'
|
||||
import Icon from './Icon'
|
||||
import ConfirmDeleteEbene from './ConfirmDeleteEbene'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarCombo } from './BarControls'
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||
|
||||
export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, onClose, embedded = false }) {
|
||||
const [draft, setDraft] = useState(zeichnungsebenen.map(z => ({ ...z })))
|
||||
@@ -46,14 +49,16 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
.filter(z => z.isGeschoss)
|
||||
.reduce((s, z) => s + (z.hoehe ?? 0), 0)
|
||||
|
||||
// move-Spalte muss 2 BarButtons (BAR_H breit) + gap aufnehmen — sonst
|
||||
// ueberlappt der rechte Pfeil mit dem G-Haken in der Nachbarspalte.
|
||||
const col = {
|
||||
move: { width: 28, flexShrink: 0 },
|
||||
move: { width: BAR_H * 2 + 6, flexShrink: 0 },
|
||||
geschoss:{ width: 24, flexShrink: 0 },
|
||||
name: { flex: 1, minWidth: 60 },
|
||||
okff: { width: 50, flexShrink: 0 },
|
||||
hoehe: { width: 64, flexShrink: 0 },
|
||||
schnitt: { width: 64, flexShrink: 0 },
|
||||
del: { width: 22, flexShrink: 0 },
|
||||
del: { width: BAR_H, flexShrink: 0 },
|
||||
}
|
||||
|
||||
const wrapperStyle = embedded ? {
|
||||
@@ -81,20 +86,34 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
maxHeight: 'calc(100vh - 80px)',
|
||||
overflow: 'hidden',
|
||||
}
|
||||
|
||||
const numberInputStyle = {
|
||||
width: 44, height: BAR_H, textAlign: 'right',
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontSize: 11, fontFamily: 'var(--font)',
|
||||
padding: '0 8px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={wrapperStyle}>
|
||||
<div style={innerStyle}>
|
||||
{/* Toolbar — Add-Buttons + Bau-Gesamthoehe. Kein Title-Header mehr;
|
||||
das Satelliten-Fenster bringt seinen eigenen mit. */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '10px 14px', borderBottom: '1px solid var(--border)', flexShrink: 0,
|
||||
display: 'flex', alignItems: 'center', gap: 6,
|
||||
padding: '8px 14px', borderBottom: '1px solid var(--border)', flexShrink: 0,
|
||||
}}>
|
||||
<span style={{ flex: 1, fontWeight: 600, fontSize: 12, color: 'var(--text-primary)' }}>
|
||||
Zeichnungsebenen
|
||||
</span>
|
||||
<BarToggle icon="add" label="Geschoss" onClick={() => add(true)} />
|
||||
<BarToggle icon="add" label="Zeichnung" onClick={() => add(false)} />
|
||||
<div style={{ flex: 1 }} />
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>
|
||||
Gebäude {gesamthoehe.toFixed(2)} m
|
||||
</span>
|
||||
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px' }}>×</button>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
@@ -119,13 +138,15 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
borderBottom: '1px solid var(--border-light)',
|
||||
background: i % 2 === 0 ? 'var(--bg-item)' : 'var(--bg-dialog)',
|
||||
}}>
|
||||
<div style={{ ...col.move, display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
<button className="btn-step" onClick={() => move(i, -1)} disabled={i === 0}>
|
||||
<Icon name="arrow_drop_up" size={14} />
|
||||
</button>
|
||||
<button className="btn-step" onClick={() => move(i, 1)} disabled={i === draft.length - 1}>
|
||||
<Icon name="arrow_drop_down" size={14} />
|
||||
</button>
|
||||
<div style={{ ...col.move, display: 'flex', flexDirection: 'row', gap: 2 }}>
|
||||
<BarButton icon="arrow_drop_up"
|
||||
onClick={() => move(i, -1)}
|
||||
disabled={i === 0}
|
||||
title="Nach oben" />
|
||||
<BarButton icon="arrow_drop_down"
|
||||
onClick={() => move(i, 1)}
|
||||
disabled={i === draft.length - 1}
|
||||
title="Nach unten" />
|
||||
</div>
|
||||
|
||||
<div style={col.geschoss}>
|
||||
@@ -141,7 +162,19 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
<input
|
||||
value={z.name}
|
||||
onChange={ev => update(i, 'name', ev.target.value)}
|
||||
style={{ ...col.name, fontWeight: 600, fontSize: 11 }}
|
||||
style={{
|
||||
...col.name,
|
||||
height: BAR_H,
|
||||
fontWeight: 600, fontSize: 11,
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: 999,
|
||||
color: 'var(--text-primary)',
|
||||
fontFamily: 'var(--font)',
|
||||
padding: '0 10px',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ ...col.okff, color: z.isGeschoss ? 'var(--text-muted)' : 'transparent', fontSize: 11, fontFamily: 'var(--font-mono)', textAlign: 'right' }}>
|
||||
@@ -154,7 +187,7 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
<input type="number" step="0.05" min="0.5" max="20"
|
||||
value={z.hoehe ?? 3.0}
|
||||
onChange={ev => update(i, 'hoehe', parseFloat(ev.target.value) || z.hoehe || 3.0)}
|
||||
style={{ width: 44, textAlign: 'right' }}
|
||||
style={numberInputStyle}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>m</span>
|
||||
</>
|
||||
@@ -169,7 +202,7 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
<input type="number" step="0.05" min="0.1"
|
||||
value={z.schnitthoehe ?? 1.0}
|
||||
onChange={ev => update(i, 'schnitthoehe', parseFloat(ev.target.value) || 1.0)}
|
||||
style={{ width: 44, textAlign: 'right' }}
|
||||
style={numberInputStyle}
|
||||
/>
|
||||
<span style={{ color: 'var(--text-muted)', fontSize: 10 }}>m</span>
|
||||
</>
|
||||
@@ -179,9 +212,10 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
</div>
|
||||
|
||||
<div style={col.del}>
|
||||
<button className="btn-icon-sm" onClick={() => remove(i)} title="Löschen">
|
||||
<Icon name="close" size={14} />
|
||||
</button>
|
||||
<BarButton icon="close"
|
||||
onClick={() => remove(i)}
|
||||
disabled={draft.length <= 1}
|
||||
title="Löschen" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -192,13 +226,9 @@ export default function GeschossDialog({ zeichnungsebenen, recalcOkff, onSave, o
|
||||
padding: '10px 14px', borderTop: '1px solid var(--border)',
|
||||
background: 'var(--bg-section)', flexShrink: 0,
|
||||
}}>
|
||||
<button className="btn-outlined" onClick={() => add(true)} style={{
|
||||
color: 'var(--accent-light)', borderColor: 'var(--accent-border)',
|
||||
}}>+ Geschoss</button>
|
||||
<button className="btn-outlined" onClick={() => add(false)}>+ Zeichnung</button>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button className="btn-text" onClick={onClose}>Abbrechen</button>
|
||||
<button className="btn-contained" onClick={() => onSave(draft)}>Übernehmen</button>
|
||||
<BarToggle label="Abbrechen" onClick={onClose} />
|
||||
<BarToggle label="Übernehmen" active onClick={() => onSave(draft)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import ContextMenu from './ContextMenu'
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||
|
||||
/** Vertikales Feld-Layout: Label oben, Input darunter — passt in schmale Panels. */
|
||||
function Field({ label, hint, children }) {
|
||||
@@ -35,6 +38,19 @@ function Toggle({ label, checked, onChange, hint }) {
|
||||
)
|
||||
}
|
||||
|
||||
// Pill-Input: rounded textfield im Stil der Oberleiste
|
||||
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 GeschossSettingsDialog({ geschoss, onSave, onClose, embedded = false }) {
|
||||
const [draft, setDraft] = useState({ ...geschoss })
|
||||
const set = (patch) => setDraft({ ...draft, ...patch })
|
||||
@@ -95,14 +111,14 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
padding: '10px 12px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}>
|
||||
<Icon name="settings" size={14} style={{ color: 'var(--text-secondary)', flexShrink: 0 }} />
|
||||
<Icon name="settings" size={14} style={{ color: 'var(--accent)', flexShrink: 0 }} />
|
||||
<span style={{
|
||||
flex: 1, fontWeight: 600, fontSize: 11,
|
||||
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{geschoss.name}
|
||||
</span>
|
||||
<button onClick={onClose} style={{ color: 'var(--text-muted)', fontSize: 16, padding: '0 4px', lineHeight: 1 }}>×</button>
|
||||
<BarButton icon="close" onClick={onClose} title="Schliessen" />
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
@@ -111,7 +127,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
<input
|
||||
value={draft.name}
|
||||
onChange={(ev) => set({ name: ev.target.value })}
|
||||
style={{ flex: 1, fontSize: 11, fontWeight: 600, minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, fontWeight: 600, minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -145,7 +161,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.5" min="0.5"
|
||||
value={depthBack}
|
||||
onChange={(ev) => set({ depthBack: parseFloat(ev.target.value) || 8.0 })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -155,7 +171,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.1"
|
||||
value={heightMin}
|
||||
onChange={(ev) => set({ heightMin: parseFloat(ev.target.value) })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="HÖHE OBEN (m)">
|
||||
@@ -163,31 +179,31 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.1"
|
||||
value={heightMax}
|
||||
onChange={(ev) => set({ heightMax: parseFloat(ev.target.value) })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field label="BLICKRICHTUNG"
|
||||
hint="Wechselt zwischen den beiden Seiten der Schnittlinie">
|
||||
<button className={dirSign >= 0 ? 'btn-contained' : 'btn-outlined'}
|
||||
onClick={() => set({ dirSign: 1 })}
|
||||
style={{ flex: 1, fontSize: 11 }}>← Seite A</button>
|
||||
<button className={dirSign < 0 ? 'btn-contained' : 'btn-outlined'}
|
||||
onClick={() => set({ dirSign: -1 })}
|
||||
style={{ flex: 1, fontSize: 11 }}>Seite B →</button>
|
||||
<BarToggle label="← Seite A"
|
||||
active={dirSign >= 0}
|
||||
onClick={() => set({ dirSign: 1 })} />
|
||||
<BarToggle label="Seite B →"
|
||||
active={dirSign < 0}
|
||||
onClick={() => set({ dirSign: -1 })} />
|
||||
</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>
|
||||
<BarToggle label="Parallel"
|
||||
active={projection === 'parallel'}
|
||||
onClick={() => set({ projection: 'parallel' })} />
|
||||
<BarToggle label="Perspektive"
|
||||
active={projection === 'perspective'}
|
||||
onClick={() => set({ projection: 'perspective' })} />
|
||||
</Field>
|
||||
|
||||
{projection === 'perspective' && (
|
||||
@@ -197,7 +213,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.1"
|
||||
value={cameraHeight}
|
||||
onChange={(ev) => set({ cameraHeight: parseFloat(ev.target.value) })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
@@ -213,7 +229,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.05" min="0.5" max="30"
|
||||
value={hoehe}
|
||||
onChange={(ev) => set({ hoehe: parseFloat(ev.target.value) || hoehe })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -222,7 +238,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.05" min="0.1"
|
||||
value={schnitt}
|
||||
onChange={(ev) => set({ schnitthoehe: parseFloat(ev.target.value) || 1.0 })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0 }}
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0 }}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -250,7 +266,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
type="number" step="0.01"
|
||||
value={draft.projectZeroMum ?? 0}
|
||||
onChange={(ev) => set({ projectZeroMum: parseFloat(ev.target.value) || 0 })}
|
||||
style={{ flex: 1, fontSize: 11, textAlign: 'right', minWidth: 0,
|
||||
style={{ ...pillInput, flex: 1, textAlign: 'right', minWidth: 0,
|
||||
fontFamily: 'var(--font-mono)' }}
|
||||
/>
|
||||
</Field>
|
||||
@@ -264,8 +280,8 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
background: 'var(--bg-section)',
|
||||
}}>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button className="btn-text" onClick={onClose}>Abbrechen</button>
|
||||
<button className="btn-contained" onClick={() => {
|
||||
<BarToggle label="Abbrechen" onClick={onClose} />
|
||||
<BarToggle label="Übernehmen" active onClick={() => {
|
||||
// Numerische Felder NIEMALS als undefined/null rausgehen lassen —
|
||||
// sonst crasht der Plugin spaeter beim float()-Cast. Defaults
|
||||
// entsprechen den Werten die das UI auch ohne User-Input zeigt.
|
||||
@@ -289,7 +305,7 @@ export default function GeschossSettingsDialog({ geschoss, onSave, onClose, embe
|
||||
}
|
||||
}
|
||||
onSave(out)
|
||||
}}>Übernehmen</button>
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
export default function Icon({ name, size = 18, fill = 0, weight = 400, style }) {
|
||||
return (
|
||||
<span
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useMemo } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useEffect } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState } from 'react'
|
||||
import Icon from './Icon'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useState, useMemo } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||
|
||||
+11
-2
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
/**
|
||||
* rhinoBridge.js — Kommunikation React ↔ Rhino/Python
|
||||
* Lange Payloads werden in Chunks <700 Zeichen aufgeteilt (document.title-Limit).
|
||||
@@ -241,8 +243,15 @@ export function saveOeffStyle(name, settings) {
|
||||
send('SAVE_OEFF_STYLE', { name, settings })
|
||||
}
|
||||
export function deleteOeffStyle(id) { send('DELETE_OEFF_STYLE', { id }) }
|
||||
export function setSectionStyle(enabled, source, color, pattern, scale, rotation) {
|
||||
send('SET_SECTION_STYLE', { enabled, source, color, pattern, scale, rotation })
|
||||
export function setSectionStyle(enabled, source, color, pattern, scale, rotation,
|
||||
opts = {}) {
|
||||
send('SET_SECTION_STYLE', {
|
||||
enabled, source, color, pattern, scale, rotation,
|
||||
boundaryVisible: opts.boundaryVisible,
|
||||
boundaryWidthScale: opts.boundaryWidthScale,
|
||||
boundaryColor: opts.boundaryColor,
|
||||
backgroundColor: opts.backgroundColor, // null = transparent (Viewport), Hex = SolidColor
|
||||
})
|
||||
}
|
||||
export function openAbout() { send('OPEN_ABOUT', {}) }
|
||||
export function createText() { send('CREATE_TEXT', {}) }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
|
||||
Reference in New Issue
Block a user