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:
2026-05-26 17:09:18 +02:00
parent e1b63aa4e6
commit 13a5e1eb7a
100 changed files with 3147 additions and 839 deletions
+171 -34
View File
@@ -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>
)
+2
View File
@@ -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 {
+40 -35
View File
@@ -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
View File
@@ -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,
}}>
+2
View File
@@ -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'
+3 -6
View File
@@ -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) || {}
+2
View File
@@ -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'
+2
View File
@@ -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'
+2
View File
@@ -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'
+3 -7
View File
@@ -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.
+3 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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',
}}
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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 }}>
+3 -7
View File
@@ -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
View File
@@ -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>
)
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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>
)
+2
View File
@@ -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 {
+54 -60
View File
@@ -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>
+2
View File
@@ -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:
+2
View File
@@ -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={{
+13 -10
View File
@@ -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>
+2
View File
@@ -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'
+2
View File
@@ -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'
+2
View File
@@ -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'
+57 -27
View File
@@ -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>
+2
View File
@@ -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'
+41 -25
View File
@@ -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>
+2
View File
@@ -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
+2
View File
@@ -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'
+2
View File
@@ -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'
+2
View File
@@ -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'
+2
View File
@@ -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
View File
@@ -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', {}) }
+2
View File
@@ -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'