v0.7.1: UI-overhaul, ribbon vervangen door de indeling van CAD/BIM-pakketten met elke functie op één plek: bovenbalk met menu Bestand (nieuw, openen, opslaan, exporteren, projectgegevens, kosten & tijden) · Beeld · Help, de ontwerpen als tabs en de knop Opslaan; links de structuur met inklapbare groepen (onthouden, groep met de selectie klapt vanzelf open); werkbalk boven de viewports met gereedschap (Selecteren V, Binnenwand W, Ruimte R, Inrichting I, opties van het gereedschap ernaast, Unit toevoegen) en weergave (één Lagen-menu met bouwfase voor 2D en 3D, Omgeving, Doorsnede X, Render, Lopen L, Rapport 4, 2D + 3D / 2D / 3D met 1/2/3); rechts het eigenschappenpaneel (Inspector: selectie met kruimelpad, of de woning met kerncijfers en inklapbare secties Gevel, Dak, Binnen, Verlichting, Elektra, Constructie); statusbalk met hint en meldingen; wand- en vloer/dak-editor in dezelfde indeling (paneel rechts met tab Resultaat, terug met ← Woning of Esc); sneltoetsenoverzicht (?), Ctrl+O, Ctrl+\ panelen verbergen; rapport als masonry; dubbele functies weg (lagenbalk, Beeld-tab en Weergave-filter, export en kosten op twee plekken, Snel nieuw); oude onderdelen gelijkgetrokken (dialoogknoppen, materiaalknoppen, chassis als schakelaar, eenheden); weergavelagen (ViewLayers) i.p.v. bouwfasen, elk los aan/uit; render-export met Blender-script voor de lampen; elektraleidingen boven een vals plafond en haaks binnen de units bij een L/T-vorm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
adb3c74f85
commit
ab0dca9bb5
@@ -4,7 +4,7 @@
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* Zie LICENTIE.md.
|
||||
*/
|
||||
const APP_VERSION = '0.7.0';
|
||||
const APP_VERSION = '0.7.1';
|
||||
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
||||
|
||||
let tabs = [];
|
||||
@@ -28,72 +28,83 @@ function defaultNameFor(kind) {
|
||||
function setWorkspaceEmpty() {
|
||||
document.getElementById('workspace').innerHTML = `<div id="workspace-empty">
|
||||
<div>Geen ontwerp geopend.</div>
|
||||
<div style="font-size:11px;">Bestand → Nieuw → kies een objecttype</div>
|
||||
<div style="font-size:12px;">Bestand › Nieuw (Ctrl+N)</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function updateEditMenuState() {
|
||||
Ribbon.refresh();
|
||||
const editorHas = (fn) => !!(currentEditor && typeof currentEditor[fn] === 'function' && (!currentEditor.can || currentEditor.can(fn)));
|
||||
const editorCall = (fn, ...args) => { if (editorHas(fn)) return currentEditor[fn](...args); };
|
||||
const viewState = () => editorCall('viewState') || {};
|
||||
|
||||
// Menu met de sneltoets per item; KEY_HELP hieronder is het overzicht (Help › Sneltoetsen). Editor-acties werken alleen als de editor ze kent.
|
||||
const MENUS = [
|
||||
{ label: 'Bestand', items: [
|
||||
{ label: 'Nieuw…', icon: 'file-earmark-plus', keys: 'Ctrl+N', run: openNewModal },
|
||||
{ label: 'Openen…', icon: 'folder2-open', keys: 'Ctrl+O', run: () => editorCall('triggerImport'), enabled: () => editorHas('triggerImport') },
|
||||
{ label: 'Opslaan (JSON + PDF)', icon: 'save', keys: 'Ctrl+S', run: () => editorCall('exportDesign'), enabled: () => editorHas('exportDesign') },
|
||||
{ sep: true }, { head: 'Exporteren' },
|
||||
{ label: 'CAD (DXF)', icon: 'vector-pen', run: () => editorCall('exportDxf'), enabled: () => editorHas('exportDxf') },
|
||||
{ label: 'BIM (IFC)', icon: 'boxes', run: () => editorCall('exportIfc'), enabled: () => editorHas('exportIfc') },
|
||||
{ label: 'Render (OBJ)', icon: 'badge-3d', run: () => editorCall('exportObj'), enabled: () => editorHas('exportObj') },
|
||||
{ sep: true },
|
||||
{ label: 'Projectgegevens…', icon: 'card-text', run: () => editorCall('openProjectProperties'), enabled: () => editorHas('openProjectProperties') },
|
||||
{ label: 'Kosten & tijden…', icon: 'calculator', run: () => CostSettings.open() },
|
||||
{ sep: true },
|
||||
{ label: 'Ontwerp sluiten', icon: 'x-lg', run: () => activeTabId !== null && closeTab(activeTabId), enabled: () => activeTabId !== null }
|
||||
] },
|
||||
{ label: 'Beeld', items: [
|
||||
...UI.VIEWS.map(v => ({ label: v.label, icon: v.icon, keys: v.key1, run: () => editorCall('setView', v.key),
|
||||
enabled: () => editorHas('setView'), checked: () => viewState().view === v.key })),
|
||||
{ label: 'Rapport', icon: 'clipboard-data', keys: '4', run: () => editorCall('toggleReport'), enabled: () => editorHas('toggleReport'), checked: () => !!viewState().report },
|
||||
{ sep: true },
|
||||
{ label: 'Doorsnede', icon: 'scissors', keys: 'X', run: () => editorCall('toggleSection'), enabled: () => editorHas('toggleSection'), checked: () => !!viewState().section },
|
||||
{ label: 'Plattegrond passend', icon: 'aspect-ratio', keys: 'F', run: () => editorCall('fitView'), enabled: () => editorHas('fitView') },
|
||||
{ label: 'Lopen', icon: 'person-walking', keys: 'L', run: () => editorCall('toggleWalk'), enabled: () => editorHas('toggleWalk'), checked: () => !!viewState().walk },
|
||||
{ sep: true },
|
||||
{ label: 'Panelen verbergen', icon: 'layout-sidebar-inset', keys: 'Ctrl+\\', run: togglePanels, checked: () => document.body.classList.contains('panels-hidden') },
|
||||
{ label: 'Volledig scherm', icon: 'fullscreen', keys: 'F11', run: toggleBrowserFullscreen, checked: () => !!document.fullscreenElement }
|
||||
] },
|
||||
{ label: 'Help', items: [
|
||||
{ label: 'Sneltoetsen en bediening', icon: 'keyboard', keys: '?', run: openKeys },
|
||||
{ label: 'Over Tectum', icon: 'info-circle', run: () => document.getElementById('about-overlay').classList.add('open') }
|
||||
] }
|
||||
];
|
||||
|
||||
const KEY_HELP = [
|
||||
['Bestand', [['Ctrl+N', 'Nieuw ontwerp'], ['Ctrl+O', 'Openen'], ['Ctrl+S', 'Opslaan (JSON + PDF)']]],
|
||||
['Gereedschap (woning)', [['V', 'Selecteren'], ['W', 'Binnenwand tekenen'], ['R', 'Ruimte vullen'], ['I', 'Inrichting plaatsen'],
|
||||
['Esc', 'Gereedschap stoppen, daarna selectie opheffen'], ['Del', 'Selectie verwijderen'], ['D / Shift+D', 'Inrichting draaien'], ['Dubbelklik', 'Onderdeel bewerken (wand, vloer)']]],
|
||||
['Beeld', [['1 / 2 / 3', '2D + 3D, alleen 2D, alleen 3D'], ['4', 'Rapport aan/uit'], ['X', 'Doorsnede aan/uit'], ['F', 'Plattegrond passend'],
|
||||
['L', 'Lopen aan/uit'], ['Ctrl+\\', 'Panelen verbergen'], ['F11', 'Volledig scherm']]],
|
||||
['Wand-editor', [['Shift+H', 'Horizontaal spiegelen']]],
|
||||
['3D rondkijken', [['Slepen', 'Draaien'], ['Rechts slepen', 'Verschuiven'], ['Scrollwiel', 'Zoomen']], true],
|
||||
['Plattegrond', [['Scrollwiel', 'Zoomen'], ['Slepen op lege plek', 'Verschuiven'], ['Middelste muisknop', 'Verschuiven']], true],
|
||||
['Lopen', [['W / S', 'Vooruit / achteruit (ook pijl op/neer)'], ['A / D', 'Opzij'], ['← / →', 'Draaien'], ['Shift', 'Rennen'], ['E', 'Deur of schakelaar bedienen'], ['L / Esc', 'Stoppen met lopen']]],
|
||||
['Rondkijken tijdens lopen', [['Slepen', 'Rondkijken']], true]
|
||||
];
|
||||
|
||||
function openKeys() {
|
||||
// Muisacties als tekst, toetsen als <kbd>.
|
||||
document.getElementById('keys-body').innerHTML = KEY_HELP.map(([title, rows, mouse]) =>
|
||||
`<section><h3>${title}</h3>${rows.map(([k, d]) => `<div class="keys-row"><span>${mouse ? `<span class="keys-mouse">${k}</span>`
|
||||
: k.split(' / ').map(x => `<kbd>${x}</kbd>`).join(' / ')}</span><span>${d}</span></div>`).join('')}</section>`).join('');
|
||||
document.getElementById('keys-overlay').classList.add('open');
|
||||
}
|
||||
|
||||
const editorHas = (fn) => !!(currentEditor && typeof currentEditor[fn] === 'function');
|
||||
const editorCall = (fn) => { if (editorHas(fn)) currentEditor[fn](); };
|
||||
const disabledUnless = (fn) => editorHas(fn) ? '' : 'disabled';
|
||||
|
||||
function appRibbonTabs() {
|
||||
const { big, small } = Ribbon;
|
||||
const wireClicks = (body) => body.querySelectorAll('[data-app]').forEach(b => b.addEventListener('click', () => {
|
||||
const a = b.dataset.app;
|
||||
if (a === 'new') openNewModal();
|
||||
else if (a === 'new-kind') openNew(b.dataset.kind, b.dataset.template);
|
||||
else if (a === 'fullscreen') toggleBrowserFullscreen();
|
||||
else if (a === 'costs') CostSettings.open();
|
||||
else if (a === 'about') document.getElementById('about-overlay').classList.add('open');
|
||||
else editorCall(a);
|
||||
}));
|
||||
return [
|
||||
{ key: 'bestand', label: 'Bestand', order: 0, wire: wireClicks, groups: () => [
|
||||
{ label: 'Document', html:
|
||||
big('Nieuw', 'file-earmark-plus', 'data-app="new" title="Nieuw ontwerp (Ctrl+N)"') +
|
||||
big('Openen', 'folder2-open', `data-app="triggerImport" title="JSON-ontwerp openen" ${disabledUnless('triggerImport')}`) +
|
||||
big('Opslaan', 'save', `data-app="exportDesign" title="Opslaan als JSON + PDF (Ctrl+S)" ${disabledUnless('exportDesign')}`) },
|
||||
{ label: 'Exporteren', html:
|
||||
big('CAD (DXF)', 'vector-pen', `data-app="exportDxf" title="Plattegrond en gevels als DXF voor CAD (AutoCAD, BricsCAD, …), 1:1 in mm; daar op te slaan als DWG" ${disabledUnless('exportDxf')}`) +
|
||||
big('BIM (IFC)', 'boxes', `data-app="exportIfc" title="Bouwconstructie als IFC4 (balken, platen, elektra) per element, zonder afwerking en inrichting" ${disabledUnless('exportIfc')}`) +
|
||||
big('Render (OBJ)', 'badge-3d', `data-app="exportObj" title="Volledige afgewerkte woning als OBJ + MTL + texturen in een ZIP, voor renders in Blender" ${disabledUnless('exportObj')}`) },
|
||||
{ label: 'Snel nieuw', html:
|
||||
Object.entries(HouseModel.MODELS).map(([key, m]) => small(m.label, m.icon, `data-app="new-kind" data-kind="woning" data-template="${key}"`)).join('') +
|
||||
small('Woning', 'house', 'data-app="new-kind" data-kind="woning" data-template="single"') +
|
||||
small('Woning L-vorm', 'bounding-box-circles', 'data-app="new-kind" data-kind="woning" data-template="L"') +
|
||||
small('Los: muur', 'bricks', 'data-app="new-kind" data-kind="muur"') +
|
||||
small('Los: vloer', 'grid-3x3', 'data-app="new-kind" data-kind="vloer"') },
|
||||
{ label: 'Project', html: big('Eigenschappen', 'card-text', `data-app="openProjectProperties" title="Projectgegevens voor de PDF" ${disabledUnless('openProjectProperties')}`) }
|
||||
] },
|
||||
{ key: 'beeld', label: 'Beeld', order: 80, wire: wireClicks, groups: () => [
|
||||
{ label: 'Venster', html:
|
||||
small('Volledig scherm', 'fullscreen', 'data-app="fullscreen" title="F"') +
|
||||
small('3D groot', 'badge-3d', `data-app="toggleFullscreen3D" title="T: standaard → 3D groot → alleen render" ${disabledUnless('toggleFullscreen3D')}`) +
|
||||
small('2D groot', 'aspect-ratio', `data-app="toggleFullscreen2D" title="R" ${disabledUnless('toggleFullscreen2D')}`) },
|
||||
{ label: 'Camera', html:
|
||||
big('Lopen', 'person-walking', `data-app="toggleWalk" title="Shift+T. W/S of pijltjes: vooruit/achteruit · A/D: opzij · pijl links/rechts: draaien · Shift: rennen · slepen: rondkijken" ${disabledUnless('toggleWalk')}`) +
|
||||
small('Bediening', 'mouse', 'title="Rondom: slepen = draaien · rechts slepen = verschuiven · scrollwiel = zoomen. Lopen: Shift+T."') }
|
||||
] },
|
||||
{ key: 'extra', label: 'Extra', order: 90, wire: wireClicks, groups: () => [
|
||||
{ label: 'Bewerken', html: small('Horizontaal spiegelen', 'symmetry-vertical', `data-app="mirrorHorizontal" title="H" ${disabledUnless('mirrorHorizontal')}`) },
|
||||
{ label: 'Instellingen', html: big('Kosten & tijden', 'calculator', 'data-app="costs"') },
|
||||
{ label: 'Info', html: big('Over Tectum', 'info-circle', 'data-app="about"') }
|
||||
] }
|
||||
];
|
||||
function togglePanels() {
|
||||
document.body.classList.toggle('panels-hidden');
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}
|
||||
|
||||
function renderTabBar() {
|
||||
const bar = document.getElementById('tab-bar');
|
||||
const bar = document.getElementById('doc-tabs');
|
||||
bar.innerHTML = tabs.map(tab => `
|
||||
<div class="tab-item ${tab.id === activeTabId ? 'active' : ''}" data-tab-id="${tab.id}">
|
||||
<span class="tab-name">${tab.name}</span>
|
||||
<span class="tab-close" data-close-id="${tab.id}">×</span>
|
||||
<div class="doc-tab ${tab.id === activeTabId ? 'active' : ''}" data-tab-id="${tab.id}" role="tab" aria-selected="${tab.id === activeTabId}" title="${UI.esc(tab.name)}">
|
||||
<span class="doc-name">${UI.esc(tab.name)}</span>
|
||||
<button type="button" class="doc-close" data-close-id="${tab.id}" title="Sluiten" aria-label="Sluiten">×</button>
|
||||
</div>
|
||||
`).join('');
|
||||
document.getElementById('top-save').disabled = !editorHas('exportDesign');
|
||||
}
|
||||
|
||||
function onTabSaved(tab, filenameWithExt) {
|
||||
@@ -106,7 +117,6 @@ function onTabSaved(tab, filenameWithExt) {
|
||||
function mountEditorForTab(tab) {
|
||||
const ws = document.getElementById('workspace');
|
||||
ws.innerHTML = '';
|
||||
Ribbon.setSource('editor', []);
|
||||
currentEditor = editorModuleFor(tab.kind).mount(ws, { onSaved: (filename) => onTabSaved(tab, filename), template: tab.template, cameraKey: 'tab' + tab.id });
|
||||
if (tab.stateJson && typeof currentEditor.loadStateJson === 'function') {
|
||||
currentEditor.loadStateJson(tab.stateJson);
|
||||
@@ -124,7 +134,6 @@ function switchToTab(tabId) {
|
||||
|
||||
activeTabId = tabId;
|
||||
mountEditorForTab(tabs.find(t => t.id === tabId));
|
||||
updateEditMenuState();
|
||||
renderTabBar();
|
||||
}
|
||||
|
||||
@@ -142,7 +151,6 @@ function openNew(kind, template) {
|
||||
tabs.push(tab);
|
||||
activeTabId = tab.id;
|
||||
mountEditorForTab(tab);
|
||||
updateEditMenuState();
|
||||
renderTabBar();
|
||||
}
|
||||
|
||||
@@ -183,17 +191,15 @@ function closeTab(tabId) {
|
||||
switchToTab(tabs[tabs.length - 1].id);
|
||||
} else {
|
||||
setWorkspaceEmpty();
|
||||
Ribbon.setSource('editor', []);
|
||||
updateEditMenuState();
|
||||
renderTabBar();
|
||||
}
|
||||
}
|
||||
|
||||
function wireTabBar() {
|
||||
document.getElementById('tab-bar').addEventListener('click', (e) => {
|
||||
const closeBtn = e.target.closest('.tab-close');
|
||||
document.getElementById('doc-tabs').addEventListener('click', (e) => {
|
||||
const closeBtn = e.target.closest('.doc-close');
|
||||
if (closeBtn) { closeTab(parseInt(closeBtn.dataset.closeId, 10)); return; }
|
||||
const item = e.target.closest('.tab-item');
|
||||
const item = e.target.closest('.doc-tab');
|
||||
if (item) switchToTab(parseInt(item.dataset.tabId, 10));
|
||||
});
|
||||
}
|
||||
@@ -237,41 +243,42 @@ function toggleBrowserFullscreen() {
|
||||
else document.documentElement.requestFullscreen();
|
||||
}
|
||||
|
||||
function closeOverlays() {
|
||||
const open = document.querySelector('.dlg-overlay.open, #about-overlay.open, #new-overlay.open');
|
||||
if (open) { open.classList.remove('open'); return true; }
|
||||
return Menubar.close() || UI.closePopovers();
|
||||
}
|
||||
|
||||
function wireHotkeys() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const inField = ['INPUT', 'SELECT', 'TEXTAREA'].includes(document.activeElement.tagName);
|
||||
|
||||
if (e.ctrlKey && e.key.toLowerCase() === 's') {
|
||||
e.preventDefault();
|
||||
if (currentEditor && typeof currentEditor.exportDesign === 'function') currentEditor.exportDesign();
|
||||
const k = e.key.toLowerCase();
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
const act = { s: () => editorCall('exportDesign'), n: openNewModal, o: () => editorCall('triggerImport'), '\\': togglePanels }[k];
|
||||
if (act && !e.altKey && !e.shiftKey) { e.preventDefault(); act(); }
|
||||
return;
|
||||
}
|
||||
if (e.ctrlKey && e.key.toLowerCase() === 'n') {
|
||||
e.preventDefault();
|
||||
openNewModal();
|
||||
return;
|
||||
}
|
||||
if (inField || e.ctrlKey || e.metaKey || e.altKey) return;
|
||||
|
||||
if (e.key.toLowerCase() === 'f') {
|
||||
toggleBrowserFullscreen();
|
||||
} else if (e.key.toLowerCase() === 'h') {
|
||||
if (currentEditor && typeof currentEditor.mirrorHorizontal === 'function') currentEditor.mirrorHorizontal();
|
||||
} else if (e.key.toLowerCase() === 't' && e.shiftKey) {
|
||||
if (currentEditor && typeof currentEditor.toggleWalk === 'function') currentEditor.toggleWalk();
|
||||
} else if (e.key.toLowerCase() === 't') {
|
||||
if (currentEditor && typeof currentEditor.toggleFullscreen3D === 'function') currentEditor.toggleFullscreen3D();
|
||||
} else if (e.key.toLowerCase() === 'r') {
|
||||
if (currentEditor && typeof currentEditor.toggleFullscreen2D === 'function') currentEditor.toggleFullscreen2D();
|
||||
}
|
||||
});
|
||||
if (e.key === 'Escape' && closeOverlays()) { e.stopImmediatePropagation(); return; }
|
||||
if (inField || e.altKey || viewState().walk && !['l', 'escape'].includes(k)) return;
|
||||
const view = UI.VIEWS.find(v => v.key1 === e.key);
|
||||
if (view) editorCall('setView', view.key);
|
||||
else if (e.key === '4') editorCall('toggleReport');
|
||||
else if (k === 'x') editorCall('toggleSection');
|
||||
else if (k === 'f') editorCall('fitView');
|
||||
else if (k === 'l') editorCall('toggleWalk');
|
||||
else if (k === 'h' && e.shiftKey) editorCall('mirrorHorizontal');
|
||||
else if (e.key === '?') openKeys();
|
||||
}, true);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.app-version-slot').forEach(el => { el.textContent = 'v' + APP_VERSION; });
|
||||
document.querySelectorAll('.app-version-number').forEach(el => { el.textContent = APP_VERSION; });
|
||||
Ribbon.init(document.getElementById('ribbon'));
|
||||
Ribbon.setSource('app', appRibbonTabs());
|
||||
Menubar.init(document.getElementById('menubar'), MENUS);
|
||||
document.getElementById('top-save').addEventListener('click', () => editorCall('exportDesign'));
|
||||
document.getElementById('keys-overlay').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'keys-overlay' || e.target.closest('[data-close]')) document.getElementById('keys-overlay').classList.remove('open');
|
||||
});
|
||||
wireAbout();
|
||||
wireNewModal();
|
||||
wireHotkeys();
|
||||
|
||||
Reference in New Issue
Block a user