Linientypen + Schraffuren-Tabs in Project-Settings + Datei-Import
Project-Settings hat jetzt 4 Tabs:
- Voreinstellungen (kompakte InlineNumberField, gruppiert in Sections)
- Materialien (List/Detail, ohne Hatch)
- Linientypen (List/Detail mit SVG-Strich-Vorschau)
- Schraffuren (List/Detail mit echtem HatchLine-Renderer)
Backend (rhinopanel.py):
- _list_linetypes_full liefert Segmente {length, type: Line/Space/Dot}
(Mac Rhino 8 GetSegment returnt (length, isLine: bool))
- _list_hatch_patterns_full liefert HatchLines mit angle/base/offset/dashes
(hl.Dashes optional ueber 3 API-Variants)
- CRUD: RENAME / DELETE / LOAD_DEFAULTS
- File-Import: IMPORT_LINETYPE_FILE (.lin), IMPORT_HATCH_FILE (.pat)
via Eto.OpenFileDialog → Linetypes.Load / HatchPatterns.LoadFromFile
Frontend (ProjectSettingsDialog.jsx):
- LinetypePreview: SVG mit tile-fenster (4 Repetitions), Line als <line>,
Dot als <circle>, currentColor fuer Renderer-Robustheit
- HatchPreview: rendert pro HatchLine alle parallelen Linien mit Angle,
Offset (Spacing + Stagger), Dashes als stroke-dasharray
- TABLES_UPDATED Message vom Backend re-rendert Listen
- Import-Pills im List-Footer
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,154 @@ def _hatch_pattern_names(doc):
|
||||
return out
|
||||
|
||||
|
||||
def _list_hatch_patterns_full(doc):
|
||||
"""Vollstaendiges Hatch-Pattern-Listing fuer die Verwaltungs-UI.
|
||||
Inkludiert die HatchLines (angle, base, offset, dashes) damit
|
||||
Frontend echte Pattern-Previews rendern kann statt Platzhalter."""
|
||||
out = []
|
||||
try:
|
||||
for i in range(doc.HatchPatterns.Count):
|
||||
try:
|
||||
hp = doc.HatchPatterns[i]
|
||||
if hp is None: continue
|
||||
try:
|
||||
if bool(hp.IsDeleted): continue
|
||||
except Exception: pass
|
||||
ftype = "Lines"
|
||||
try:
|
||||
ft = hp.FillType
|
||||
ftype = str(ft).split(".")[-1]
|
||||
except Exception: pass
|
||||
ref = 0
|
||||
try: ref = int(hp.Reference)
|
||||
except Exception: pass
|
||||
desc = ""
|
||||
try: desc = str(hp.Description) or ""
|
||||
except Exception: pass
|
||||
# HatchLines extrahieren (nur Lines-Typ)
|
||||
hlines = []
|
||||
if ftype.lower() == "lines":
|
||||
try:
|
||||
# Probiere zuerst die Property, dann GetHatchLines()
|
||||
lines = None
|
||||
try: lines = hp.HatchLines
|
||||
except Exception: pass
|
||||
if lines is None:
|
||||
try: lines = hp.GetHatchLines()
|
||||
except Exception: pass
|
||||
if lines is None:
|
||||
print("[EBENEN] hp[{}] '{}' HatchLines=None".format(
|
||||
i, hp.Name))
|
||||
else:
|
||||
try: cnt = len(lines)
|
||||
except Exception:
|
||||
try: cnt = lines.Count
|
||||
except Exception: cnt = -1
|
||||
print("[EBENEN] hp[{}] '{}' HatchLines type={} count={}".format(
|
||||
i, hp.Name, type(lines).__name__, cnt))
|
||||
for hl in lines:
|
||||
try:
|
||||
bp = hl.BasePoint
|
||||
off = hl.Offset
|
||||
# Dashes optional — Property heisst auf
|
||||
# manchen Rhino-Versionen GetDashes() oder
|
||||
# DashCount/GetDash(i)
|
||||
dashes = []
|
||||
try:
|
||||
dr = hl.Dashes
|
||||
if dr is not None:
|
||||
for d in dr: dashes.append(float(d))
|
||||
except Exception:
|
||||
# Versuche GetDashes()
|
||||
try:
|
||||
dr = hl.GetDashes()
|
||||
if dr is not None:
|
||||
for d in dr: dashes.append(float(d))
|
||||
except Exception:
|
||||
# Versuche DashCount + GetDash(i)
|
||||
try:
|
||||
dc = int(hl.DashCount)
|
||||
for k in range(dc):
|
||||
dashes.append(float(hl.GetDash(k)))
|
||||
except Exception: pass
|
||||
entry = {
|
||||
"angle": float(hl.Angle),
|
||||
"baseX": float(bp.X),
|
||||
"baseY": float(bp.Y),
|
||||
"offX": float(off.X),
|
||||
"offY": float(off.Y),
|
||||
"dashes": dashes,
|
||||
}
|
||||
hlines.append(entry)
|
||||
except Exception as ex:
|
||||
print("[EBENEN] hp[{}] hl FAIL:".format(i), ex)
|
||||
except Exception as ex:
|
||||
print("[EBENEN] hp[{}] HatchLines outer FAIL:".format(i), ex)
|
||||
out.append({
|
||||
"index": i,
|
||||
"name": hp.Name or "",
|
||||
"fillType": ftype,
|
||||
"description": desc,
|
||||
"isReference": (ref > 0),
|
||||
"hatchLines": hlines,
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
except Exception as ex:
|
||||
print("[EBENEN] _list_hatch_patterns_full:", ex)
|
||||
return out
|
||||
|
||||
|
||||
def _list_linetypes_full(doc):
|
||||
"""Vollstaendiges Linetype-Listing fuer die Verwaltungs-UI.
|
||||
Mac Rhino 8 GetSegment(i) returnt (length: float, isLine: bool):
|
||||
True = Line-Segment, False = Space (Gap). Bei Dot ist length=0.0
|
||||
+ isLine=True (Punkt = unendlich kurzer Strich)."""
|
||||
out = []
|
||||
try:
|
||||
for i in range(doc.Linetypes.Count):
|
||||
try:
|
||||
lt = doc.Linetypes[i]
|
||||
if lt is None or lt.IsDeleted: continue
|
||||
segs = []
|
||||
sc_cnt = 0
|
||||
try: sc_cnt = int(lt.SegmentCount)
|
||||
except Exception: pass
|
||||
for s in range(sc_cnt):
|
||||
try:
|
||||
seg = lt.GetSegment(s)
|
||||
if seg is None: continue
|
||||
length = float(seg[0])
|
||||
# seg[1]: True = Line/Dot, False = Space/Gap
|
||||
is_line = bool(seg[1])
|
||||
if is_line and length == 0.0:
|
||||
stype = "Dot"
|
||||
elif is_line:
|
||||
stype = "Line"
|
||||
else:
|
||||
stype = "Space"
|
||||
segs.append({"length": length, "type": stype})
|
||||
except Exception as ex:
|
||||
print("[EBENEN] lt[{}] GetSegment({}) FAIL: {}".format(
|
||||
i, s, ex))
|
||||
ref = 0
|
||||
try: ref = int(lt.Reference)
|
||||
except Exception: pass
|
||||
out.append({
|
||||
"index": i,
|
||||
"name": lt.Name or "",
|
||||
"segments": segs,
|
||||
"isContinuous": (len(segs) == 0),
|
||||
"isReference": (ref > 0),
|
||||
})
|
||||
except Exception as ex:
|
||||
print("[EBENEN] linetype outer FAIL:", ex)
|
||||
continue
|
||||
except Exception as ex:
|
||||
print("[EBENEN] _list_linetypes_full:", ex)
|
||||
return out
|
||||
|
||||
|
||||
def _read_launcher_schema():
|
||||
"""Liest das Default-Layer-Schema aus dossier_settings.json (Launcher-Pfad).
|
||||
Liefert eine Liste {code, name, color, lw} oder None wenn nicht gesetzt."""
|
||||
@@ -692,6 +840,8 @@ class EbenenBridge(panel_base.BaseBridge):
|
||||
"materials": current.get("materials", []),
|
||||
"builtinMaterials": built_in,
|
||||
"hatchPatterns": _hatch_pattern_names(doc),
|
||||
"hatchPatternsFull": _list_hatch_patterns_full(doc),
|
||||
"linetypes": _list_linetypes_full(doc),
|
||||
}
|
||||
def on_save(updated):
|
||||
doc2 = Rhino.RhinoDoc.ActiveDoc
|
||||
@@ -774,6 +924,20 @@ class EbenenBridge(panel_base.BaseBridge):
|
||||
except Exception: pass
|
||||
elif t == "PICK_TEXTURE_FILE":
|
||||
self._pick_texture(p)
|
||||
elif t == "RENAME_LINETYPE":
|
||||
self._rename_linetype(p)
|
||||
elif t == "DELETE_LINETYPE":
|
||||
self._delete_linetype(p)
|
||||
elif t == "LOAD_LINETYPE_DEFAULTS":
|
||||
self._load_linetype_defaults()
|
||||
elif t == "IMPORT_LINETYPE_FILE":
|
||||
self._import_linetype_file()
|
||||
elif t == "RENAME_HATCH":
|
||||
self._rename_hatch(p)
|
||||
elif t == "DELETE_HATCH":
|
||||
self._delete_hatch(p)
|
||||
elif t == "IMPORT_HATCH_FILE":
|
||||
self._import_hatch_file()
|
||||
def _pick_texture(self, payload):
|
||||
slot = payload.get("slot") or "diffuse"
|
||||
try:
|
||||
@@ -794,6 +958,151 @@ class EbenenBridge(panel_base.BaseBridge):
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] pick_texture:", ex)
|
||||
self.send("TEXTURE_PICKED", {"slot": slot, "path": None})
|
||||
|
||||
# ---- Linetype CRUD ----
|
||||
def _rename_linetype(self, payload):
|
||||
idx = payload.get("index")
|
||||
new_name = (payload.get("name") or "").strip()
|
||||
if idx is None or not new_name: return
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
if d is None: return
|
||||
try:
|
||||
lt = d.Linetypes[int(idx)]
|
||||
if lt is None or lt.IsDeleted: return
|
||||
lt.Name = new_name
|
||||
d.Linetypes.Modify(lt, int(idx), True)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] rename_linetype:", ex)
|
||||
self._send_tables()
|
||||
|
||||
def _delete_linetype(self, payload):
|
||||
idx = payload.get("index")
|
||||
if idx is None: return
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
if d is None: return
|
||||
try:
|
||||
lt = d.Linetypes[int(idx)]
|
||||
if lt is None: return
|
||||
# Default-Linetypes (Continuous, ByLayer) sollten nicht
|
||||
# geloescht werden — Rhino's Delete macht es uns aber
|
||||
# einfach: gibt False zurueck wenn nicht erlaubt.
|
||||
d.Linetypes.Delete(int(idx), True)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] delete_linetype:", ex)
|
||||
self._send_tables()
|
||||
|
||||
def _load_linetype_defaults(self):
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
if d is None: return
|
||||
try:
|
||||
d.Linetypes.LoadDefaultLinetypes(True)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] load_linetype_defaults:", ex)
|
||||
self._send_tables()
|
||||
|
||||
def _import_linetype_file(self):
|
||||
"""Datei-Picker fuer .lin (AutoCAD-Linetype) → Linetypes.Load."""
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
if d is None: return
|
||||
try:
|
||||
import Eto.Forms as forms
|
||||
dlg = forms.OpenFileDialog()
|
||||
dlg.Title = "Linientyp-Datei waehlen (.lin)"
|
||||
dlg.MultiSelect = False
|
||||
dlg.Filters.Add(forms.FileFilter("AutoCAD Linetypes", ".lin"))
|
||||
dlg.Filters.Add(forms.FileFilter("Alle", ".*"))
|
||||
parent = bridge_holder.get("form")
|
||||
res = dlg.ShowDialog(parent) if parent else dlg.ShowDialog(None)
|
||||
if str(res) != "Ok": return
|
||||
path = dlg.FileName or ""
|
||||
if not path: return
|
||||
cnt_before = d.Linetypes.Count
|
||||
# Rhino-API: Linetypes.Load(filename) — Achtung in
|
||||
# manchen Builds heisst es LoadLinetypeFile(...).
|
||||
ok = False
|
||||
try:
|
||||
ok = bool(d.Linetypes.Load(path))
|
||||
except Exception:
|
||||
try: ok = bool(d.Linetypes.LoadLinetypeFile(path))
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] Linetypes.Load:", ex)
|
||||
cnt_after = d.Linetypes.Count
|
||||
print("[PROJECT-SETTINGS] linetype import: ok={} {} -> {} ({} neu)".format(
|
||||
ok, cnt_before, cnt_after, cnt_after - cnt_before))
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] import_linetype_file:", ex)
|
||||
self._send_tables()
|
||||
|
||||
def _import_hatch_file(self):
|
||||
"""Datei-Picker fuer .pat (AutoCAD-Hatch) →
|
||||
HatchPatterns.LoadFromFile."""
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
if d is None: return
|
||||
try:
|
||||
import Eto.Forms as forms
|
||||
dlg = forms.OpenFileDialog()
|
||||
dlg.Title = "Schraffur-Datei waehlen (.pat)"
|
||||
dlg.MultiSelect = False
|
||||
dlg.Filters.Add(forms.FileFilter("AutoCAD Hatches", ".pat"))
|
||||
dlg.Filters.Add(forms.FileFilter("Alle", ".*"))
|
||||
parent = bridge_holder.get("form")
|
||||
res = dlg.ShowDialog(parent) if parent else dlg.ShowDialog(None)
|
||||
if str(res) != "Ok": return
|
||||
path = dlg.FileName or ""
|
||||
if not path: return
|
||||
cnt_before = d.HatchPatterns.Count
|
||||
cnt_imported = 0
|
||||
try:
|
||||
# LoadFromFile(file, replaceExisting) — returnt int
|
||||
cnt_imported = int(d.HatchPatterns.LoadFromFile(path, False))
|
||||
except Exception:
|
||||
try:
|
||||
cnt_imported = int(d.HatchPatterns.Load(path))
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] HatchPatterns.Load:", ex)
|
||||
cnt_after = d.HatchPatterns.Count
|
||||
print("[PROJECT-SETTINGS] hatch import: {} (Tabelle {} -> {})".format(
|
||||
cnt_imported, cnt_before, cnt_after))
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] import_hatch_file:", ex)
|
||||
self._send_tables()
|
||||
|
||||
# ---- Hatch-Pattern CRUD ----
|
||||
def _rename_hatch(self, payload):
|
||||
idx = payload.get("index")
|
||||
new_name = (payload.get("name") or "").strip()
|
||||
if idx is None or not new_name: return
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
if d is None: return
|
||||
try:
|
||||
hp = d.HatchPatterns[int(idx)]
|
||||
if hp is None or hp.IsDeleted: return
|
||||
hp.Name = new_name
|
||||
d.HatchPatterns.Modify(hp, int(idx), True)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] rename_hatch:", ex)
|
||||
self._send_tables()
|
||||
|
||||
def _delete_hatch(self, payload):
|
||||
idx = payload.get("index")
|
||||
if idx is None: return
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
if d is None: return
|
||||
try:
|
||||
d.HatchPatterns.Delete(int(idx), True)
|
||||
except Exception as ex:
|
||||
print("[PROJECT-SETTINGS] delete_hatch:", ex)
|
||||
self._send_tables()
|
||||
|
||||
def _send_tables(self):
|
||||
"""Sendet aktuelle Linetype + Hatch-Tabellen ans Frontend
|
||||
(TABLES_UPDATED-Message). Frontend re-rendert die Listen."""
|
||||
d = Rhino.RhinoDoc.ActiveDoc
|
||||
if d is None: return
|
||||
self.send("TABLES_UPDATED", {
|
||||
"linetypes": _list_linetypes_full(d),
|
||||
"hatchPatternsFull": _list_hatch_patterns_full(d),
|
||||
})
|
||||
b = _ProjectSettingsBridge()
|
||||
bridge_holder["form"] = panel_base.open_satellite_window(
|
||||
"project_settings",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import Icon from './Icon'
|
||||
import { BarToggle, BarButton, BAR_H } from './BarControls'
|
||||
import { openLibrary, pickTextureFile, onMessage } from '../lib/rhinoBridge'
|
||||
import {
|
||||
openLibrary, pickTextureFile, onMessage,
|
||||
renameLinetype, deleteLinetype, loadLinetypeDefaults, importLinetypeFile,
|
||||
renameHatch, deleteHatch, importHatchFile,
|
||||
} from '../lib/rhinoBridge'
|
||||
|
||||
/* Field — Stack-Layout fuer komplexe Inputs (mehrere Felder nebeneinander) */
|
||||
function Field({ label, hint, children, style }) {
|
||||
@@ -67,6 +71,248 @@ function TabBar({ tabs, active, onChange }) {
|
||||
)
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
function LinetypePreview({ segments, width = 120, height = 12 }) {
|
||||
if (!segments || segments.length === 0) {
|
||||
return (
|
||||
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}
|
||||
style={{ color: 'var(--text-primary)', display: 'block' }}>
|
||||
<line x1="0" y1={height/2} x2={width} y2={height/2}
|
||||
stroke="currentColor" strokeWidth="1" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
// Dot hat length=0 — fuer Layout-Zwecke kleinen Pseudo-Wert
|
||||
const DOT_DRAW_LEN = 0.6
|
||||
const lenOf = (seg) => {
|
||||
const l = Math.abs(seg.length || 0)
|
||||
return seg.type === 'Dot' ? DOT_DRAW_LEN : l
|
||||
}
|
||||
const patternLen = segments.reduce((s, seg) => s + lenOf(seg), 0)
|
||||
if (patternLen <= 0) {
|
||||
return (
|
||||
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}
|
||||
style={{ color: 'var(--text-primary)', display: 'block' }}>
|
||||
<line x1="0" y1={height/2} x2={width} y2={height/2}
|
||||
stroke="currentColor" strokeWidth="1" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
// Statt das Pattern auf die ganze Breite zu strecken: tilen mit fixer
|
||||
// Skala. Ziel: ca. 4-5 Pattern-Repetitions sichtbar, dann ist die
|
||||
// Sequenz auch bei kurzen Patterns (Dots) deutlich erkennbar.
|
||||
const TARGET_REPETITIONS = 4
|
||||
const scale = width / (patternLen * TARGET_REPETITIONS)
|
||||
const scaledPatternLen = patternLen * scale
|
||||
const repetitions = Math.ceil(width / scaledPatternLen) + 1
|
||||
const parts = []
|
||||
let x = 0
|
||||
let key = 0
|
||||
for (let r = 0; r < repetitions && x < width; r++) {
|
||||
for (const seg of segments) {
|
||||
const drawLen = lenOf(seg) * scale
|
||||
if (x >= width) break
|
||||
const x2 = Math.min(x + drawLen, width)
|
||||
if (seg.type === 'Line' && drawLen > 0) {
|
||||
parts.push(<line key={key++} x1={x} y1={height/2}
|
||||
x2={x2} y2={height/2}
|
||||
stroke="currentColor" strokeWidth="1.4"
|
||||
strokeLinecap="square" />)
|
||||
} else if (seg.type === 'Dot') {
|
||||
parts.push(<circle key={key++} cx={x + drawLen/2} cy={height/2}
|
||||
r={1.2} fill="currentColor" />)
|
||||
}
|
||||
x += drawLen
|
||||
}
|
||||
}
|
||||
return (
|
||||
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}
|
||||
style={{ color: 'var(--text-primary)', display: 'block' }}>
|
||||
{parts}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
/* LinetypeListRow — Zeile mit Name + Preview + selected-state */
|
||||
function LinetypeListRow({ lt, isSelected, onSelect }) {
|
||||
return (
|
||||
<div onClick={onSelect}
|
||||
style={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
gap: 2,
|
||||
padding: '6px 10px',
|
||||
cursor: 'pointer',
|
||||
background: isSelected ? 'var(--accent-dim)' : 'transparent',
|
||||
borderLeft: '2px solid ' + (isSelected ? 'var(--accent)' : 'transparent'),
|
||||
transition: 'background 0.12s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isSelected) e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isSelected) e.currentTarget.style.background = 'transparent'
|
||||
}}>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-primary)',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap' }}>{lt.name || 'Unbenannt'}</span>
|
||||
<LinetypePreview segments={lt.segments} width={140} height={10} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* HatchPreview — rendert das ECHTE Hatch-Pattern (alle HatchLines mit
|
||||
Winkel/Offset/Dashes) als SVG-Tile. Solid/Gradient haben keine Lines.
|
||||
pixelsPerUnit skaliert das Pattern damit's im Tile gut sichtbar ist. */
|
||||
function HatchPreview({ pattern, size = 28, pixelsPerUnit = 8 }) {
|
||||
const ft = (pattern.fillType || '').toLowerCase()
|
||||
const stroke = 'var(--text-primary)'
|
||||
const bg = 'var(--bg-input)'
|
||||
if (ft === 'solid') {
|
||||
return (
|
||||
<svg width={size} height={size}
|
||||
style={{ display: 'block', color: stroke }}>
|
||||
<rect x="0" y="0" width={size} height={size}
|
||||
fill="currentColor"
|
||||
stroke="var(--border-light)" strokeWidth="0.5" rx="2" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
if (ft === 'gradient') {
|
||||
const id = 'gr_' + (pattern.index ?? 0)
|
||||
return (
|
||||
<svg width={size} height={size}
|
||||
style={{ display: 'block', color: stroke }}>
|
||||
<defs>
|
||||
<linearGradient id={id} x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stopColor="currentColor" />
|
||||
<stop offset="100%" stopColor="currentColor" stopOpacity="0.2" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="0" y="0" width={size} height={size}
|
||||
fill={`url(#${id})`}
|
||||
stroke="var(--border-light)" strokeWidth="0.5" rx="2" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
// Lines: jede HatchLine ist eine Familie paralleler Linien, mit
|
||||
// Angle, BasePoint, Offset (zwischen Linien), Dashes (Strich/Lücke).
|
||||
const hls = pattern.hatchLines || []
|
||||
const clipId = 'hp_clip_' + (pattern.index ?? 0)
|
||||
const pieces = []
|
||||
let lineKey = 0
|
||||
// Tile-Mitte im SVG-Koordinatensystem
|
||||
const cx = size / 2
|
||||
const cy = size / 2
|
||||
hls.forEach((hl, hi) => {
|
||||
// SVG hat Y nach unten — Rhino-Pattern definiert Y nach oben.
|
||||
// Wir spiegeln nicht, weil's nur ein Visual ist; Rotation reicht.
|
||||
const a = hl.angle || 0 // rad
|
||||
const cosA = Math.cos(a)
|
||||
const sinA = Math.sin(a)
|
||||
// Perpendicular zur Linie:
|
||||
const perpX = -sinA
|
||||
const perpY = cosA
|
||||
// Offset entlang perp + along — Rhino's Offset ist {X, Y} im
|
||||
// Pattern-Koord-System. Die Komponente PARALLEL zur Linien-Richtung
|
||||
// (entlang) entscheidet uebers Stagger (Strich-Versatz).
|
||||
const offX = hl.offX || 0
|
||||
const offY = hl.offY || 0
|
||||
// Spacing = Komponente von Offset SENKRECHT zur Linien-Richtung
|
||||
const spacing = Math.abs(offX * perpX + offY * perpY)
|
||||
const drawSpacing = Math.max(spacing * pixelsPerUnit, 1.5)
|
||||
// Stagger = Komponente von Offset PARALLEL zur Linien-Richtung
|
||||
const stagger = offX * cosA + offY * sinA
|
||||
const drawStagger = stagger * pixelsPerUnit
|
||||
// Dashes in Pixel
|
||||
const dashes = (hl.dashes || []).map(d => Math.abs(d) * pixelsPerUnit)
|
||||
const dashLen = dashes.reduce((s, d) => s + d, 0)
|
||||
const strokeDasharray = (dashes.length >= 2 && dashLen > 0.1)
|
||||
? dashes.join(',') : undefined
|
||||
// Basis-Punkt in Pixel (von Tile-Center)
|
||||
const baseX = (hl.baseX || 0) * pixelsPerUnit
|
||||
const baseY = (hl.baseY || 0) * pixelsPerUnit
|
||||
// Wir zeichnen genug parallele Linien um Tile zu fuellen
|
||||
const halfDiag = size * 1.5 // overlap fuer rotate-clip
|
||||
const nLines = Math.ceil((size * 2.0) / drawSpacing) + 4
|
||||
const start = -Math.floor(nLines / 2)
|
||||
for (let i = 0; i < nLines; i++) {
|
||||
const k = start + i
|
||||
// Mittelpunkt dieser Linie: base + k*offset, vom Tile-Center aus
|
||||
// Wir setzen die Pattern-Origin in die Tile-Mitte.
|
||||
const mx = cx + baseX + k * (offX * pixelsPerUnit)
|
||||
const my = cy + baseY + k * (offY * pixelsPerUnit)
|
||||
// Linie streckt sich +-halfDiag entlang Linien-Richtung
|
||||
const x1 = mx - cosA * halfDiag
|
||||
const y1 = my - sinA * halfDiag
|
||||
const x2 = mx + cosA * halfDiag
|
||||
const y2 = my + sinA * halfDiag
|
||||
pieces.push(<line key={'l' + lineKey++}
|
||||
x1={x1} y1={y1} x2={x2} y2={y2}
|
||||
stroke="currentColor"
|
||||
strokeWidth="0.7"
|
||||
strokeDasharray={strokeDasharray} />)
|
||||
}
|
||||
})
|
||||
// Fallback wenn keine HatchLines kamen (Hatch ist 'Lines' aber leer)
|
||||
const hasLines = pieces.length > 0
|
||||
return (
|
||||
<svg width={size} height={size}
|
||||
style={{ display: 'block', color: stroke }}
|
||||
viewBox={`0 0 ${size} ${size}`}>
|
||||
<defs>
|
||||
<clipPath id={clipId}>
|
||||
<rect x="0" y="0" width={size} height={size} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect x="0" y="0" width={size} height={size}
|
||||
fill={bg}
|
||||
stroke="var(--border-light)" strokeWidth="0.5" rx="2" />
|
||||
<g clipPath={`url(#${clipId})`}>
|
||||
{hasLines ? pieces : (
|
||||
// Fallback: einzelne Diagonale alle 4 px
|
||||
Array.from({length: Math.ceil(size * 2 / 4)}, (_, i) => i * 4 - size)
|
||||
.map(i => (
|
||||
<line key={i} x1={i} y1="0" x2={i + size} y2={size}
|
||||
stroke="currentColor" strokeWidth="0.8" />
|
||||
))
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
/* HatchListRow */
|
||||
function HatchListRow({ hp, isSelected, onSelect }) {
|
||||
return (
|
||||
<div onClick={onSelect}
|
||||
style={{
|
||||
display: 'grid', gridTemplateColumns: '32px 1fr',
|
||||
alignItems: 'center', gap: 8,
|
||||
padding: '6px 10px',
|
||||
cursor: 'pointer',
|
||||
background: isSelected ? 'var(--accent-dim)' : 'transparent',
|
||||
borderLeft: '2px solid ' + (isSelected ? 'var(--accent)' : 'transparent'),
|
||||
transition: 'background 0.12s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isSelected) e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isSelected) e.currentTarget.style.background = 'transparent'
|
||||
}}>
|
||||
<HatchPreview pattern={hp} />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--text-primary)',
|
||||
overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap' }}>{hp.name || 'Unbenannt'}</div>
|
||||
<div style={{ fontSize: 9, color: 'var(--text-muted)' }}>{hp.fillType}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* MaterialListRow — schmale Listen-Zeile links (ArchiCAD-Stil):
|
||||
Color-Swatch + Name + Source-Badge. Click selektiert. */
|
||||
function MaterialListRow({ mat, isBuiltin, isSelected, onSelect }) {
|
||||
@@ -343,6 +589,12 @@ export default function ProjectSettingsDialog({
|
||||
return null
|
||||
})
|
||||
const [matSearch, setMatSearch] = useState('')
|
||||
// Linetype + Hatch-Tabellen — initial aus params, danach via
|
||||
// TABLES_UPDATED Message vom Backend nach jeder CRUD-Op.
|
||||
const [linetypes, setLinetypes] = useState(initial.linetypes || [])
|
||||
const [hatches, setHatches] = useState(initial.hatchPatternsFull || [])
|
||||
const [selLt, setSelLt] = useState(null)
|
||||
const [selHt, setSelHt] = useState(null)
|
||||
const builtin = initial.builtinMaterials || []
|
||||
|
||||
// Aktuell ausgewaehltes Material aus Selection ableiten
|
||||
@@ -367,6 +619,14 @@ export default function ProjectSettingsDialog({
|
||||
setSelMat(null)
|
||||
}
|
||||
|
||||
// Linetype/Hatch-Tabelle nach Backend-CRUD aktualisieren
|
||||
useEffect(() => {
|
||||
onMessage('TABLES_UPDATED', ({ linetypes: lts, hatchPatternsFull: hps }) => {
|
||||
if (lts) setLinetypes(lts)
|
||||
if (hps) setHatches(hps)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// Backend-File-Picker-Antwort: aktualisiert das Slot im aktuell
|
||||
// selektierten Material. Wenn path leer = User abgebrochen → no-op.
|
||||
useEffect(() => {
|
||||
@@ -436,6 +696,8 @@ export default function ProjectSettingsDialog({
|
||||
<TabBar tabs={[
|
||||
{ key: 'defaults', label: 'Voreinstellungen' },
|
||||
{ key: 'materials', label: 'Materialien' },
|
||||
{ key: 'linetypes', label: 'Linientypen' },
|
||||
{ key: 'hatches', label: 'Schraffuren' },
|
||||
]} active={tab} onChange={setTab} />
|
||||
|
||||
{/* Body */}
|
||||
@@ -556,6 +818,188 @@ export default function ProjectSettingsDialog({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'linetypes' && (
|
||||
<div style={{ display: 'flex', height: '100%',
|
||||
margin: '-8px -14px', minHeight: 0 }}>
|
||||
<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' }}>
|
||||
{linetypes.length === 0 && (
|
||||
<div style={{ padding: 20, textAlign: 'center',
|
||||
color: 'var(--text-muted)', fontSize: 10 }}>
|
||||
Keine Linientypen geladen.
|
||||
</div>
|
||||
)}
|
||||
{linetypes.map((lt) => (
|
||||
<LinetypeListRow key={lt.index}
|
||||
lt={lt}
|
||||
isSelected={selLt === lt.index}
|
||||
onSelect={() => setSelLt(lt.index)} />
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4,
|
||||
padding: '6px 8px',
|
||||
borderTop: '1px solid var(--border-light)' }}>
|
||||
<BarToggle icon="upload_file" onClick={importLinetypeFile}
|
||||
title="Aus .lin-Datei importieren" />
|
||||
<BarToggle icon="refresh" onClick={loadLinetypeDefaults}
|
||||
title="Rhino-Defaults laden" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden',
|
||||
padding: '12px 14px', overflowY: 'auto' }}>
|
||||
{(() => {
|
||||
const lt = linetypes.find(x => x.index === selLt)
|
||||
if (!lt) return (
|
||||
<div style={{ padding: 40, textAlign: 'center',
|
||||
color: 'var(--text-muted)', fontSize: 11 }}>
|
||||
Linientyp links auswählen.
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<DetailSection title="Identität">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<input type="text" value={lt.name}
|
||||
onChange={(ev) => {
|
||||
const newName = ev.target.value
|
||||
setLinetypes(arr => arr.map(x =>
|
||||
x.index === lt.index ? { ...x, name: newName } : x))
|
||||
}}
|
||||
onBlur={(ev) => renameLinetype(lt.index, ev.target.value)}
|
||||
style={{ flex: 1, height: BAR_H, padding: '0 12px',
|
||||
fontSize: 12, fontWeight: 500 }} />
|
||||
{!lt.isContinuous && (
|
||||
<BarButton icon="delete"
|
||||
onClick={() => { deleteLinetype(lt.index); setSelLt(null) }}
|
||||
title="Linientyp löschen" />
|
||||
)}
|
||||
</div>
|
||||
</DetailSection>
|
||||
<DetailSection title="Vorschau">
|
||||
<div style={{ padding: '8px 12px',
|
||||
background: 'var(--bg-section)',
|
||||
borderRadius: 4 }}>
|
||||
<LinetypePreview segments={lt.segments}
|
||||
width={280} height={16} />
|
||||
</div>
|
||||
</DetailSection>
|
||||
{lt.segments.length > 0 && (
|
||||
<DetailSection title="Segmente">
|
||||
<div style={{ fontSize: 10, fontFamily: 'var(--font-mono)',
|
||||
color: 'var(--text-muted)',
|
||||
lineHeight: 1.6 }}>
|
||||
{lt.segments.map((s, i) => (
|
||||
<div key={i}>
|
||||
{String(i+1).padStart(2, '0')} —
|
||||
{' '}{s.type.padEnd(10)}
|
||||
{' '}{s.length.toFixed(3)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DetailSection>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'hatches' && (
|
||||
<div style={{ display: 'flex', height: '100%',
|
||||
margin: '-8px -14px', minHeight: 0 }}>
|
||||
<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' }}>
|
||||
{hatches.length === 0 && (
|
||||
<div style={{ padding: 20, textAlign: 'center',
|
||||
color: 'var(--text-muted)', fontSize: 10 }}>
|
||||
Keine Schraffuren geladen.
|
||||
</div>
|
||||
)}
|
||||
{hatches.map((hp) => (
|
||||
<HatchListRow key={hp.index}
|
||||
hp={hp}
|
||||
isSelected={selHt === hp.index}
|
||||
onSelect={() => setSelHt(hp.index)} />
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4,
|
||||
padding: '6px 8px',
|
||||
borderTop: '1px solid var(--border-light)' }}>
|
||||
<BarToggle icon="upload_file" onClick={importHatchFile}
|
||||
title="Aus .pat-Datei importieren" />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden',
|
||||
padding: '12px 14px', overflowY: 'auto' }}>
|
||||
{(() => {
|
||||
const hp = hatches.find(x => x.index === selHt)
|
||||
if (!hp) return (
|
||||
<div style={{ padding: 40, textAlign: 'center',
|
||||
color: 'var(--text-muted)', fontSize: 11 }}>
|
||||
Schraffur links auswählen.
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<DetailSection title="Identität">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<HatchPreview pattern={hp} size={36} />
|
||||
<input type="text" value={hp.name}
|
||||
onChange={(ev) => {
|
||||
const newName = ev.target.value
|
||||
setHatches(arr => arr.map(x =>
|
||||
x.index === hp.index ? { ...x, name: newName } : x))
|
||||
}}
|
||||
onBlur={(ev) => renameHatch(hp.index, ev.target.value)}
|
||||
style={{ flex: 1, height: BAR_H, padding: '0 12px',
|
||||
fontSize: 12, fontWeight: 500 }} />
|
||||
<BarButton icon="delete"
|
||||
onClick={() => { deleteHatch(hp.index); setSelHt(null) }}
|
||||
title="Schraffur löschen" />
|
||||
</div>
|
||||
<div style={{ fontSize: 9, color: 'var(--text-muted)',
|
||||
marginTop: 4 }}>
|
||||
Typ: {hp.fillType}
|
||||
{hp.isReference && ' · importiert'}
|
||||
</div>
|
||||
</DetailSection>
|
||||
{hp.description && (
|
||||
<DetailSection title="Beschreibung">
|
||||
<div style={{ fontSize: 10,
|
||||
color: 'var(--text-muted)',
|
||||
lineHeight: 1.5 }}>
|
||||
{hp.description}
|
||||
</div>
|
||||
</DetailSection>
|
||||
)}
|
||||
<DetailSection title="Verwendung">
|
||||
<div style={{ fontSize: 10, color: 'var(--text-muted)',
|
||||
lineHeight: 1.5 }}>
|
||||
Schraffuren werden ueber das Gestaltung-Panel
|
||||
oder via Layer Section-Hatch im Ebenen-Editor
|
||||
auf Objekte/Layer angewendet.
|
||||
</div>
|
||||
</DetailSection>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer — Pill-Buttons */}
|
||||
|
||||
@@ -194,6 +194,16 @@ export function openLibrary() { send('OPEN_LIBRARY', {}) }
|
||||
export function pickTextureFile(slot) {
|
||||
send('PICK_TEXTURE_FILE', { slot: slot || 'diffuse' })
|
||||
}
|
||||
// Linetype-Tabelle (doc.Linetypes) verwalten — Antwort via
|
||||
// TABLES_UPDATED-Message mit {linetypes, hatchPatternsFull}.
|
||||
export function renameLinetype(index, name) { send('RENAME_LINETYPE', { index, name }) }
|
||||
export function deleteLinetype(index) { send('DELETE_LINETYPE', { index }) }
|
||||
export function loadLinetypeDefaults() { send('LOAD_LINETYPE_DEFAULTS', {}) }
|
||||
export function importLinetypeFile() { send('IMPORT_LINETYPE_FILE', {}) }
|
||||
// Hatch-Pattern-Tabelle (doc.HatchPatterns) verwalten
|
||||
export function renameHatch(index, name) { send('RENAME_HATCH', { index, name }) }
|
||||
export function deleteHatch(index) { send('DELETE_HATCH', { index }) }
|
||||
export function importHatchFile() { send('IMPORT_HATCH_FILE', {}) }
|
||||
// Schnitt/Ansicht — interaktiver 2-Punkt-Pick im Rhino-Viewport. Erzeugt
|
||||
// eine neue Zeichnungsebene type=schnitt + 2D-Plan-Symbol + aktiviert sie.
|
||||
// opts: { cutAtLine: bool, depthBack: m, heightMin: m, heightMax: m, namePrefix }
|
||||
|
||||
Reference in New Issue
Block a user