DOSSIER Multi-Phase: C#-Plugin + Yak + Wandstile + UX-Polish

- C#-Plugin "DOSSIER" mit 23 nativen Commands (dWall, dDoor, ..., dSection)
  - Native Command-Namen + Autocomplete + saubere History
  - Idle-Defer + RhinoCode-API → kein _-RunPythonScript-Echo
  - Yak-Paket via build.sh, Install in ~/Library/.../packages/8.0/
- Launcher (Tauri):
  - dossier_init Tauri-Command + Setup-Tab in Settings
  - Yak-Install + StartupCommands-XML + Window-Layout in einem Schritt
  - clean-rhino.sh fuer reproduzierbare Resets
  - check_dossier_initialized triggert Auto-Open-Setup beim ersten Start
- Wand-Architektur:
  - Chain-Logik DEAKTIVIERT → jede Wand baut eigenes Volume (individuell
    anwaehlbar, einzeln loeschbar)
  - Polyline-Wand: jedes Segment = eigene Wand
  - Smart-Split fuer wand_axis/decke/dach/raum/aussparung/traeger
  - Auto-Group axis+volume → kein ChooseOne-Dialog, Delete loescht beides
  - Stale-Mitre-Fix: Joint-Cache wird vor jedem Wand-Regen invalidiert
  - T-Junction-Tolerance auf 1mm (war 1cm, lieferte falsche T-Mitres)
- Wand-Stile:
  - Schema in dossier_project_settings.wand_styles (Material + Prio +
    Default-Dicke + Referenz, oder Layered mit Schichten)
  - dWall-Command Stil-Picker
  - ProjectSettingsDialog: Sidebar-Layout (Pill-Selection) +
    Wandstile-Tab mit Liste/Editor
  - _wand_chain_compat benutzt style_id
  - Prio-Dominanz: hoehere Prio gewinnt Eckverbindung, niedrigere wird
    T-mitered (siehe _resolve_corner_miter)
- Cmd+G fuer Group (Geschoss-Up auf Alias 'gu')
- Welcome + Cheatsheet borderless mit X/Back-Buttons
- BeginCommand-Hook fuer Gestaltung-Panel-Auto-Open
- panel_base: Python.NET-Enum-Fix fuer Material-Render
This commit is contained in:
2026-05-30 12:46:53 +02:00
parent 7930705d01
commit 18d6d98e07
54 changed files with 5575 additions and 398 deletions
+362 -14
View File
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano
import { useState, useEffect } from 'react'
import React, { useState, useEffect } from 'react'
import Icon from './Icon'
import { BarToggle, BarButton, BAR_H } from './BarControls'
import {
@@ -117,6 +117,57 @@ function TabBar({ tabs, active, onChange }) {
)
}
// Vertikale Sidebar mit Gruppen — skaliert auf beliebig viele Kategorien.
// Aktiver Eintrag = abgerundete Pill (mit Margin links/rechts), nicht
// full-width Fläche — passt zum DOSSIER-Pill-Stil.
function Sidebar({ groups, active, onChange }) {
return (
<div style={{
width: 180, flexShrink: 0,
borderRight: '1px solid var(--border)',
background: 'var(--bg-dialog)',
overflowY: 'auto',
padding: '8px 0',
}}>
{groups.map((grp) => (
<div key={grp.label}>
<div style={{
padding: '12px 14px 6px',
fontSize: 8, fontWeight: 600,
color: 'var(--text-muted)',
letterSpacing: '0.12em',
textTransform: 'uppercase',
}}>{grp.label}</div>
{grp.items.map((it) => {
const isActive = active === it.key
return (
<div key={it.key}
style={{ padding: '1px 8px' }}>
<button
onClick={() => onChange(it.key)}
style={{
display: 'block', width: '100%',
textAlign: 'left',
padding: '6px 12px',
background: isActive ? 'var(--accent)' : 'transparent',
color: isActive ? '#fff' : 'var(--text-primary)',
border: 'none',
borderRadius: 999,
cursor: 'pointer',
fontSize: 11,
lineHeight: 1.4,
}}>
{it.label}
</button>
</div>
)
})}
</div>
))}
</div>
)
}
/* LinetypePreview — SVG-Linie mit Strich-Segmenten. segments = [{length,type}]
type ∈ Line/Space (manchmal auch Continuous-Ableitungen). Width in px;
wir skalieren die Segmente damit das Gesamtmuster in width passt. */
@@ -623,10 +674,13 @@ export default function ProjectSettingsDialog({
}) {
const [tab, setTab] = useState('defaults')
const [draft, setDraft] = useState(() => ({
defaults: { ...(initial.defaults || {}) },
materials: [...(initial.materials || [])],
project: { ...(initial.project || {}) },
defaults: { ...(initial.defaults || {}) },
materials: [...(initial.materials || [])],
wandStyles: [...(initial.wandStyles || [])],
project: { ...(initial.project || {}) },
}))
const [selWandStyleIdx, setSelWandStyleIdx] = useState(() =>
(initial.wandStyles && initial.wandStyles.length) ? 0 : null)
const setProject = (k, v) =>
setDraft(d => ({ ...d, project: { ...(d.project || {}), [k]: v } }))
const [selMat, setSelMat] = useState(() => {
@@ -750,6 +804,42 @@ export default function ProjectSettingsDialog({
setSelMat({ kind: 'local', idx: draft.materials.length })
}
// Wand-Stile CRUD — gleiche Pattern wie Materialien
const setWandStyle = (i, patch) => setDraft(d => ({
...d, wandStyles: d.wandStyles.map((s, idx) =>
idx === i ? { ...s, ...patch } : s),
}))
const delWandStyle = (i) => {
setDraft(d => ({
...d, wandStyles: d.wandStyles.filter((_, idx) => idx !== i),
}))
setSelWandStyleIdx(null)
}
const addWandStyle = () => {
const newStyle = {
id: 'style_' + Math.random().toString(36).slice(2, 10),
name: 'Neuer Stil', prio: 500,
dicke: 0.25, referenz: 'mid',
layered: false, material: '', layers: [],
}
setDraft(d => ({
...d, wandStyles: [...d.wandStyles, newStyle],
}))
setSelWandStyleIdx(draft.wandStyles.length)
}
const dupWandStyle = (i) => {
setDraft(d => {
const src = d.wandStyles[i]
if (!src) return d
const copy = { ...src,
id: 'style_' + Math.random().toString(36).slice(2, 10),
name: (src.name || 'Stil') + ' (Kopie)',
}
return { ...d, wandStyles: [...d.wandStyles, copy] }
})
setSelWandStyleIdx(draft.wandStyles.length)
}
const wrapperStyle = embedded ? {
width: '100%', height: '100%',
background: 'var(--bg-dialog)',
@@ -766,17 +856,30 @@ export default function ProjectSettingsDialog({
<div style={wrapperStyle}>
<div style={{ display: 'flex', flexDirection: 'column', flex: 1,
minHeight: 0, overflow: 'hidden' }}>
<TabBar tabs={[
{ key: 'defaults', label: 'Voreinstellungen' },
{ key: 'materials', label: 'Materialien' },
{ key: 'linetypes', label: 'Linientypen' },
{ key: 'hatches', label: 'Schraffuren' },
{ key: 'symbols', label: 'Symbole' },
{ key: 'raumstile', label: 'Raumstile' },
{ key: 'stempelstile', label: 'Stempelstile' },
]} active={tab} onChange={setTab} />
{/* Body: Sidebar links + Inhalt rechts */}
<div style={{ display: 'flex', flexDirection: 'row', flex: 1,
minHeight: 0, overflow: 'hidden' }}>
<Sidebar
active={tab}
onChange={setTab}
groups={[
{ label: 'Projekt', items: [
{ key: 'defaults', label: 'Projekt & Defaults' },
]},
{ label: 'Stile', items: [
{ key: 'wandstile', label: 'Wandstile' },
{ key: 'raumstile', label: 'Raumstile' },
{ key: 'stempelstile', label: 'Stempelstile' },
{ key: 'symbols', label: 'Symbole' },
]},
{ label: 'Bibliothek', items: [
{ key: 'materials', label: 'Materialien' },
{ key: 'linetypes', label: 'Linientypen' },
{ key: 'hatches', label: 'Schraffuren' },
]},
]} />
{/* Body */}
{/* Tab content */}
<div style={{ flex: 1, minHeight: 0, overflowY: 'auto',
padding: '8px 14px' }}>
{tab === 'defaults' && (
@@ -873,6 +976,18 @@ export default function ProjectSettingsDialog({
</div>
)}
{tab === 'wandstile' && (
<WandStileTab
styles={draft.wandStyles}
selectedIdx={selWandStyleIdx}
onSelect={setSelWandStyleIdx}
materials={[...(initial.builtinMaterials || []), ...(draft.materials || [])]}
onChange={setWandStyle}
onAdd={addWandStyle}
onDelete={delWandStyle}
onDuplicate={dupWandStyle} />
)}
{tab === 'materials' && (
<div style={{ display: 'flex', height: '100%',
margin: '-8px -14px', /* Tab-Padding aufheben */
@@ -1582,6 +1697,7 @@ export default function ProjectSettingsDialog({
</div>
)}
</div>
</div>{/* ← schließt Body-Row (Sidebar + Tab-Content) */}
{/* Footer — Pill-Buttons */}
<div style={{
@@ -1598,3 +1714,235 @@ export default function ProjectSettingsDialog({
</div>
)
}
// ============================================================================
// WandStileTab — Liste links + Editor rechts (Pattern wie MaterialDetail).
// styles: Array von Wand-Style-Dicts, sortiert nach prio absteigend angezeigt.
// onChange(idx, patch): partial update des Style-Eintrags an Index idx.
// ============================================================================
function WandStileTab({
styles, selectedIdx, onSelect,
materials,
onChange, onAdd, onDelete, onDuplicate,
}) {
const matNames = (materials || []).map(m => m.name).filter(Boolean)
// Sortier-Index fuer Anzeige (nach prio absteigend). Editieren bleibt auf
// dem Original-Index — wir mappen visuell-Index → realer-Index.
const sorted = [...(styles || [])].map((s, i) => ({ s, i }))
.sort((a, b) => (b.s.prio || 0) - (a.s.prio || 0))
const sel = (selectedIdx != null && styles && styles[selectedIdx])
? styles[selectedIdx] : null
return (
<div style={{ display: 'flex', height: '100%',
margin: '-8px -14px', minHeight: 0 }}>
{/* Links: Liste */}
<div style={{
width: 240, flexShrink: 0,
display: 'flex', flexDirection: 'column',
borderRight: '1px solid var(--border)',
background: 'var(--bg-dialog)',
}}>
<div style={{ flex: 1, overflowY: 'auto', padding: '4px 0' }}>
{sorted.length === 0 && (
<div style={{ padding: 20, textAlign: 'center',
color: 'var(--text-muted)', fontSize: 10 }}>
Noch keine Wandstile.<br/>+ klicken zum Anlegen.
</div>
)}
{sorted.map(({ s, i }) => {
const isActive = selectedIdx === i
return (
<button key={s.id || i}
onClick={() => onSelect(i)}
style={{
display: 'flex', alignItems: 'center', gap: 8,
width: '100%', textAlign: 'left',
padding: '8px 12px',
background: isActive ? 'var(--accent)' : 'transparent',
color: isActive ? '#fff' : 'var(--text-primary)',
border: 'none', cursor: 'pointer',
fontSize: 11,
}}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 500 }}>
{s.name || s.id || '(unnamed)'}
</div>
<div style={{ fontSize: 9, opacity: 0.7, marginTop: 2 }}>
{s.layered ? 'geschichtet' : (s.material || 'kein Material')}
{' · '}{s.layered
? ((s.layers || []).reduce((sum, l) => sum + (l.dicke || 0), 0)).toFixed(2)
: (s.dicke || 0).toFixed(2)} m
</div>
</div>
<div style={{
fontSize: 9, padding: '2px 6px',
background: isActive ? 'rgba(255,255,255,0.2)' : 'var(--bg-section)',
borderRadius: 3, minWidth: 32, textAlign: 'center',
}}>{s.prio || 500}</div>
</button>
)
})}
</div>
<div style={{ display: 'flex', gap: 4,
padding: '6px 8px',
borderTop: '1px solid var(--border-light)' }}>
<BarToggle icon="add" onClick={onAdd} title="Neuer Wandstil" />
{selectedIdx != null && (
<>
<BarToggle icon="content_copy"
onClick={() => onDuplicate(selectedIdx)}
title="Duplizieren" />
<BarToggle icon="delete"
onClick={() => onDelete(selectedIdx)}
title="Löschen" />
</>
)}
</div>
</div>
{/* Rechts: Editor */}
<div style={{ flex: 1, minWidth: 0, overflowY: 'auto', padding: '14px' }}>
{!sel && (
<div style={{ color: 'var(--text-muted)', fontSize: 11,
padding: 20, textAlign: 'center' }}>
Wandstil links auswählen oder + für neuen.
</div>
)}
{sel && (
<div style={{ maxWidth: 540 }}>
<DetailSection title="Identität">
<InlineTextField label="Name"
value={sel.name}
onChange={(v) => onChange(selectedIdx, { name: v })} />
<InlineTextField label="ID (intern)"
value={sel.id}
onChange={(v) => onChange(selectedIdx, { id: v })}
hint="Wird in der Wand als UserString gespeichert. Aenderung verlinkt bestehende Waende nicht automatisch um." />
<InlineNumberField label="Prio (1-999)"
value={sel.prio || 500}
step={50} min={1} max={999}
onChange={(v) => onChange(selectedIdx, { prio: Math.max(1, Math.min(999, v || 500)) })}
hint="Bei Joints zwischen verschiedenen Stilen gewinnt der mit höherer Prio die Eckverbindung." />
</DetailSection>
<DetailSection title="Geometrie">
<div style={{ padding: '5px 0',
display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ flex: 1, fontSize: 11,
color: 'var(--text-primary)' }}>Aufbau</span>
<div style={{ display: 'flex', gap: 3 }}>
<BarToggle label="Massiv" active={!sel.layered}
onClick={() => onChange(selectedIdx, { layered: false })} />
<BarToggle label="Geschichtet" active={sel.layered}
onClick={() => onChange(selectedIdx, { layered: true })} />
</div>
</div>
{!sel.layered && (
<>
<div style={{ padding: '5px 0',
display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ flex: 1, fontSize: 11,
color: 'var(--text-primary)' }}>Material</span>
<select value={sel.material || ''}
onChange={(e) => onChange(selectedIdx, { material: e.target.value })}
style={{ fontSize: 11, padding: '3px 8px',
background: 'var(--bg-section)',
color: 'var(--text-primary)',
border: '1px solid var(--border)',
borderRadius: 3 }}>
<option value="">(keines)</option>
{matNames.map(n => (
<option key={n} value={n}>{n}</option>
))}
</select>
</div>
<InlineNumberField label="Default-Dicke"
value={sel.dicke || 0.25}
step={0.01} min={0.01} suffix="m"
onChange={(v) => onChange(selectedIdx, { dicke: v || 0.25 })}
hint="Wird beim Erstellen vorgeschlagen. User kann pro Wand überschreiben." />
<div style={{ padding: '5px 0',
display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ flex: 1, fontSize: 11,
color: 'var(--text-primary)' }}>Referenz</span>
<div style={{ display: 'flex', gap: 3 }}>
{[
{ code: 'mid', label: 'Mittig' },
{ code: 'left', label: 'Links' },
{ code: 'right', label: 'Rechts' },
].map(r => (
<BarToggle key={r.code} label={r.label}
active={(sel.referenz || 'mid') === r.code}
onClick={() => onChange(selectedIdx, { referenz: r.code })} />
))}
</div>
</div>
</>
)}
{sel.layered && (
<div style={{ padding: '5px 0' }}>
<div style={{ fontSize: 10, color: 'var(--text-muted)',
paddingBottom: 4 }}>
Schichten (von links nach rechts):
</div>
{(sel.layers || []).map((ly, li) => (
<div key={li} style={{
display: 'flex', gap: 6, alignItems: 'center',
padding: '4px 0',
}}>
<select value={ly.material || ''}
onChange={(e) => {
const newLayers = [...(sel.layers || [])]
newLayers[li] = { ...newLayers[li], material: e.target.value }
onChange(selectedIdx, { layers: newLayers })
}}
style={{ flex: 1, fontSize: 11, padding: '3px 6px',
background: 'var(--bg-section)',
color: 'var(--text-primary)',
border: '1px solid var(--border)',
borderRadius: 3 }}>
<option value="">(material)</option>
{matNames.map(n => (
<option key={n} value={n}>{n}</option>
))}
</select>
<input type="number" step="0.01" min="0.005"
value={ly.dicke || 0}
onChange={(e) => {
const newLayers = [...(sel.layers || [])]
newLayers[li] = { ...newLayers[li], dicke: parseFloat(e.target.value) || 0 }
onChange(selectedIdx, { layers: newLayers })
}}
style={{ width: 60, fontSize: 11, padding: '3px 6px',
background: 'var(--bg-section)',
color: 'var(--text-primary)',
border: '1px solid var(--border)',
borderRadius: 3 }} />
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>m</span>
<BarToggle icon="delete" onClick={() => {
const newLayers = (sel.layers || []).filter((_, idx) => idx !== li)
onChange(selectedIdx, { layers: newLayers })
}} title="Schicht löschen" />
</div>
))}
<BarToggle icon="add" label="Schicht hinzufügen"
onClick={() => {
const newLayers = [...(sel.layers || []),
{ material: '', dicke: 0.05 }]
onChange(selectedIdx, { layers: newLayers })
}} />
<div style={{ fontSize: 10, color: 'var(--text-muted)',
padding: '6px 0 0' }}>
Total: {((sel.layers || []).reduce((s, l) => s + (l.dicke || 0), 0)).toFixed(3)} m
</div>
</div>
)}
</DetailSection>
</div>
)}
</div>
</div>
)
}