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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user