/** * rhinoBridge.js — Kommunikation React ↔ Rhino/Python * Lange Payloads werden in Chunks <700 Zeichen aufgeteilt (document.title-Limit). */ const CHUNK_SIZE = 700 let _queue = [] let _busy = false function _flush() { if (_busy || _queue.length === 0) return _busy = true document.title = 'RHINOMSG::' + _queue.shift() setTimeout(() => { _busy = false; _flush() }, 80) } // Mac WKWebView -> .NET String-Bruecke kann mit non-ASCII-Bytes haengen // (Umlaute wie A-Umlaut/O-Umlaut/U-Umlaut -> "Unable to translate bytes from specified code page"). // Vor Transport ASCII-eskapen, Python's json.loads dekodiert die \uXXXX wieder. const _NON_ASCII = /[^\x00-\x7f]/g function _asciiEscape(s) { return s.replace(_NON_ASCII, (c) => '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4)) } export function send(type, payload = {}) { if (!window.RHINO_MODE) { console.log('[Bridge] →', type, payload); return } const json = _asciiEscape(JSON.stringify({ type, payload })) if (json.length <= CHUNK_SIZE) { _queue.push(json) } else { const total = Math.ceil(json.length / CHUNK_SIZE) for (let i = 0; i < total; i++) { _queue.push(_asciiEscape(JSON.stringify({ _chunk: { i, n: total }, d: json.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE), }))) } } _flush() } const _handlers = {} export function onMessage(type, handler) { _handlers[type] = handler } if (typeof window !== 'undefined') { window.onRhinoMessage = (data) => { const h = _handlers[data.type] if (h) h(data.payload) else console.log('[Bridge] ←', data.type, data.payload) } } export function notifyReady() { const tryReady = () => { if (window.RHINO_MODE) send('READY', {}) else setTimeout(tryReady, 50) } tryReady() } export function applyAll(zeichnungsebenen, ebenen) { send('APPLY', { zeichnungsebenen, ebenen }) } export function setLayerVisibility(code, visible) { send('LAYER_VISIBILITY', { code, visible }) } export function setLayerLock(code, locked) { send('LAYER_LOCK', { code, locked }) } export function setLayerStyle(code, color, lw) { send('LAYER_STYLE', { code, color, lw }) } export function setActiveZeichnungsebene(z) { send('SET_ACTIVE', { id: z.id, name: z.name, isGeschoss: !!z.isGeschoss, okff: z.okff ?? null }) } export function setActiveEbene(code) { send('SET_ACTIVE_LAYER', { code }) } export function deleteEbene(code, moveTo) { send('DELETE_EBENE', { code, moveTo: moveTo || null }) } export function moveSelectionToEbene(code) { send('MOVE_SELECTION_TO_LAYER', { code }) } export function setClippingEnabled(enabled) { send('SET_CLIPPING', { enabled: !!enabled }) } // --- Ausschnitte --- export function listAusschnitte() { send('LIST', {}) } export function saveAusschnitt(name) { send('SAVE', { name }) } export function updateAusschnitt(id) { send('UPDATE', { id }) } export function restoreAusschnitt(id) { send('RESTORE', { id }) } export function applyAusschnittToDetail(id) { send('APPLY_TO_DETAIL', { id }) } export function renameAusschnitt(id, name) { send('RENAME', { id, name }) } export function deleteAusschnitt(id) { send('DELETE', { id }) } export function setAusschnittFolder(id, folder) { send('SET_FOLDER', { id, folder }) } export function setAusschnittScale(id, scale) { send('SET_SCALE', { id, scale }) } export function duplicateAusschnitt(id) { send('DUPLICATE', { id }) } export function addAusschnittFolder(name) { send('ADD_FOLDER', { name }) } export function removeAusschnittFolder(name) { send('REMOVE_FOLDER', { name }) } export function getAusschnittLayers(id) { send('GET_LAYERS', { id }) } export function updateAusschnittLayers(id, layers) { send('UPDATE_LAYERS', { id, layers }) } export function saveLayerPreset(name, layers) { send('SAVE_PRESET', { name, layers }) } export function deleteLayerPreset(name) { send('DELETE_PRESET', { name }) } export function openAusschnittSettings(id) { send('OPEN_SETTINGS', { id }) } // --- Gestaltung-Panel --- export function requestSelection() { send('GET_SELECTION', {}) } export function setColorSource(source, color) { send('SET_COLOR_SOURCE', { source, color: color || null }) } export function setLwSource(source, lw) { send('SET_LW_SOURCE', { source, lw: lw == null ? null : Number(lw) }) } export function setLinetypeSource(source, name) { send('SET_LINETYPE_SOURCE', { source, name: name || null }) } export function setLinetypeScale(scale) { send('SET_LINETYPE_SCALE', { scale: scale == null ? null : Number(scale) }) } export function setFill(enabled, source, color, pattern, scale, rotation) { send('SET_FILL', { enabled: !!enabled, source: source || 'object', color: color || null, pattern: pattern || null, scale: scale == null ? null : Number(scale), rotation: rotation == null ? null : Number(rotation), }) } // --- Massstab-Panel --- export function requestMassstab() { send('REQUEST_STATE', {}) } export function setMassstab(ratio) { send('SET_SCALE', { ratio: Number(ratio) }) } export function zoomOneToOne() { send('ZOOM_ONE_TO_ONE', {}) } export function zoomExtents() { send('ZOOM_EXTENTS', {}) } export function zoomSelection() { send('ZOOM_SELECTION', {}) } export function setMassstabDpi(dpi) { send('SET_DPI', { dpi: Number(dpi) }) } export function detectMassstabDpi() { send('DETECT_DPI', {}) } export function setShowLineweights(on) { send('SET_LINEWEIGHTS',{ enabled: !!on }) } // --- Werkzeuge-Panel --- export function runRhinoCommand(cmd) { send('RUN', { cmd }) } // --- Oberleiste-Panel --- export function setView(view) { send('SET_VIEW', { view }) } export function setDisplayMode(name) { send('SET_DISPLAY_MODE', { name }) } export function toggleOrtho(on) { send('TOGGLE_ORTHO', { enabled: !!on }) } export function toggleGridSnap(on) { send('TOGGLE_GRID_SNAP', { enabled: !!on }) } export function toggleOsnap(on) { send('TOGGLE_OSNAP', { enabled: !!on }) } export function toggleOverrides(on) { send('TOGGLE_OVERRIDES', { enabled: !!on }) } export function setOverridesPreset(name) { send('SET_OVERRIDES_PRESET', { name: name || null }) } export function saveOverridesPreset(name) { send('SAVE_OVERRIDES_PRESET', { name }) } export function openOverridesPanel() { send('OPEN_OVERRIDES_PANEL', {}) } export function openKameraPanel() { send('OPEN_KAMERA_PANEL', {}) } export function openElementeUebersicht() { send('OPEN_ELEMENTE_UEBERSICHT', {}) } export function openElementeProperties() { send('OPEN_ELEMENTE_PROPERTIES', {}) } export function setDarstellung(d) { send('SET_DARSTELLUNG', { darstellung: d || '' }) } // Anordnen — 2D-Z-Stack via Rhino-DisplayOrder. dir: 'front'|'forward'|'backward'|'back' export function arrangeSelection(dir) { send('ARRANGE', { dir }) } // Referenzlinien-Layer (Code 19) on/off — Shortcut zur Layer-Sichtbarkeit // damit der User nicht durchs Ebenen-Panel muss. Layer bleibt erhalten, // Ausschnitte speichern den State automatisch mit. export function toggleReferenzlinien(visible) { send('TOGGLE_REFERENZLINIEN', { visible: !!visible }) } // 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 } export function createSchnitt(opts = {}) { send('CREATE_SCHNITT', { cutAtLine: true, depthBack: 8.0, heightMin: -1.0, heightMax: 12.0, ...opts, }) } export function deleteSchnitt(id) { send('DELETE_SCHNITT', { id }) } 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 openAbout() { send('OPEN_ABOUT', {}) } export function createText() { send('CREATE_TEXT', {}) } export function setTextSettings(settings) { send('SET_TEXT_SETTINGS', { settings }) } export function applyTextStyle(id) { send('APPLY_TEXT_STYLE', { id }) } export function saveTextStyle(name) { send('SAVE_TEXT_STYLE', { name }) } export function deleteTextStyle(id) { send('DELETE_TEXT_STYLE', { id }) } // --- Masse (in Oberleiste + Satellite-Fenster MasseSettings) --- // Topbar: aktives Mass setzen + Settings-Fenster oeffnen export function setMasseActive(id) { send('SET_MASSE_ACTIVE', { id }) } export function openMasseSettings() { send('OPEN_MASSE_SETTINGS', {}) } // MasseSettings-Fenster: eigene Bridge (Topics ohne MASSE_ prefix) export function masseSetActive(id) { send('SET_ACTIVE', { id }) } export function masseSavePreset(preset) { send('SAVE', { preset }) } export function masseDeletePreset(id) { send('DELETE', { id }) } // --- Kamera-Panel --- export function setKameraViewport(state) { send('SET_VIEWPORT', { ...state }) } export function setKameraProjection(parallel) { send('SET_PROJECTION', { parallel: !!parallel }) } export function setKameraIso(octant) { send('SET_ISO', { octant: octant || 'NE' }) } export function kameraZoomExtents() { send('ZOOM_EXTENTS', {}) } export function saveKameraPreset(name) { send('SAVE_PRESET', { name }) } export function applyKameraPreset(id) { send('APPLY_PRESET', { id }) } export function deleteKameraPreset(id) { send('DELETE_PRESET', { id }) } export function setKameraNorthAngle(angle) { send('SET_NORTH_ANGLE', { angle: Number(angle) || 0 }) } // Ebenenkombinationen (gehosted in Oberleiste, gleicher Store wie EBENEN) export function pickLayerCombination(name) { send('PICK_LAYER_COMBINATION', { name: name || null }) } export function saveLayerCombination(name) { send('SAVE_LAYER_COMBINATION', { name }) } export function deleteLayerCombination(name) { send('DELETE_LAYER_COMBINATION', { name }) } export function openLayerCombinationsDialog() { send('OPEN_LAYER_COMBINATIONS_DIALOG', {}) } export function runCommand(cmd) { send('RUN_COMMAND', { cmd }) } export function sendKeys(text, enter) { send('SEND_KEYS', { text, enter: enter !== false }) } export function cancelCommand() { send('CANCEL_COMMAND', {}) } export function toggleRhinoCmdLine() { send('TOGGLE_RHINO_CMD_LINE', {}) } export function getDossierSettings() { send('GET_SETTINGS', {}) } export function openDossierSettings() { send('OPEN_SETTINGS', {}) } export function applyWindowLayout(name) { send('APPLY_LAYOUT', { name }) } export function saveLayoutPref(name, autoApply) { send('SAVE_LAYOUT_PREF', { defaultLayout: name || '', autoApplyLayout: !!autoApply }) } // --- Overrides-Panel --- export function setOverridesEnabled(on) { send('SET_ENABLED', { enabled: !!on }) } export function addRule(rule) { send('ADD_RULE', { rule }) } export function updateRule(id, rule) { send('UPDATE_RULE', { id, rule }) } export function deleteRule(id) { send('DELETE_RULE', { id }) } export function reorderRules(order) { send('REORDER_RULES', { order }) } export function duplicateRule(id) { send('DUPLICATE_RULE', { id }) } export function reapplyOverrides() { send('REAPPLY', {}) } export function clearOverrideRules() { send('CLEAR_RULES', {}) } export function savePreset(name) { send('SAVE_PRESET', { name }) } export function loadPreset(name, mode) { send('LOAD_PRESET', { name, mode: mode || 'replace' }) } export function deletePreset(name) { send('DELETE_PRESET', { name }) } // Rule-Templates: einzelne Regel speichern/anwenden/loeschen export function saveRuleTemplate(name, rule) { send('SAVE_RULE_TEMPLATE', { name, rule }) } export function addFromTemplate(name) { send('ADD_FROM_TEMPLATE', { name }) } export function deleteRuleTemplate(name) { send('DELETE_RULE_TEMPLATE', { name }) } // --- Dimensionen-Panel --- export function setRefPoint(x, y, z) { send('SET_REF_POINT', { x, y, z }) } export function setCoordSystem(mode) { send('SET_COORD_SYSTEM', { mode }) } export function setDimPosition(axis, value) { send('SET_POSITION', { axis, value }) } export function setDimDimension(axis, value) { send('SET_DIMENSION', { axis, value }) } export function setDimRotationZ(angle) { send('SET_ROTATION_Z', { angle }) } export function setCircleRadius(value) { send('SET_CIRCLE_RADIUS', { value }) } export function setLineLength(value) { send('SET_LINE_LENGTH', { value }) } export function setRectangleDims(width, height) { send('SET_RECTANGLE', { width, height }) } // --- Layouts-Panel --- export function listLayouts() { send('LIST', {}) } export function newLayout(name, format, landscape, customWidth, customHeight) { send('NEW_LAYOUT', { name, format, landscape: !!landscape, customWidth, customHeight }) } export function deleteLayout(id) { send('DELETE_LAYOUT', { id }) } export function renameLayout(id, name) { send('RENAME_LAYOUT', { id, name }) } export function activateLayout(id) { send('ACTIVATE_LAYOUT', { id }) } export function addDetail(pageId, ausschnittId) { send('ADD_DETAIL', { pageId, ausschnittId: ausschnittId || null }) } export function deleteDetail(pageId, detailId) { send('DELETE_DETAIL', { pageId, detailId }) } export function bindAusschnitt(pageId, detailId, ausschnittId) { send('BIND_AUSSCHNITT', { pageId, detailId, ausschnittId: ausschnittId || null }) } export function syncDetail(pageId, detailId) { send('SYNC_DETAIL', { pageId, detailId }) } export function syncLayout(id) { send('SYNC_LAYOUT', { id }) } export function setPageSize(id, format, landscape, customWidth, customHeight) { send('SET_PAGE_SIZE', { id, format, landscape: !!landscape, customWidth, customHeight }) } export function exportPdf(id, dpi) { send('EXPORT_PDF', { id, dpi: dpi || 300 }) } export function openLayoutDialog(mode, layout) { send('OPEN_LAYOUT_DIALOG', { mode: mode || 'new', layout: layout || null }) } export function exportPdfAll(dpi) { send('EXPORT_PDF', { dpi: dpi || 300 }) } export function exportPdfMany(ids, dpi) { send('EXPORT_PDF', { ids, dpi: dpi || 300 }) } export function addLayoutFolder(name) { send('ADD_FOLDER', { name }) } export function removeLayoutFolder(name) { send('REMOVE_FOLDER', { name }) } export function setLayoutFolder(id, folder) { send('SET_FOLDER', { id, folder }) } // --- Elemente-Panel (Smart Architektur-Elemente) --- export function listElemente() { send('LIST', {}) } export function createWall(p) { send('CREATE_WALL', p || {}) } export function createDecke(p) { send('CREATE_DECKE', p || {}) } export function createDach(p) { send('CREATE_DACH', p || {}) } export function createFenster(p) { send('CREATE_FENSTER', p || {}) } export function createTuer(p) { send('CREATE_TUER', p || {}) } export function createAussparung(p) { send('CREATE_AUSSPARUNG', p || {}) } export function createTreppe(p) { send('CREATE_TREPPE', p || {}) } export function createStuetze(p) { send('CREATE_STUETZE', p || {}) } export function createTraeger(p) { send('CREATE_TRAEGER', p || {}) } export function createRaum(p) { send('CREATE_RAUM', p || {}) } export function exportRaeume() { send('EXPORT_RAEUME', {}) } export function openSwisstopo(mode) { send('OPEN_SWISSTOPO', { mode: mode || 'both' }) } export function importSwisstopo(kind) { send('IMPORT_SWISSTOPO', { kind: kind || 'buildings' }) } export function openSwisstopoDialog() { send('OPEN_SWISSTOPO_DIALOG', {}) } export function openOsmDialog() { send('OPEN_OSM_DIALOG', {}) } export function updateElement(id, patch) { send('UPDATE_ELEMENT', { id, ...(patch || {}) }) } export function deleteElement(id) { send('DELETE_ELEMENT', { id }) } // Backwards-Compat-Aliases export function updateWall(id, patch) { send('UPDATE_WALL', { id, ...(patch || {}) }) } export function deleteWall(id) { send('DELETE_WALL', { id }) } export function regenerateAllWalls() { send('REGENERATE_ALL', {}) } export function regenerateAllElements() { send('REGENERATE_ALL', {}) } let _visTimer = null let _visArgs = null // --- EBENEN: Ebenenkombinationen (geteilter Store mit AUSSCHNITTE) --- export function getCombination() { send('GET_COMBINATION', {}) } export function applyCombination(layersOrPreset) { // Akzeptiert entweder ein Array von Layer-States (alt) oder ein Preset- // Objekt {layers, dossierEbenen, dossierZeichnungsebenen} (neu). Backend kommt // mit beiden Formaten klar. if (Array.isArray(layersOrPreset)) { send('APPLY_COMBINATION', { layers: layersOrPreset }) } else if (layersOrPreset && typeof layersOrPreset === 'object') { send('APPLY_COMBINATION', layersOrPreset) } else { send('APPLY_COMBINATION', { layers: [] }) } } export function saveCombinationPreset(name, layers) { send('SAVE_PRESET', { name, layers }) } export function saveCurrentAsCombination(name) { send('SAVE_CURRENT_AS_PRESET', { name }) } export function deleteCombinationPreset(name) { send('DELETE_PRESET', { name }) } // Satelliten-Fenster oeffnen — Python oeffnet ein echtes Rhino-Fenster // (Eto.Form mit eingebetteter WebView) mit dem Settings-Dialog. export function openGeschossSettings(geschoss) { send('OPEN_GESCHOSS_SETTINGS', { geschoss }) } export function openEbenenSettings(ebene, hatchPatterns) { send('OPEN_EBENEN_SETTINGS', { ebene, hatchPatterns }) } export function openGeschossDialog(zeichnungsebenen) { send('OPEN_GESCHOSS_DIALOG', { zeichnungsebenen }) } export function applyVisibility(activeZ, zeichnungsebenen, activeCode, ebenen, zMode, eMode) { // Split-Panels koennen mit null/[] fuer fremde Slice aufrufen — Backend // fuellt aus doc.Strings. Hier robust gegen alles Falsy. _visArgs = { activeZ, zeichnungsebenen, activeCode, ebenen, zMode, eMode } if (_visTimer) clearTimeout(_visTimer) _visTimer = setTimeout(() => { _visTimer = null const a = _visArgs const zList = Array.isArray(a.zeichnungsebenen) ? a.zeichnungsebenen : [] const eList = Array.isArray(a.ebenen) ? a.ebenen : [] const slimZ = zList.map(z => ({ id: z.id, name: z.name, visible: z.visible !== false, locked: z.locked === true, })) // Rekursiv durch Children — sonst landen Sub-Ebenen-Toggles nicht beim // Backend. const slimEbene = (e) => { const out = { code: e.code, visible: e.visible !== false, locked: e.locked === true, } if (Array.isArray(e.children) && e.children.length) { out.children = e.children.map(slimEbene) } return out } const slimE = eList.map(slimEbene) send('SET_VISIBILITY', { activeZ: a.activeZ ? { id: a.activeZ.id } : null, activeCode: a.activeCode, zeichnungsebenen: slimZ, ebenen: slimE, zMode: a.zMode, eMode: a.eMode, }) }, 30) }