Tectum v0.4.0 — mijlpaal gevelopbouw
Configurator voor Van Emous Mantelzorgwoningen: wanden, vloeren, units, woningen (L-vorm), plat dak met dakrand, buitenverlichting, latwerk op vochtdoek, trasraam, sandwichpanelen en PDF-tekenbladen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
/*!
|
||||
* Tectum
|
||||
* Copyright © 2026 Machiel Heinen.
|
||||
* Alle rechten voorbehouden. Gebruik uitsluitend met schriftelijke toestemming.
|
||||
* Zie LICENTIE.md.
|
||||
*/
|
||||
const APP_VERSION = '0.4.0';
|
||||
// 0.0.X per kleine iteratie, 0.X.0 per mijlpaal, X.0.0 bij live-gang.
|
||||
|
||||
let tabs = [];
|
||||
let activeTabId = null;
|
||||
let tabIdCounter = 1;
|
||||
let kindCounters = {};
|
||||
let currentEditor = null;
|
||||
|
||||
function editorModuleFor(kind) {
|
||||
if (kind === 'muur') return window.WallEditor;
|
||||
if (kind === 'vloer') return window.FloorEditor;
|
||||
if (kind === 'woning') return window.HouseEditor;
|
||||
return null;
|
||||
}
|
||||
|
||||
function defaultNameFor(kind) {
|
||||
kindCounters[kind] = (kindCounters[kind] || 0) + 1;
|
||||
return `${kind} ${String(kindCounters[kind]).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
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>`;
|
||||
}
|
||||
|
||||
function updateEditMenuState() {
|
||||
const has = (fn) => !!(currentEditor && typeof currentEditor[fn] === 'function');
|
||||
document.getElementById('menu-opslaan-als').classList.toggle('disabled', !has('exportDesign'));
|
||||
document.getElementById('menu-importeren').classList.toggle('disabled', !has('triggerImport'));
|
||||
document.getElementById('menu-mirror-h').classList.toggle('disabled', !has('mirrorHorizontal'));
|
||||
document.getElementById('menu-3d-fullscreen').classList.toggle('disabled', !has('toggleFullscreen3D'));
|
||||
document.getElementById('menu-2d-fullscreen').classList.toggle('disabled', !has('toggleFullscreen2D'));
|
||||
document.getElementById('menu-project-props').classList.toggle('disabled', !has('openProjectProperties'));
|
||||
}
|
||||
|
||||
function renderTabBar() {
|
||||
const bar = document.getElementById('tab-bar');
|
||||
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>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function onTabSaved(tab, filenameWithExt) {
|
||||
const base = filenameWithExt.replace(/\.json$/i, '');
|
||||
tab.name = base;
|
||||
tab.savedName = base;
|
||||
renderTabBar();
|
||||
}
|
||||
|
||||
function mountEditorForTab(tab) {
|
||||
const ws = document.getElementById('workspace');
|
||||
ws.innerHTML = '';
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function switchToTab(tabId) {
|
||||
if (activeTabId === tabId) return;
|
||||
const oldTab = tabs.find(t => t.id === activeTabId);
|
||||
if (oldTab && currentEditor && typeof currentEditor.getStateJson === 'function') {
|
||||
oldTab.stateJson = currentEditor.getStateJson();
|
||||
}
|
||||
if (currentEditor && typeof currentEditor.unmount === 'function') currentEditor.unmount();
|
||||
currentEditor = null;
|
||||
|
||||
activeTabId = tabId;
|
||||
mountEditorForTab(tabs.find(t => t.id === tabId));
|
||||
updateEditMenuState();
|
||||
renderTabBar();
|
||||
}
|
||||
|
||||
function openNew(kind, template) {
|
||||
if (!editorModuleFor(kind)) { closeAllMenus(); return; } // 'dak' nog niet beschikbaar
|
||||
|
||||
const oldTab = tabs.find(t => t.id === activeTabId);
|
||||
if (oldTab && currentEditor && typeof currentEditor.getStateJson === 'function') {
|
||||
oldTab.stateJson = currentEditor.getStateJson();
|
||||
}
|
||||
if (currentEditor && typeof currentEditor.unmount === 'function') currentEditor.unmount();
|
||||
currentEditor = null;
|
||||
|
||||
const tab = { id: tabIdCounter++, kind, template: template || null, name: defaultNameFor(kind), savedName: null, stateJson: null };
|
||||
tabs.push(tab);
|
||||
activeTabId = tab.id;
|
||||
mountEditorForTab(tab);
|
||||
updateEditMenuState();
|
||||
renderTabBar();
|
||||
closeAllMenus();
|
||||
}
|
||||
|
||||
function isTabDirty(tab) {
|
||||
const module = editorModuleFor(tab.kind);
|
||||
if (!module || typeof module.getDefaultState !== 'function') return false;
|
||||
const state = (tab.id === activeTabId && currentEditor && typeof currentEditor.getStateJson === 'function')
|
||||
? currentEditor.getStateJson()
|
||||
: tab.stateJson;
|
||||
if (!state) return false;
|
||||
return JSON.stringify(state) !== JSON.stringify(module.getDefaultState(tab.template));
|
||||
}
|
||||
|
||||
function closeTab(tabId) {
|
||||
const tab = tabs.find(t => t.id === tabId);
|
||||
if (!tab) return;
|
||||
|
||||
if (tab.id !== activeTabId) {
|
||||
if (!isTabDirty(tab)) {
|
||||
tabs = tabs.filter(t => t.id !== tabId);
|
||||
renderTabBar();
|
||||
return;
|
||||
}
|
||||
switchToTab(tabId);
|
||||
}
|
||||
|
||||
if (isTabDirty(tab) && currentEditor && typeof currentEditor.exportDesign === 'function') {
|
||||
const saved = currentEditor.exportDesign();
|
||||
if (!saved) return;
|
||||
}
|
||||
|
||||
tabs = tabs.filter(t => t.id !== tabId);
|
||||
if (currentEditor && typeof currentEditor.unmount === 'function') currentEditor.unmount();
|
||||
currentEditor = null;
|
||||
activeTabId = null;
|
||||
|
||||
if (tabs.length > 0) {
|
||||
switchToTab(tabs[tabs.length - 1].id);
|
||||
} else {
|
||||
setWorkspaceEmpty();
|
||||
updateEditMenuState();
|
||||
renderTabBar();
|
||||
}
|
||||
}
|
||||
|
||||
function wireTabBar() {
|
||||
document.getElementById('tab-bar').addEventListener('click', (e) => {
|
||||
const closeBtn = e.target.closest('.tab-close');
|
||||
if (closeBtn) { closeTab(parseInt(closeBtn.dataset.closeId, 10)); return; }
|
||||
const item = e.target.closest('.tab-item');
|
||||
if (item) switchToTab(parseInt(item.dataset.tabId, 10));
|
||||
});
|
||||
}
|
||||
|
||||
function closeAllMenus() {
|
||||
document.querySelectorAll('.menu-dropdown, .submenu').forEach(el => el.classList.remove('open'));
|
||||
document.querySelectorAll('.menu-btn').forEach(el => el.classList.remove('open'));
|
||||
}
|
||||
|
||||
function wireTopLevelMenu(btnId, dropdownId) {
|
||||
const btn = document.getElementById(btnId);
|
||||
const dropdown = document.getElementById(dropdownId);
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const willOpen = !dropdown.classList.contains('open');
|
||||
closeAllMenus();
|
||||
if (willOpen) { dropdown.classList.add('open'); btn.classList.add('open'); }
|
||||
});
|
||||
}
|
||||
|
||||
function wireMenu() {
|
||||
wireTopLevelMenu('menu-bestand-btn', 'menu-bestand-dropdown');
|
||||
wireTopLevelMenu('menu-bewerken-btn', 'menu-bewerken-dropdown');
|
||||
wireTopLevelMenu('menu-beeld-btn', 'menu-beeld-dropdown');
|
||||
|
||||
document.getElementById('menu-nieuw-item').addEventListener('click', (e) => e.stopPropagation());
|
||||
document.querySelectorAll('.menu-nieuw-option').forEach(opt => {
|
||||
opt.addEventListener('click', () => openNew(opt.dataset.kind, opt.dataset.template));
|
||||
});
|
||||
|
||||
document.getElementById('menu-opslaan-als').addEventListener('click', () => {
|
||||
if (currentEditor && typeof currentEditor.exportDesign === 'function') currentEditor.exportDesign();
|
||||
closeAllMenus();
|
||||
});
|
||||
document.getElementById('menu-importeren').addEventListener('click', () => {
|
||||
if (currentEditor && typeof currentEditor.triggerImport === 'function') currentEditor.triggerImport();
|
||||
closeAllMenus();
|
||||
});
|
||||
document.getElementById('menu-mirror-h').addEventListener('click', () => {
|
||||
if (currentEditor && typeof currentEditor.mirrorHorizontal === 'function') currentEditor.mirrorHorizontal();
|
||||
closeAllMenus();
|
||||
});
|
||||
document.getElementById('menu-fullscreen').addEventListener('click', () => {
|
||||
toggleBrowserFullscreen();
|
||||
closeAllMenus();
|
||||
});
|
||||
document.getElementById('menu-3d-fullscreen').addEventListener('click', () => {
|
||||
if (currentEditor && typeof currentEditor.toggleFullscreen3D === 'function') currentEditor.toggleFullscreen3D();
|
||||
closeAllMenus();
|
||||
});
|
||||
document.getElementById('menu-2d-fullscreen').addEventListener('click', () => {
|
||||
if (currentEditor && typeof currentEditor.toggleFullscreen2D === 'function') currentEditor.toggleFullscreen2D();
|
||||
closeAllMenus();
|
||||
});
|
||||
document.getElementById('menu-project-props').addEventListener('click', () => {
|
||||
if (currentEditor && typeof currentEditor.openProjectProperties === 'function') currentEditor.openProjectProperties();
|
||||
closeAllMenus();
|
||||
});
|
||||
|
||||
document.addEventListener('click', closeAllMenus);
|
||||
}
|
||||
|
||||
// Minimale markdown-weergave voor de licentietekst (koppen + alinea's).
|
||||
function renderLicenseHtml(md) {
|
||||
const esc = (t) => t.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return md.trim().split(/\n\s*\n/).map(block => {
|
||||
const b = block.trim();
|
||||
if (b.startsWith('## ')) return `<h4>${esc(b.slice(3))}</h4>`;
|
||||
if (b.startsWith('# ')) return `<h3>${esc(b.slice(2))}</h3>`;
|
||||
return `<p>${esc(b.replace(/\s*\n\s*/g, ' '))}</p>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function wireAbout() {
|
||||
const overlay = document.getElementById('about-overlay');
|
||||
document.getElementById('about-license').innerHTML = renderLicenseHtml(window.TECTUM_LICENSE_MD || '');
|
||||
document.getElementById('btn-about').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
overlay.classList.add('open');
|
||||
});
|
||||
document.getElementById('about-close').addEventListener('click', () => overlay.classList.remove('open'));
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.classList.remove('open'); });
|
||||
}
|
||||
|
||||
function wireNewModal() {
|
||||
const overlay = document.getElementById('new-overlay');
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.classList.remove('open'); });
|
||||
document.querySelectorAll('.new-option:not(.disabled)').forEach(opt => {
|
||||
opt.addEventListener('click', () => {
|
||||
openNew(opt.dataset.kind, opt.dataset.template);
|
||||
overlay.classList.remove('open');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openNewModal() {
|
||||
closeAllMenus();
|
||||
document.getElementById('new-overlay').classList.add('open');
|
||||
}
|
||||
|
||||
function toggleBrowserFullscreen() {
|
||||
if (document.fullscreenElement) document.exitFullscreen();
|
||||
else document.documentElement.requestFullscreen();
|
||||
}
|
||||
|
||||
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();
|
||||
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') {
|
||||
if (currentEditor && typeof currentEditor.toggleFullscreen3D === 'function') currentEditor.toggleFullscreen3D();
|
||||
} else if (e.key.toLowerCase() === 'r') {
|
||||
if (currentEditor && typeof currentEditor.toggleFullscreen2D === 'function') currentEditor.toggleFullscreen2D();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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; });
|
||||
wireMenu();
|
||||
wireAbout();
|
||||
wireNewModal();
|
||||
wireHotkeys();
|
||||
wireTabBar();
|
||||
openNew('woning', 'L');
|
||||
|
||||
setTimeout(() => {
|
||||
const splash = document.getElementById('splash-screen');
|
||||
if (splash) splash.remove();
|
||||
}, 4300);
|
||||
});
|
||||
Reference in New Issue
Block a user