//// DELAY CHECKER ////
(function () {
if (window.__scriptProfilerAuto) return;
const AUTO_STOP_MS = 2200; // tijd zonder nieuwe activiteit = meting klaar
const MAX_SESSION_MS = 8000; // harde limiet
const TOP_N = 15;
const state = {
activeSession: null,
sessions: [],
byScript: new Map(),
callStack: [],
originals: {},
observerMeta: new WeakMap(),
timerMeta: new Map(),
listenerMap: new WeakMap(),
pendingAutoStop: null,
pendingHardStop: null,
monitorInstalled: false
};
function now() {
return performance.now();
}
function extractScriptNameFromText(text) {
if (!text) return "ONBEKEND SCRIPT";
const m = text.match(/\/\/\/\/\s*([^\n\r]+?)\s*\/\/\/\//);
return m ? m[1].trim() : "ONBEKEND SCRIPT";
}
function currentScriptName() {
const cs = document.currentScript;
if (!cs) return state.callStack[state.callStack.length - 1] || "ONBEKEND SCRIPT";
return extractScriptNameFromText(cs.textContent || "");
}
function ensureBucket(scriptName) {
if (!state.byScript.has(scriptName)) {
state.byScript.set(scriptName, {
script: scriptName,
totalMs: 0,
events: 0,
observers: 0,
intervals: 0,
timeouts: 0,
rafs: 0,
querySelectorAll: 0,
querySelector: 0,
setAttribute: 0,
classListOps: 0,
styleOps: 0,
appendOps: 0,
removeOps: 0
});
}
return state.byScript.get(scriptName);
}
function markActivity() {
if (!state.activeSession) return;
if (state.pendingAutoStop) clearTimeout(state.pendingAutoStop);
state.pendingAutoStop = setTimeout(() => {
stopSession("idle");
}, AUTO_STOP_MS);
}
function addTime(scriptName, ms, kind) {
if (!state.activeSession) return;
const bucket = ensureBucket(scriptName);
bucket.totalMs += ms;
if (kind === "event") bucket.events += 1;
if (kind === "observer") bucket.observers += 1;
if (kind === "interval") bucket.intervals += 1;
if (kind === "timeout") bucket.timeouts += 1;
if (kind === "raf") bucket.rafs += 1;
markActivity();
}
function withScriptContext(scriptName, kind, fn, args, thisArg) {
const start = now();
state.callStack.push(scriptName);
try {
return fn.apply(thisArg, args || []);
} finally {
state.callStack.pop();
addTime(scriptName, now() - start, kind);
}
}
function wrapListener(listener, scriptName, eventType) {
if (!listener) return listener;
if (typeof listener === "function") {
const wrapped = function () {
return withScriptContext(scriptName, "event", listener, arguments, this);
};
state.listenerMap.set(listener, wrapped);
return wrapped;
}
if (typeof listener.handleEvent === "function") {
const wrappedObj = {
handleEvent: function () {
return withScriptContext(scriptName, "event", listener.handleEvent, arguments, listener);
}
};
state.listenerMap.set(listener, wrappedObj);
return wrappedObj;
}
return listener;
}
function patchAddEventListener() {
const proto = EventTarget.prototype;
state.originals.addEventListener = proto.addEventListener;
state.originals.removeEventListener = proto.removeEventListener;
proto.addEventListener = function (type, listener, options) {
const scriptName = currentScriptName();
const wrapped = wrapListener(listener, scriptName, type);
return state.originals.addEventListener.call(this, type, wrapped, options);
};
proto.removeEventListener = function (type, listener, options) {
const wrapped = state.listenerMap.get(listener) || listener;
return state.originals.removeEventListener.call(this, type, wrapped, options);
};
}
function patchTimers() {
state.originals.setTimeout = window.setTimeout;
state.originals.setInterval = window.setInterval;
state.originals.requestAnimationFrame = window.requestAnimationFrame;
window.setTimeout = function (fn, delay) {
const scriptName = currentScriptName();
const wrapped = typeof fn === "function"
? function () { return withScriptContext(scriptName, "timeout", fn, arguments, this); }
: fn;
return state.originals.setTimeout.call(window, wrapped, delay);
};
window.setInterval = function (fn, delay) {
const scriptName = currentScriptName();
const wrapped = typeof fn === "function"
? function () { return withScriptContext(scriptName, "interval", fn, arguments, this); }
: fn;
return state.originals.setInterval.call(window, wrapped, delay);
};
window.requestAnimationFrame = function (fn) {
const scriptName = currentScriptName();
const wrapped = function () {
return withScriptContext(scriptName, "raf", fn, arguments, this);
};
return state.originals.requestAnimationFrame.call(window, wrapped);
};
}
function patchMutationObserver() {
state.originals.MutationObserver = window.MutationObserver;
window.MutationObserver = function (callback) {
const scriptName = currentScriptName();
const wrapped = function () {
return withScriptContext(scriptName, "observer", callback, arguments, this);
};
return new state.originals.MutationObserver(wrapped);
};
}
function patchDomHeavyCalls() {
const qsaE = Element.prototype.querySelectorAll;
const qsE = Element.prototype.querySelector;
const qsaD = Document.prototype.querySelectorAll;
const qsD = Document.prototype.querySelector;
const setAttr = Element.prototype.setAttribute;
const appendChild = Node.prototype.appendChild;
const insertBefore = Node.prototype.insertBefore;
const removeChild = Node.prototype.removeChild;
state.originals.elementQuerySelectorAll = qsaE;
state.originals.elementQuerySelector = qsE;
state.originals.documentQuerySelectorAll = qsaD;
state.originals.documentQuerySelector = qsD;
state.originals.setAttribute = setAttr;
state.originals.appendChild = appendChild;
state.originals.insertBefore = insertBefore;
state.originals.removeChild = removeChild;
function mark(op) {
if (!state.activeSession) return;
const scriptName = state.callStack[state.callStack.length - 1];
if (!scriptName) return;
const bucket = ensureBucket(scriptName);
bucket[op] = (bucket[op] || 0) + 1;
markActivity();
}
Element.prototype.querySelectorAll = function (sel) {
mark("querySelectorAll");
return qsaE.call(this, sel);
};
Document.prototype.querySelectorAll = function (sel) {
mark("querySelectorAll");
return qsaD.call(this, sel);
};
Element.prototype.querySelector = function (sel) {
mark("querySelector");
return qsE.call(this, sel);
};
Document.prototype.querySelector = function (sel) {
mark("querySelector");
return qsD.call(this, sel);
};
Element.prototype.setAttribute = function (name, value) {
if (
name === "display" ||
name === "transform" ||
name === "d" ||
name === "points" ||
name === "x" ||
name === "width" ||
name === "height"
) {
mark("setAttribute");
}
return setAttr.call(this, name, value);
};
Node.prototype.appendChild = function (node) {
mark("appendOps");
return appendChild.call(this, node);
};
Node.prototype.insertBefore = function (node, ref) {
mark("appendOps");
return insertBefore.call(this, node, ref);
};
Node.prototype.removeChild = function (node) {
mark("removeOps");
return removeChild.call(this, node);
};
const classAdd = DOMTokenList.prototype.add;
const classRemove = DOMTokenList.prototype.remove;
const classToggle = DOMTokenList.prototype.toggle;
state.originals.classAdd = classAdd;
state.originals.classRemove = classRemove;
state.originals.classToggle = classToggle;
DOMTokenList.prototype.add = function () {
if (state.activeSession) {
const scriptName = state.callStack[state.callStack.length - 1];
if (scriptName) ensureBucket(scriptName).classListOps += 1;
markActivity();
}
return classAdd.apply(this, arguments);
};
DOMTokenList.prototype.remove = function () {
if (state.activeSession) {
const scriptName = state.callStack[state.callStack.length - 1];
if (scriptName) ensureBucket(scriptName).classListOps += 1;
markActivity();
}
return classRemove.apply(this, arguments);
};
DOMTokenList.prototype.toggle = function () {
if (state.activeSession) {
const scriptName = state.callStack[state.callStack.length - 1];
if (scriptName) ensureBucket(scriptName).classListOps += 1;
markActivity();
}
return classToggle.apply(this, arguments);
};
const styleSetProperty = CSSStyleDeclaration.prototype.setProperty;
state.originals.styleSetProperty = styleSetProperty;
CSSStyleDeclaration.prototype.setProperty = function () {
if (state.activeSession) {
const scriptName = state.callStack[state.callStack.length - 1];
if (scriptName) ensureBucket(scriptName).styleOps += 1;
markActivity();
}
return styleSetProperty.apply(this, arguments);
};
}
function startSession(triggerLabel) {
if (state.activeSession) stopSession("restart");
state.byScript.clear();
state.activeSession = {
label: triggerLabel,
startedAt: now()
};
if (state.pendingAutoStop) clearTimeout(state.pendingAutoStop);
if (state.pendingHardStop) clearTimeout(state.pendingHardStop);
state.pendingAutoStop = setTimeout(() => {
stopSession("idle");
}, AUTO_STOP_MS);
state.pendingHardStop = setTimeout(() => {
stopSession("max");
}, MAX_SESSION_MS);
console.log(`[AutoProfiler] gestart door: ${triggerLabel}`);
}
function stopSession(reason) {
if (!state.activeSession) return [];
if (state.pendingAutoStop) clearTimeout(state.pendingAutoStop);
if (state.pendingHardStop) clearTimeout(state.pendingHardStop);
const elapsed = now() - state.activeSession.startedAt;
const rows = Array.from(state.byScript.values())
.sort((a, b) => b.totalMs - a.totalMs)
.map(r => ({
script: r.script,
totalMs: Number(r.totalMs.toFixed(1)),
events: r.events,
observers: r.observers,
intervals: r.intervals,
timeouts: r.timeouts,
rafs: r.rafs,
querySelectorAll: r.querySelectorAll,
querySelector: r.querySelector,
setAttribute: r.setAttribute,
classListOps: r.classListOps,
styleOps: r.styleOps,
appendOps: r.appendOps,
removeOps: r.removeOps
}));
const topRows = rows.slice(0, TOP_N);
console.groupCollapsed(
`%c[AutoProfiler] ${state.activeSession.label} klaar`,
"color:#0a7;font-weight:bold;"
);
console.log(`reden: ${reason}`);
console.log(`duur: ${elapsed.toFixed(1)} ms`);
console.table(topRows);
console.log("volledige lijst:", rows);
console.groupEnd();
state.sessions.push({
label: state.activeSession.label,
reason,
elapsedMs: elapsed,
rows
});
state.activeSession = null;
return rows;
}
function isTargetField(el) {
if (!el || !el.name) return false;
return (
el.name === "form_fields[dorpeltype]" ||
el.name === "form_fields[vakken]"
);
}
function installAutoMonitor() {
if (state.monitorInstalled) return;
state.monitorInstalled = true;
document.addEventListener("change", function (e) {
const t = e.target;
if (!isTargetField(t)) return;
const value =
t.getAttribute("data-original-value") ||
t.value ||
"(leeg)";
startSession(`${t.name} = ${value}`);
}, true);
document.addEventListener("input", function (e) {
const t = e.target;
if (!isTargetField(t)) return;
const value =
t.getAttribute("data-original-value") ||
t.value ||
"(leeg)";
startSession(`${t.name} = ${value}`);
}, true);
}
patchAddEventListener();
patchTimers();
patchMutationObserver();
patchDomHeavyCalls();
installAutoMonitor();
window.__scriptProfilerAuto = {
sessions: state.sessions,
stopNow() {
return stopSession("manual");
},
last() {
return state.sessions[state.sessions.length - 1] || null;
},
all() {
return state.sessions;
}
};
console.log("[AutoProfiler] actief. Wijzig dorpeltype of vakken; output verschijnt automatisch in de console.");
})();
//// DELAY CHECKER ////
(function () {
if (window.__scriptProfilerAuto) return;
const AUTO_STOP_MS = 2200; // tijd zonder nieuwe activiteit = meting klaar
const MAX_SESSION_MS = 8000; // harde limiet
const TOP_N = 15;
const state = {
activeSession: null,
sessions: [],
byScript: new Map(),
callStack: [],
originals: {},
observerMeta: new WeakMap(),
timerMeta: new Map(),
listenerMap: new WeakMap(),
pendingAutoStop: null,
pendingHardStop: null,
monitorInstalled: false
};
function now() {
return performance.now();
}
function extractScriptNameFromText(text) {
if (!text) return "ONBEKEND SCRIPT";
const m = text.match(/\/\/\/\/\s*([^\n\r]+?)\s*\/\/\/\//);
return m ? m[1].trim() : "ONBEKEND SCRIPT";
}
function currentScriptName() {
const cs = document.currentScript;
if (!cs) return state.callStack[state.callStack.length - 1] || "ONBEKEND SCRIPT";
return extractScriptNameFromText(cs.textContent || "");
}
function ensureBucket(scriptName) {
if (!state.byScript.has(scriptName)) {
state.byScript.set(scriptName, {
script: scriptName,
totalMs: 0,
events: 0,
observers: 0,
intervals: 0,
timeouts: 0,
rafs: 0,
querySelectorAll: 0,
querySelector: 0,
setAttribute: 0,
classListOps: 0,
styleOps: 0,
appendOps: 0,
removeOps: 0
});
}
return state.byScript.get(scriptName);
}
function markActivity() {
if (!state.activeSession) return;
if (state.pendingAutoStop) clearTimeout(state.pendingAutoStop);
state.pendingAutoStop = setTimeout(() => {
stopSession("idle");
}, AUTO_STOP_MS);
}
function addTime(scriptName, ms, kind) {
if (!state.activeSession) return;
const bucket = ensureBucket(scriptName);
bucket.totalMs += ms;
if (kind === "event") bucket.events += 1;
if (kind === "observer") bucket.observers += 1;
if (kind === "interval") bucket.intervals += 1;
if (kind === "timeout") bucket.timeouts += 1;
if (kind === "raf") bucket.rafs += 1;
markActivity();
}
function withScriptContext(scriptName, kind, fn, args, thisArg) {
const start = now();
state.callStack.push(scriptName);
try {
return fn.apply(thisArg, args || []);
} finally {
state.callStack.pop();
addTime(scriptName, now() - start, kind);
}
}
function wrapListener(listener, scriptName, eventType) {
if (!listener) return listener;
if (typeof listener === "function") {
const wrapped = function () {
return withScriptContext(scriptName, "event", listener, arguments, this);
};
state.listenerMap.set(listener, wrapped);
return wrapped;
}
if (typeof listener.handleEvent === "function") {
const wrappedObj = {
handleEvent: function () {
return withScriptContext(scriptName, "event", listener.handleEvent, arguments, listener);
}
};
state.listenerMap.set(listener, wrappedObj);
return wrappedObj;
}
return listener;
}
function patchAddEventListener() {
const proto = EventTarget.prototype;
state.originals.addEventListener = proto.addEventListener;
state.originals.removeEventListener = proto.removeEventListener;
proto.addEventListener = function (type, listener, options) {
const scriptName = currentScriptName();
const wrapped = wrapListener(listener, scriptName, type);
return state.originals.addEventListener.call(this, type, wrapped, options);
};
proto.removeEventListener = function (type, listener, options) {
const wrapped = state.listenerMap.get(listener) || listener;
return state.originals.removeEventListener.call(this, type, wrapped, options);
};
}
function patchTimers() {
state.originals.setTimeout = window.setTimeout;
state.originals.setInterval = window.setInterval;
state.originals.requestAnimationFrame = window.requestAnimationFrame;
window.setTimeout = function (fn, delay) {
const scriptName = currentScriptName();
const wrapped = typeof fn === "function"
? function () { return withScriptContext(scriptName, "timeout", fn, arguments, this); }
: fn;
return state.originals.setTimeout.call(window, wrapped, delay);
};
window.setInterval = function (fn, delay) {
const scriptName = currentScriptName();
const wrapped = typeof fn === "function"
? function () { return withScriptContext(scriptName, "interval", fn, arguments, this); }
: fn;
return state.originals.setInterval.call(window, wrapped, delay);
};
window.requestAnimationFrame = function (fn) {
const scriptName = currentScriptName();
const wrapped = function () {
return withScriptContext(scriptName, "raf", fn, arguments, this);
};
return state.originals.requestAnimationFrame.call(window, wrapped);
};
}
function patchMutationObserver() {
state.originals.MutationObserver = window.MutationObserver;
window.MutationObserver = function (callback) {
const scriptName = currentScriptName();
const wrapped = function () {
return withScriptContext(scriptName, "observer", callback, arguments, this);
};
return new state.originals.MutationObserver(wrapped);
};
}
function patchDomHeavyCalls() {
const qsaE = Element.prototype.querySelectorAll;
const qsE = Element.prototype.querySelector;
const qsaD = Document.prototype.querySelectorAll;
const qsD = Document.prototype.querySelector;
const setAttr = Element.prototype.setAttribute;
const appendChild = Node.prototype.appendChild;
const insertBefore = Node.prototype.insertBefore;
const removeChild = Node.prototype.removeChild;
state.originals.elementQuerySelectorAll = qsaE;
state.originals.elementQuerySelector = qsE;
state.originals.documentQuerySelectorAll = qsaD;
state.originals.documentQuerySelector = qsD;
state.originals.setAttribute = setAttr;
state.originals.appendChild = appendChild;
state.originals.insertBefore = insertBefore;
state.originals.removeChild = removeChild;
function mark(op) {
if (!state.activeSession) return;
const scriptName = state.callStack[state.callStack.length - 1];
if (!scriptName) return;
const bucket = ensureBucket(scriptName);
bucket[op] = (bucket[op] || 0) + 1;
markActivity();
}
Element.prototype.querySelectorAll = function (sel) {
mark("querySelectorAll");
return qsaE.call(this, sel);
};
Document.prototype.querySelectorAll = function (sel) {
mark("querySelectorAll");
return qsaD.call(this, sel);
};
Element.prototype.querySelector = function (sel) {
mark("querySelector");
return qsE.call(this, sel);
};
Document.prototype.querySelector = function (sel) {
mark("querySelector");
return qsD.call(this, sel);
};
Element.prototype.setAttribute = function (name, value) {
if (
name === "display" ||
name === "transform" ||
name === "d" ||
name === "points" ||
name === "x" ||
name === "width" ||
name === "height"
) {
mark("setAttribute");
}
return setAttr.call(this, name, value);
};
Node.prototype.appendChild = function (node) {
mark("appendOps");
return appendChild.call(this, node);
};
Node.prototype.insertBefore = function (node, ref) {
mark("appendOps");
return insertBefore.call(this, node, ref);
};
Node.prototype.removeChild = function (node) {
mark("removeOps");
return removeChild.call(this, node);
};
const classAdd = DOMTokenList.prototype.add;
const classRemove = DOMTokenList.prototype.remove;
const classToggle = DOMTokenList.prototype.toggle;
state.originals.classAdd = classAdd;
state.originals.classRemove = classRemove;
state.originals.classToggle = classToggle;
DOMTokenList.prototype.add = function () {
if (state.activeSession) {
const scriptName = state.callStack[state.callStack.length - 1];
if (scriptName) ensureBucket(scriptName).classListOps += 1;
markActivity();
}
return classAdd.apply(this, arguments);
};
DOMTokenList.prototype.remove = function () {
if (state.activeSession) {
const scriptName = state.callStack[state.callStack.length - 1];
if (scriptName) ensureBucket(scriptName).classListOps += 1;
markActivity();
}
return classRemove.apply(this, arguments);
};
DOMTokenList.prototype.toggle = function () {
if (state.activeSession) {
const scriptName = state.callStack[state.callStack.length - 1];
if (scriptName) ensureBucket(scriptName).classListOps += 1;
markActivity();
}
return classToggle.apply(this, arguments);
};
const styleSetProperty = CSSStyleDeclaration.prototype.setProperty;
state.originals.styleSetProperty = styleSetProperty;
CSSStyleDeclaration.prototype.setProperty = function () {
if (state.activeSession) {
const scriptName = state.callStack[state.callStack.length - 1];
if (scriptName) ensureBucket(scriptName).styleOps += 1;
markActivity();
}
return styleSetProperty.apply(this, arguments);
};
}
function startSession(triggerLabel) {
if (state.activeSession) stopSession("restart");
state.byScript.clear();
state.activeSession = {
label: triggerLabel,
startedAt: now()
};
if (state.pendingAutoStop) clearTimeout(state.pendingAutoStop);
if (state.pendingHardStop) clearTimeout(state.pendingHardStop);
state.pendingAutoStop = setTimeout(() => {
stopSession("idle");
}, AUTO_STOP_MS);
state.pendingHardStop = setTimeout(() => {
stopSession("max");
}, MAX_SESSION_MS);
console.log(`[AutoProfiler] gestart door: ${triggerLabel}`);
}
function stopSession(reason) {
if (!state.activeSession) return [];
if (state.pendingAutoStop) clearTimeout(state.pendingAutoStop);
if (state.pendingHardStop) clearTimeout(state.pendingHardStop);
const elapsed = now() - state.activeSession.startedAt;
const rows = Array.from(state.byScript.values())
.sort((a, b) => b.totalMs - a.totalMs)
.map(r => ({
script: r.script,
totalMs: Number(r.totalMs.toFixed(1)),
events: r.events,
observers: r.observers,
intervals: r.intervals,
timeouts: r.timeouts,
rafs: r.rafs,
querySelectorAll: r.querySelectorAll,
querySelector: r.querySelector,
setAttribute: r.setAttribute,
classListOps: r.classListOps,
styleOps: r.styleOps,
appendOps: r.appendOps,
removeOps: r.removeOps
}));
const topRows = rows.slice(0, TOP_N);
console.groupCollapsed(
`%c[AutoProfiler] ${state.activeSession.label} klaar`,
"color:#0a7;font-weight:bold;"
);
console.log(`reden: ${reason}`);
console.log(`duur: ${elapsed.toFixed(1)} ms`);
console.table(topRows);
console.log("volledige lijst:", rows);
console.groupEnd();
state.sessions.push({
label: state.activeSession.label,
reason,
elapsedMs: elapsed,
rows
});
state.activeSession = null;
return rows;
}
function isTargetField(el) {
if (!el || !el.name) return false;
return (
el.name === "form_fields[dorpeltype]" ||
el.name === "form_fields[vakken]"
);
}
function installAutoMonitor() {
if (state.monitorInstalled) return;
state.monitorInstalled = true;
document.addEventListener("change", function (e) {
const t = e.target;
if (!isTargetField(t)) return;
const value =
t.getAttribute("data-original-value") ||
t.value ||
"(leeg)";
startSession(`${t.name} = ${value}`);
}, true);
document.addEventListener("input", function (e) {
const t = e.target;
if (!isTargetField(t)) return;
const value =
t.getAttribute("data-original-value") ||
t.value ||
"(leeg)";
startSession(`${t.name} = ${value}`);
}, true);
}
patchAddEventListener();
patchTimers();
patchMutationObserver();
patchDomHeavyCalls();
installAutoMonitor();
window.__scriptProfilerAuto = {
sessions: state.sessions,
stopNow() {
return stopSession("manual");
},
last() {
return state.sessions[state.sessions.length - 1] || null;
},
all() {
return state.sessions;
}
};
console.log("[AutoProfiler] actief. Wijzig dorpeltype of vakken; output verschijnt automatisch in de console.");
})();
//// VAKVULLING BI #ALLE-VAKKEN - VASTE SHIFTBOX CAPTURE ////
(() => {
const SCALE = 3.84615384615;
const SVG_ID = "alle-vakken";
const VVI_TO_SUFFIX = {
"dubbelglas": "2glas",
"tripleglas": "3glas",
"paneel": "paneel",
"glaspaneel": "glaspaneel",
"deur": "deur",
"kaderdeur": "kaderdeur",
"dubbelkaderdeur": "dubbelkaderdeur",
"draaikiep": "draaikiep",
"vul in": null
};
const BI_SUFFIXES = [
"2glas",
"3glas",
"paneel",
"glaspaneel",
"deur",
"kaderdeur",
"dubbelkaderdeur",
"draaikiep"
];
const esc = s => CSS.escape(s);
/*
* Hier onthouden we EENMALIG welke delen
* bij welke shiftbox horen.
*
* De actuele positie van de shiftbox speelt
* daarna geen rol meer bij de selectie.
*/
let geometryCaptures = new WeakMap();
let gradientCaptures = new WeakMap();
let capturesReady = false;
/* ============================================================
* ALGEMEEN
* ============================================================ */
function svgRoot() {
return document.querySelector(
`svg#${esc(SVG_ID)}`
);
}
function setDisp(el, value) {
if (el) {
el.setAttribute(
"display",
value
);
}
}
function readNumById(id) {
const el =
document.getElementById(id);
if (!el) {
return null;
}
const value =
String(
el.value ?? ""
)
.trim()
.replace(",", ".");
const n =
Number(value);
return Number.isFinite(n)
? n
: 0;
}
function numField(
prefixA,
prefixB,
suffix
) {
const a =
readNumById(
`form-field-${prefixA}${suffix}`
);
if (a != null) {
return a;
}
const b =
readNumById(
`form-field-${prefixB}${suffix}`
);
if (b != null) {
return b;
}
return 0;
}
function vakCount() {
const checked =
document.querySelector(
'input[type="radio"][name="form_fields[vakken]"]:checked'
);
if (
checked &&
checked.value
) {
const m =
String(
checked.value
).match(/([1-5])/);
if (m) {
return Number(m[1]);
}
}
return 5;
}
function getVVI(n, i) {
const select =
document.querySelector(
`select[name="form_fields[${n}VVI${i}]"]`
);
return select
? String(select.value || "")
: "vul in";
}
/* ============================================================
* VISIBILITY
* ============================================================ */
function hideNeverShow(svg) {
svg
.querySelectorAll(
`[id^="vak-"][id*="-BI-shiftbox-"]`
)
.forEach(el => {
setDisp(
el,
"none"
);
});
}
function hideAllBIVariants(
svg,
i
) {
for (
const suffix
of BI_SUFFIXES
) {
setDisp(
svg.querySelector(
`#${esc(
`vak-${i}-BI-${suffix}`
)}`
),
"none"
);
}
}
function showGroupAndNestedGs(
group
) {
if (!group) {
return;
}
if (
String(
group.id
).includes("-shiftbox-")
) {
setDisp(
group,
"none"
);
return;
}
setDisp(
group,
"block"
);
group
.querySelectorAll("g")
.forEach(g => {
if (
String(
g.id
).includes("-shiftbox-")
) {
setDisp(
g,
"none"
);
} else {
setDisp(
g,
"block"
);
}
});
}
function updateVisibility() {
const svg =
svgRoot();
if (!svg) {
return;
}
hideNeverShow(svg);
const n =
vakCount();
for (
let i = 1;
i <= 5;
i++
) {
hideAllBIVariants(
svg,
i
);
}
for (
let i = 1;
i <= n;
i++
) {
const value =
getVVI(
n,
i
);
const suffix =
VVI_TO_SUFFIX[
value
];
if (!suffix) {
continue;
}
const group =
svg.querySelector(
`#${esc(
`vak-${i}-BI-${suffix}`
)}`
);
showGroupAndNestedGs(
group
);
}
hideNeverShow(svg);
}
/* ============================================================
* INPUT POSITIES
* ============================================================ */
function BI_LI(n) {
return numField(
`${n}`,
"5",
"LI"
);
}
function BI_TI(n, i) {
return numField(
`${n}`,
"5",
`TI${i}`
);
}
function BI_DMI_scaled(
n,
i
) {
return (
numField(
`${n}`,
"5",
`DMI${i}`
) /
SCALE
);
}
function Sponning() {
return (
readNumById(
"form-field-BI_sponningbreedte"
) ?? 0
);
}
function prefixLeft(
n,
i
) {
let x =
BI_LI(n);
if (i >= 2) {
x +=
BI_DMI_scaled(n, 1) +
BI_TI(n, 1);
}
if (i >= 3) {
x +=
BI_DMI_scaled(n, 2) +
BI_TI(n, 2);
}
if (i >= 4) {
x +=
BI_DMI_scaled(n, 3) +
BI_TI(n, 3);
}
if (i >= 5) {
x +=
BI_DMI_scaled(n, 4) +
BI_TI(n, 4);
}
return x;
}
/* ============================================================
* SHIFTBOX ENGINE
* ============================================================ */
function desiredXFromDelta(
rect,
exprNow
) {
const currentX =
Number(
rect.getAttribute("x") ??
"0"
);
if (
rect.dataset.baseX == null
) {
rect.dataset.baseX =
String(currentX);
}
if (
rect.dataset.baseExpr == null
) {
rect.dataset.baseExpr =
String(exprNow);
}
const baseX =
Number(
rect.dataset.baseX
);
const baseExpr =
Number(
rect.dataset.baseExpr
);
return (
baseX +
(
exprNow -
baseExpr
)
);
}
function boxFromShiftRect(
rect,
exprNow,
boxId
) {
const currentX =
Number(
rect.getAttribute("x") ??
"0"
);
const currentY =
Number(
rect.getAttribute("y") ??
"0"
);
const width =
Number(
rect.getAttribute("width") ??
"0"
);
const height =
Number(
rect.getAttribute("height") ??
"0"
);
const desiredX =
desiredXFromDelta(
rect,
exprNow
);
return {
id:
boxId,
rect,
desiredX,
dxInc:
desiredX -
currentX,
minX:
currentX,
maxX:
currentX +
width,
minY:
currentY,
maxY:
currentY +
height
};
}
function buildBoxes(
items
) {
const map =
new Map();
for (
const item
of items
) {
if (!item.gEl) {
continue;
}
const rect =
item.gEl.querySelector(
"rect"
);
if (!rect) {
continue;
}
const box =
boxFromShiftRect(
rect,
item.exprNow(),
item.boxId
);
if (
!map.has(
item.key
)
) {
map.set(
item.key,
[]
);
}
map
.get(
item.key
)
.push(box);
}
return map;
}
function moveBoxes(map) {
for (
const boxes
of map.values()
) {
for (
const box
of boxes
) {
box.rect.setAttribute(
"x",
box.desiredX
);
}
}
}
/* ============================================================
* EENMALIGE MEMBERSHIP CAPTURE
* ============================================================ */
function initialBoxesForVak(
svg,
vak
) {
const result = [];
const left =
svg.querySelector(
`#${esc(
`vak-${vak}-BI-shiftbox-L`
)} rect`
);
const right =
svg.querySelector(
`#${esc(
`vak-${vak}-BI-shiftbox-R`
)} rect`
);
function add(
rect,
id
) {
if (!rect) {
return;
}
const x =
Number(
rect.getAttribute("x") ??
0
);
const y =
Number(
rect.getAttribute("y") ??
0
);
const w =
Number(
rect.getAttribute("width") ??
0
);
const h =
Number(
rect.getAttribute("height") ??
0
);
result.push({
id,
minX: x,
maxX: x + w,
minY: y,
maxY: y + h
});
}
add(
left,
"L"
);
add(
right,
"R"
);
return result;
}
/*
* Alleen deze functie kijkt naar de geometrische positie.
*
* Dit gebeurt uitsluitend tijdens de capture.
*/
function captureMembership(
x,
y,
boxes
) {
const result = [];
for (
const box
of boxes
) {
if (
x >= box.minX &&
x <= box.maxX &&
y >= box.minY &&
y <= box.maxY
) {
result.push(
box.id
);
}
}
return result;
}
/*
* Vanaf hier wordt NIET meer gekeken waar een punt ligt.
*
* Alleen de eerder opgeslagen L/R membership wordt gebruikt.
*/
function dxFromMembership(
membership,
boxes
) {
let dx = 0;
if (
!membership ||
!membership.length
) {
return dx;
}
for (
const boxId
of membership
) {
const box =
boxes.find(
b =>
b.id === boxId
);
if (box) {
dx +=
box.dxInc;
}
}
return dx;
}
/* ============================================================
* POINTS CAPTURE
* ============================================================ */
function parsePoints(
value
) {
return String(
value || ""
)
.trim()
.replace(
/,/g,
" "
)
.split(
/\s+/
)
.filter(Boolean)
.map(Number);
}
function capturePointsElement(
element,
boxes
) {
const nums =
parsePoints(
element.getAttribute(
"points"
)
);
const points = [];
for (
let i = 0;
i < nums.length - 1;
i += 2
) {
points.push(
captureMembership(
nums[i],
nums[i + 1],
boxes
)
);
}
geometryCaptures.set(
element,
{
type: "points",
points
}
);
}
function shiftPointsCaptured(
element,
boxes
) {
const capture =
geometryCaptures.get(
element
);
if (
!capture ||
capture.type !== "points"
) {
return;
}
const nums =
parsePoints(
element.getAttribute(
"points"
)
);
const out = [];
let pointIndex = 0;
for (
let i = 0;
i < nums.length - 1;
i += 2
) {
const x =
nums[i];
const y =
nums[i + 1];
const membership =
capture.points[
pointIndex
] || [];
out.push(
String(
x +
dxFromMembership(
membership,
boxes
)
),
String(y)
);
pointIndex++;
}
element.setAttribute(
"points",
out.join(" ")
);
}
/* ============================================================
* RECT CAPTURE
* ============================================================ */
function captureRect(
rect,
boxes
) {
const x =
Number(
rect.getAttribute("x")
);
const y =
Number(
rect.getAttribute("y")
);
const width =
Number(
rect.getAttribute("width")
);
const height =
Number(
rect.getAttribute("height")
);
if (
!Number.isFinite(x) ||
!Number.isFinite(y) ||
!Number.isFinite(width)
) {
return;
}
const yMid =
Number.isFinite(height)
? y + height / 2
: y;
geometryCaptures.set(
rect,
{
type:
"rect",
left:
captureMembership(
x,
yMid,
boxes
),
right:
captureMembership(
x + width,
yMid,
boxes
)
}
);
}
function shiftRectCaptured(
rect,
boxes
) {
const capture =
geometryCaptures.get(
rect
);
if (
!capture ||
capture.type !== "rect"
) {
return;
}
const x =
Number(
rect.getAttribute("x")
);
const width =
Number(
rect.getAttribute("width")
);
if (
!Number.isFinite(x) ||
!Number.isFinite(width)
) {
return;
}
const oldRight =
x + width;
const newLeft =
x +
dxFromMembership(
capture.left,
boxes
);
const newRight =
oldRight +
dxFromMembership(
capture.right,
boxes
);
rect.setAttribute(
"x",
Math.min(
newLeft,
newRight
)
);
rect.setAttribute(
"width",
Math.abs(
newRight -
newLeft
)
);
}
/* ============================================================
* PATH
* ============================================================ */
function tokenizePath(d) {
return (
String(d || "")
.match(
/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/g
) || []
);
}
/*
* Een path wordt bij capture eenmalig doorlopen.
*
* Voor ieder X/Y-punt onthouden we alleen:
* [] / ["L"] / ["R"] / ["L","R"]
*/
function capturePath(
element,
boxes
) {
const d =
element.getAttribute(
"d"
);
const tokens =
tokenizePath(d);
if (!tokens.length) {
return;
}
let i = 0;
let cmd = "";
let x = 0;
let y = 0;
let x0 = 0;
let y0 = 0;
const memberships = [];
const isCmd =
token =>
/^[a-zA-Z]$/.test(
token
);
const nextNum =
() =>
Number(
tokens[i++]
);
const addPoint =
(
px,
py
) => {
memberships.push(
captureMembership(
px,
py,
boxes
)
);
};
while (
i < tokens.length
) {
if (
isCmd(
tokens[i]
)
) {
cmd =
tokens[i++];
}
const relative =
cmd ===
cmd.toLowerCase();
const C =
cmd.toUpperCase();
if (
C === "M" ||
C === "L" ||
C === "T"
) {
while (
i < tokens.length &&
!isCmd(
tokens[i]
)
) {
const px =
nextNum();
const py =
nextNum();
const ax =
relative
? x + px
: px;
const ay =
relative
? y + py
: py;
addPoint(
ax,
ay
);
x = ax;
y = ay;
if (
C === "M"
) {
x0 = ax;
y0 = ay;
}
}
continue;
}
if (
C === "H"
) {
while (
i < tokens.length &&
!isCmd(
tokens[i]
)
) {
const px =
nextNum();
const ax =
relative
? x + px
: px;
addPoint(
ax,
y
);
x = ax;
}
continue;
}
if (
C === "V"
) {
while (
i < tokens.length &&
!isCmd(
tokens[i]
)
) {
const py =
nextNum();
y =
relative
? y + py
: py;
}
continue;
}
if (
C === "C"
) {
while (
i < tokens.length &&
!isCmd(
tokens[i]
)
) {
const values = [];
for (
let j = 0;
j < 3;
j++
) {
const px =
nextNum();
const py =
nextNum();
const ax =
relative
? x + px
: px;
const ay =
relative
? y + py
: py;
values.push([
ax,
ay
]);
}
for (
const point
of values
) {
addPoint(
point[0],
point[1]
);
}
x =
values[2][0];
y =
values[2][1];
}
continue;
}
if (
C === "S" ||
C === "Q"
) {
while (
i < tokens.length &&
!isCmd(
tokens[i]
)
) {
const values = [];
for (
let j = 0;
j < 2;
j++
) {
const px =
nextNum();
const py =
nextNum();
const ax =
relative
? x + px
: px;
const ay =
relative
? y + py
: py;
values.push([
ax,
ay
]);
}
for (
const point
of values
) {
addPoint(
point[0],
point[1]
);
}
x =
values[1][0];
y =
values[1][1];
}
continue;
}
if (
C === "A"
) {
while (
i < tokens.length &&
!isCmd(
tokens[i]
)
) {
nextNum();
nextNum();
nextNum();
nextNum();
nextNum();
const px =
nextNum();
const py =
nextNum();
const ax =
relative
? x + px
: px;
const ay =
relative
? y + py
: py;
addPoint(
ax,
ay
);
x = ax;
y = ay;
}
continue;
}
if (
C === "Z"
) {
x = x0;
y = y0;
continue;
}
return;
}
geometryCaptures.set(
element,
{
type:
"path",
memberships
}
);
}
function shiftPathCaptured(
element,
boxes
) {
const capture =
geometryCaptures.get(
element
);
if (
!capture ||
capture.type !== "path"
) {
return;
}
const d =
element.getAttribute(
"d"
);
const tokens =
tokenizePath(d);
if (!tokens.length) {
return;
}
let tokenIndex = 0;
let memberIndex = 0;
let cmd = "";
let x = 0;
let y = 0;
let x0 = 0;
let y0 = 0;
const out = [];
const isCmd =
token =>
/^[a-zA-Z]$/.test(
token
);
const nextNum =
() =>
Number(
tokens[tokenIndex++]
);
const membership =
() =>
capture.memberships[
memberIndex++
] || [];
const shiftedPoint =
(
px,
py
) => {
return [
px +
dxFromMembership(
membership(),
boxes
),
py
];
};
while (
tokenIndex <
tokens.length
) {
if (
isCmd(
tokens[tokenIndex]
)
) {
cmd =
tokens[
tokenIndex++
];
}
const relative =
cmd ===
cmd.toLowerCase();
const C =
cmd.toUpperCase();
if (
C === "M"
) {
out.push("M");
const px =
nextNum();
const py =
nextNum();
const ax =
relative
? x + px
: px;
const ay =
relative
? y + py
: py;
const p =
shiftedPoint(
ax,
ay
);
out.push(
p[0],
p[1]
);
x = ax;
y = ay;
x0 = ax;
y0 = ay;
while (
tokenIndex <
tokens.length &&
!isCmd(
tokens[
tokenIndex
]
)
) {
const lx =
nextNum();
const ly =
nextNum();
const ax2 =
relative
? x + lx
: lx;
const ay2 =
relative
? y + ly
: ly;
const p2 =
shiftedPoint(
ax2,
ay2
);
out.push(
"L",
p2[0],
p2[1]
);
x = ax2;
y = ay2;
}
continue;
}
if (
C === "L" ||
C === "T"
) {
out.push(C);
while (
tokenIndex <
tokens.length &&
!isCmd(
tokens[
tokenIndex
]
)
) {
const px =
nextNum();
const py =
nextNum();
const ax =
relative
? x + px
: px;
const ay =
relative
? y + py
: py;
const p =
shiftedPoint(
ax,
ay
);
out.push(
p[0],
p[1]
);
x = ax;
y = ay;
}
continue;
}
if (
C === "H"
) {
out.push("H");
while (
tokenIndex <
tokens.length &&
!isCmd(
tokens[
tokenIndex
]
)
) {
const px =
nextNum();
const ax =
relative
? x + px
: px;
const p =
shiftedPoint(
ax,
y
);
out.push(
p[0]
);
x = ax;
}
continue;
}
if (
C === "V"
) {
out.push("V");
while (
tokenIndex <
tokens.length &&
!isCmd(
tokens[
tokenIndex
]
)
) {
const py =
nextNum();
const ay =
relative
? y + py
: py;
out.push(ay);
y = ay;
}
continue;
}
if (
C === "C"
) {
out.push("C");
while (
tokenIndex <
tokens.length &&
!isCmd(
tokens[
tokenIndex
]
)
) {
const values = [];
for (
let j = 0;
j < 3;
j++
) {
const px =
nextNum();
const py =
nextNum();
const ax =
relative
? x + px
: px;
const ay =
relative
? y + py
: py;
values.push([
ax,
ay
]);
}
for (
const point
of values
) {
const shifted =
shiftedPoint(
point[0],
point[1]
);
out.push(
shifted[0],
shifted[1]
);
}
x =
values[2][0];
y =
values[2][1];
}
continue;
}
if (
C === "S" ||
C === "Q"
) {
out.push(C);
while (
tokenIndex <
tokens.length &&
!isCmd(
tokens[
tokenIndex
]
)
) {
const values = [];
for (
let j = 0;
j < 2;
j++
) {
const px =
nextNum();
const py =
nextNum();
const ax =
relative
? x + px
: px;
const ay =
relative
? y + py
: py;
values.push([
ax,
ay
]);
}
for (
const point
of values
) {
const shifted =
shiftedPoint(
point[0],
point[1]
);
out.push(
shifted[0],
shifted[1]
);
}
x =
values[1][0];
y =
values[1][1];
}
continue;
}
if (
C === "A"
) {
out.push("A");
while (
tokenIndex <
tokens.length &&
!isCmd(
tokens[
tokenIndex
]
)
) {
const rx =
nextNum();
const ry =
nextNum();
const rotation =
nextNum();
const largeArc =
nextNum();
const sweep =
nextNum();
const px =
nextNum();
const py =
nextNum();
const ax =
relative
? x + px
: px;
const ay =
relative
? y + py
: py;
const shifted =
shiftedPoint(
ax,
ay
);
out.push(
rx,
ry,
rotation,
largeArc,
sweep,
shifted[0],
shifted[1]
);
x = ax;
y = ay;
}
continue;
}
if (
C === "Z"
) {
out.push("Z");
x = x0;
y = y0;
continue;
}
return;
}
element.setAttribute(
"d",
out.join(" ")
);
}
/* ============================================================
* USE / GLAS
* ============================================================ */
function getUseHref(
useEl
) {
return (
useEl.getAttribute(
"href"
) ||
useEl.getAttribute(
"xlink:href"
) ||
useEl.getAttributeNS(
"http://www.w3.org/1999/xlink",
"href"
) ||
""
);
}
function getReferencedWidth(
svg,
useEl
) {
const href =
getUseHref(
useEl
);
if (
!href ||
!href.startsWith("#")
) {
return null;
}
const ref =
svg.querySelector(
`#${esc(
href.substring(1)
)}`
);
if (!ref) {
return null;
}
const width =
Number(
ref.getAttribute(
"width"
)
);
if (
Number.isFinite(width) &&
width > 0
) {
return width;
}
const viewBox =
ref.getAttribute(
"viewBox"
);
if (viewBox) {
const values =
viewBox
.trim()
.split(
/[\s,]+/
)
.map(Number);
if (
values.length === 4 &&
Number.isFinite(
values[2]
) &&
values[2] > 0
) {
return values[2];
}
}
return null;
}
function parseUseTransform(
useEl
) {
const transform =
useEl.getAttribute(
"transform"
) || "";
const NUM =
"[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?";
const translateRe =
new RegExp(
`translate\\(\\s*(${NUM})\\s*(?:[,\\s]\\s*(${NUM}))?\\s*\\)`,
"i"
);
const scaleRe =
new RegExp(
`scale\\(\\s*(${NUM})\\s*(?:[,\\s]\\s*(${NUM}))?\\s*\\)`,
"i"
);
const translate =
transform.match(
translateRe
);
const scale =
transform.match(
scaleRe
);
if (
!translate ||
!scale
) {
return null;
}
const tx =
Number(
translate[1]
);
const ty =
translate[2] != null
? Number(
translate[2]
)
: 0;
const sx =
Number(
scale[1]
);
const sy =
scale[2] != null
? Number(
scale[2]
)
: sx;
if (
!Number.isFinite(tx) ||
!Number.isFinite(ty) ||
!Number.isFinite(sx) ||
!Number.isFinite(sy)
) {
return null;
}
return {
transform,
translateText:
translate[0],
scaleText:
scale[0],
tx,
ty,
sx,
sy
};
}
function captureUse(
svg,
useEl,
boxes
) {
const parsed =
parseUseTransform(
useEl
);
if (!parsed) {
return;
}
const sourceWidth =
getReferencedWidth(
svg,
useEl
);
if (
!sourceWidth
) {
return;
}
const width =
sourceWidth *
parsed.sx;
geometryCaptures.set(
useEl,
{
type:
"use",
sourceWidth,
left:
captureMembership(
parsed.tx,
parsed.ty,
boxes
),
right:
captureMembership(
parsed.tx + width,
parsed.ty,
boxes
)
}
);
}
function shiftUseCaptured(
useEl,
boxes
) {
const capture =
geometryCaptures.get(
useEl
);
if (
!capture ||
capture.type !== "use"
) {
return;
}
const parsed =
parseUseTransform(
useEl
);
if (!parsed) {
return;
}
const currentWidth =
capture.sourceWidth *
parsed.sx;
const oldRight =
parsed.tx +
currentWidth;
const newLeft =
parsed.tx +
dxFromMembership(
capture.left,
boxes
);
const newRight =
oldRight +
dxFromMembership(
capture.right,
boxes
);
const newWidth =
newRight -
newLeft;
if (
!Number.isFinite(
newWidth
) ||
newWidth <= 0
) {
return;
}
const newScaleX =
newWidth /
capture.sourceWidth;
/*
* translateY blijft onaangetast.
*/
let newTransform =
parsed.transform.replace(
parsed.translateText,
`translate(${newLeft} ${parsed.ty})`
);
/*
* scaleY blijft onaangetast.
*/
newTransform =
newTransform.replace(
parsed.scaleText,
`scale(${newScaleX} ${parsed.sy})`
);
useEl.setAttribute(
"transform",
newTransform
);
}
/* ============================================================
* GRADIENTEN
* ============================================================ */
function ensureDefs(svg) {
let defs =
svg.querySelector(
"defs"
);
if (!defs) {
defs =
document.createElementNS(
"http://www.w3.org/2000/svg",
"defs"
);
svg.insertBefore(
defs,
svg.firstChild
);
}
return defs;
}
function parseFillUrl(fill) {
const match =
String(fill).match(
/^url\((["']?)#([^)'" ]+)\1\)$/
);
return match
? match[2]
: null;
}
function isClearlyUserSpace(
gradient
) {
const units =
(
gradient.getAttribute(
"gradientUnits"
) || ""
).trim();
if (
units ===
"userSpaceOnUse"
) {
return true;
}
if (units) {
return false;
}
for (
const attr
of [
"x1",
"y1",
"x2",
"y2",
"cx",
"cy",
"fx",
"fy",
"r"
]
) {
const value =
gradient.getAttribute(
attr
);
if (
value == null
) {
continue;
}
const n =
Number(
String(value)
.trim()
.replace(
",",
"."
)
);
if (
Number.isFinite(n) &&
(
n < -0.001 ||
n > 1.001
)
) {
return true;
}
}
return false;
}
let gradCloneCounter = 0;
function ensureGradientClone(
svg,
element,
baseId
) {
const defs =
ensureDefs(svg);
if (
!element.dataset.gradCloneId
) {
gradCloneCounter++;
element.dataset.gradCloneId =
`${baseId}__clone__${gradCloneCounter}`;
}
const cloneId =
element.dataset.gradCloneId;
let clone =
defs.querySelector(
`#${esc(
cloneId
)}`
);
if (!clone) {
const original =
svg.querySelector(
`#${esc(
baseId
)}`
);
if (
!original ||
!isClearlyUserSpace(
original
)
) {
return null;
}
clone =
original.cloneNode(
true
);
clone.setAttribute(
"id",
cloneId
);
clone.setAttribute(
"gradientUnits",
"userSpaceOnUse"
);
clone.setAttribute(
"data-base-gradient-transform",
original.getAttribute(
"gradientTransform"
) || ""
);
defs.appendChild(
clone
);
}
element.setAttribute(
"fill",
`url(#${cloneId})`
);
return clone;
}
function representativePoint(
element
) {
const tag =
element.tagName.toLowerCase();
if (
tag === "rect"
) {
const x =
Number(
element.getAttribute("x")
);
const y =
Number(
element.getAttribute("y")
);
const h =
Number(
element.getAttribute(
"height"
)
);
return [
x,
Number.isFinite(h)
? y + h / 2
: y
];
}
if (
tag === "polygon" ||
tag === "polyline"
) {
const values =
parsePoints(
element.getAttribute(
"points"
)
);
if (
values.length >= 2
) {
return [
values[0],
values[1]
];
}
}
if (
tag === "path"
) {
const nums =
(
element.getAttribute(
"d"
) || ""
).match(
/-?\d*\.?\d+(?:e[-+]?\d+)?/g
);
if (
nums &&
nums.length >= 2
) {
return [
Number(nums[0]),
Number(nums[1])
];
}
}
return null;
}
function captureGradient(
element,
boxes
) {
const point =
representativePoint(
element
);
if (!point) {
return;
}
gradientCaptures.set(
element,
captureMembership(
point[0],
point[1],
boxes
)
);
}
function applyGradientShift(
svg,
element,
boxes
) {
const membership =
gradientCaptures.get(
element
);
if (!membership) {
return;
}
const dx =
dxFromMembership(
membership,
boxes
);
if (!dx) {
return;
}
const fill =
element.getAttribute(
"fill"
);
const baseId =
parseFillUrl(fill);
if (!baseId) {
return;
}
const clone =
ensureGradientClone(
svg,
element,
baseId
);
if (!clone) {
return;
}
const baseTransform =
(
clone.getAttribute(
"data-base-gradient-transform"
) || ""
).trim();
const shift =
`translate(${dx},0)`;
clone.setAttribute(
"gradientTransform",
baseTransform
? `${baseTransform} ${shift}`
: shift
);
}
/* ============================================================
* CAPTURE VOLLEDIGE GROUP
* ============================================================ */
function captureGroup(
svg,
group,
boxes
) {
if (
!group ||
!boxes.length
) {
return;
}
group
.querySelectorAll(
"polygon, polyline"
)
.forEach(el => {
capturePointsElement(
el,
boxes
);
});
group
.querySelectorAll(
"rect"
)
.forEach(el => {
captureRect(
el,
boxes
);
});
group
.querySelectorAll(
"path"
)
.forEach(el => {
capturePath(
el,
boxes
);
});
const id =
String(
group.id || ""
).toLowerCase();
if (
id.includes("2glas") ||
id.includes("3glas")
) {
group
.querySelectorAll(
"use"
)
.forEach(el => {
captureUse(
svg,
el,
boxes
);
});
}
group
.querySelectorAll(
"[fill^='url(']"
)
.forEach(el => {
captureGradient(
el,
boxes
);
});
}
/*
* DE BELANGRIJKE FUNCTIE:
*
* Dit gebeurt maar EEN keer.
*/
function captureEverything() {
const svg =
svgRoot();
if (!svg) {
return false;
}
geometryCaptures =
new WeakMap();
gradientCaptures =
new WeakMap();
for (
let vak = 1;
vak <= 5;
vak++
) {
const boxes =
initialBoxesForVak(
svg,
vak
);
if (!boxes.length) {
continue;
}
for (
const suffix
of BI_SUFFIXES
) {
const group =
svg.querySelector(
`#${esc(
`vak-${vak}-BI-${suffix}`
)}`
);
captureGroup(
svg,
group,
boxes
);
}
}
capturesReady =
true;
return true;
}
/* ============================================================
* APPLY CAPTURED GEOMETRY
* ============================================================ */
function applyShiftToGroup(
svg,
group,
boxes
) {
if (
!group ||
!boxes ||
!boxes.length
) {
return;
}
group
.querySelectorAll(
"polygon, polyline"
)
.forEach(el => {
shiftPointsCaptured(
el,
boxes
);
});
group
.querySelectorAll(
"rect"
)
.forEach(el => {
shiftRectCaptured(
el,
boxes
);
});
group
.querySelectorAll(
"path"
)
.forEach(el => {
shiftPathCaptured(
el,
boxes
);
});
const groupId =
String(
group.id || ""
).toLowerCase();
if (
groupId.includes(
"2glas"
) ||
groupId.includes(
"3glas"
)
) {
group
.querySelectorAll(
"use"
)
.forEach(el => {
shiftUseCaptured(
el,
boxes
);
});
}
group
.querySelectorAll(
"[fill^='url(']"
)
.forEach(el => {
applyGradientShift(
svg,
el,
boxes
);
});
}
/* ============================================================
* BUILD + APPLY
* ============================================================ */
function buildAndApplyShifts() {
const svg =
svgRoot();
if (!svg) {
return;
}
/*
* Captures moeten bestaan VOORDAT
* shiftboxen/geometrie verplaatst worden.
*/
if (
!capturesReady
) {
if (
!captureEverything()
) {
return;
}
}
hideNeverShow(svg);
const n =
vakCount();
const items = [];
for (
let i = 1;
i <= n;
i++
) {
const left =
svg.querySelector(
`#${esc(
`vak-${i}-BI-shiftbox-L`
)}`
);
const right =
svg.querySelector(
`#${esc(
`vak-${i}-BI-shiftbox-R`
)}`
);
items.push({
key:
`BI:${i}`,
boxId:
"L",
gEl:
left,
exprNow:
() =>
prefixLeft(
n,
i
) -
Sponning()
});
items.push({
key:
`BI:${i}`,
boxId:
"R",
gEl:
right,
exprNow:
() =>
prefixLeft(
n,
i
) +
BI_DMI_scaled(
n,
i
) +
Sponning()
});
}
const boxesMap =
buildBoxes(
items
);
for (
let i = 1;
i <= n;
i++
) {
const boxes =
boxesMap.get(
`BI:${i}`
) || [];
if (!boxes.length) {
continue;
}
for (
const suffix
of BI_SUFFIXES
) {
const group =
svg.querySelector(
`#${esc(
`vak-${i}-BI-${suffix}`
)}`
);
if (group) {
applyShiftToGroup(
svg,
group,
boxes
);
}
}
}
moveBoxes(
boxesMap
);
hideNeverShow(svg);
}
/* ============================================================
* SCHEDULING
* ============================================================ */
let updateQueued =
false;
function runUpdateAll() {
updateQueued =
false;
updateVisibility();
buildAndApplyShifts();
}
function updateAll() {
if (
updateQueued
) {
return;
}
updateQueued =
true;
queueMicrotask(
runUpdateAll
);
}
/* ============================================================
* LISTENERS
* ============================================================ */
function bind(
root = document
) {
root
.querySelectorAll(
`select[name^="form_fields["][name*="VVI"]`
)
.forEach(el => {
if (
el.dataset.biBound ===
"1"
) {
return;
}
el.dataset.biBound =
"1";
el.addEventListener(
"change",
updateAll
);
el.addEventListener(
"input",
updateAll
);
});
root
.querySelectorAll(
`input[id^="form-field-"]`
)
.forEach(el => {
if (
el.dataset.biShiftBound ===
"1"
) {
return;
}
el.dataset.biShiftBound =
"1";
el.addEventListener(
"change",
updateAll
);
el.addEventListener(
"input",
updateAll
);
});
root
.querySelectorAll(
`input[type="radio"][name="form_fields[vakken]"]`
)
.forEach(el => {
if (
el.dataset.biVakkenBound ===
"1"
) {
return;
}
el.dataset.biVakkenBound =
"1";
el.addEventListener(
"change",
updateAll
);
el.addEventListener(
"input",
updateAll
);
});
}
/* ============================================================
* BOOT
* ============================================================ */
function boot() {
bind(document);
/*
* EERST CAPTURE.
* Daarna pas eerste update.
*/
captureEverything();
runUpdateAll();
const observer =
new MutationObserver(
mutations => {
let newHtml =
false;
for (
const mutation
of mutations
) {
mutation
.addedNodes
.forEach(node => {
if (
!(
node
instanceof
HTMLElement
)
) {
return;
}
newHtml =
true;
bind(node);
});
}
if (newHtml) {
updateAll();
}
}
);
observer.observe(
document.documentElement,
{
childList:
true,
subtree:
true
}
);
}
if (
document.readyState ===
"loading"
) {
document.addEventListener(
"DOMContentLoaded",
boot
);
} else {
boot();
}
/* ============================================================
* HANDMATIGE REBASE
* ============================================================ */
window.rebaseAlleVakkenBI =
() => {
const svg =
svgRoot();
if (!svg) {
return;
}
/*
* Nieuwe shiftbox nulposities.
*/
svg
.querySelectorAll(
`[id*="-BI-shiftbox-"] rect`
)
.forEach(rect => {
rect.dataset.baseX =
String(
Number(
rect.getAttribute(
"x"
) ?? 0
)
);
delete rect.dataset.baseExpr;
});
/*
* Oude L/R koppelingen vergeten.
*/
capturesReady =
false;
geometryCaptures =
new WeakMap();
gradientCaptures =
new WeakMap();
/*
* Eenmalig opnieuw koppelen.
*/
captureEverything();
runUpdateAll();
};
})();
//// VAKVULLING BI #ALLE-VAKKEN ////
(() => {
const SCALE = 3.84615384615;
const SVG_ID = "alle-vakken";
// VVI value -> BI suffix
const VVI_TO_SUFFIX = {
"dubbelglas": "2glas",
"tripleglas": "3glas",
"paneel": "paneel",
"glaspaneel": "glaspaneel",
"deur": "deur",
"kaderdeur": "kaderdeur",
"dubbelkaderdeur": "dubbelkaderdeur",
"draaikiep": "draaikiep",
"vul in": null
};
const BI_SUFFIXES = ["2glas","3glas","paneel","glaspaneel","deur","kaderdeur","dubbelkaderdeur","draaikiep"];
const esc = (s) => CSS.escape(s);
function svgRoot() {
return document.querySelector(`svg#${esc(SVG_ID)}`);
}
function setDisp(el, v) { if (el) el.setAttribute("display", v); }
function ensureDefs(svg) {
let defs = svg.querySelector("defs");
if (!defs) {
defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
svg.insertBefore(defs, svg.firstChild);
}
return defs;
}
function readNumById(id) {
const el = document.getElementById(id);
if (!el) return null;
const v = (el.value ?? "").toString().trim().replace(",", ".");
const n = Number(v);
return Number.isFinite(n) ? n : 0;
}
// Prefer prefixA, else prefixB, else 0
function numField(prefixA, prefixB, suffix) {
const a = readNumById(`form-field-${prefixA}${suffix}`);
if (a != null) return a;
const b = readNumById(`form-field-${prefixB}${suffix}`);
if (b != null) return b;
return 0;
}
function vakCount() {
const checked = document.querySelector('input[type="radio"][name="form_fields[vakken]"]:checked');
if (checked && checked.value) {
const m = String(checked.value).match(/([1-5])/);
if (m) return Number(m[1]);
}
return 5;
}
// VVI select by name form_fields[VVI]
function getVVI(n, i) {
const sel = document.querySelector(`select[name="form_fields[${n}VVI${i}]"]`);
return sel ? String(sel.value || "") : "vul in";
}
function hideNeverShow(svg) {
// BI shiftboxen nooit tonen
svg.querySelectorAll(`[id^="vak-"][id*="-BI-shiftbox-"]`).forEach(el => setDisp(el, "none"));
}
function hideAllBIVariants(svg, i) {
for (const suf of BI_SUFFIXES) {
setDisp(svg.querySelector(`#${esc(`vak-${i}-BI-${suf}`)}`), "none");
}
}
function showGroupAndNestedGs(groupEl) {
if (!groupEl) return;
// shiftboxen sowieso niet tonen
if (String(groupEl.id).includes("-shiftbox-")) { setDisp(groupEl, "none"); return; }
setDisp(groupEl, "block");
groupEl.querySelectorAll("g").forEach(g => {
if (String(g.id).includes("-shiftbox-")) setDisp(g, "none");
else setDisp(g, "block");
});
}
function updateVisibility() {
const svg = svgRoot();
if (!svg) return;
hideNeverShow(svg);
const n = vakCount();
// reset alleen onze BI-groepen
for (let i = 1; i <= 5; i++) hideAllBIVariants(svg, i);
// show voor i<=n op basis van VVI
for (let i = 1; i <= n; i++) {
const value = getVVI(n, i);
const suffix = VVI_TO_SUFFIX[value];
if (!suffix) continue;
const g = svg.querySelector(`#${esc(`vak-${i}-BI-${suffix}`)}`);
showGroupAndNestedGs(g);
}
hideNeverShow(svg);
}
/******************************************************************
* BI position building (LI + DMI/TI cumulatief)
* Alleen DMI geschaald: DMI/SCALE
******************************************************************/
function BI_LI(n) { return numField(`${n}`, "5", "LI"); }
function BI_TI(n, i) { return numField(`${n}`, "5", `TI${i}`); }
function BI_DMI_scaled(n, i) { return (numField(`${n}`, "5", `DMI${i}`) / SCALE); }
function Sponning() { return (readNumById("form-field-BI_sponningbreedte") ?? 0); }
function prefixLeft(n, i) {
let x = BI_LI(n);
if (i >= 2) x += BI_DMI_scaled(n,1) + BI_TI(n,1);
if (i >= 3) x += BI_DMI_scaled(n,2) + BI_TI(n,2);
if (i >= 4) x += BI_DMI_scaled(n,3) + BI_TI(n,3);
if (i >= 5) x += BI_DMI_scaled(n,4) + BI_TI(n,4);
return x;
}
/******************************************************************
* Shiftbox delta-engine
******************************************************************/
function desiredXFromDelta(rect, exprNow) {
const curX = Number(rect.getAttribute("x") ?? "0");
if (rect.dataset.baseX == null) rect.dataset.baseX = String(curX);
if (rect.dataset.baseExpr == null) rect.dataset.baseExpr = String(exprNow);
const baseX = Number(rect.dataset.baseX);
const baseExpr = Number(rect.dataset.baseExpr);
return baseX + (exprNow - baseExpr);
}
function boxFromShiftRect(rect, exprNow) {
const curX = Number(rect.getAttribute("x") ?? "0");
const curY = Number(rect.getAttribute("y") ?? "0");
const w = Number(rect.getAttribute("width") ?? "0");
const h = Number(rect.getAttribute("height") ?? "0");
const desiredX = desiredXFromDelta(rect, exprNow);
return {
rect,
desiredX,
dxInc: desiredX - curX,
minX: curX, maxX: curX + w,
minY: curY, maxY: curY + h,
};
}
function buildBoxes(items) {
const map = new Map(); // key -> boxes[]
for (const it of items) {
if (!it.gEl) continue;
const r = it.gEl.querySelector("rect");
if (!r) continue;
const b = boxFromShiftRect(r, it.exprNow());
if (!map.has(it.key)) map.set(it.key, []);
map.get(it.key).push(b);
}
return map;
}
function moveBoxes(map) {
for (const boxes of map.values()) {
for (const b of boxes) b.rect.setAttribute("x", b.desiredX);
}
}
function dxForPoint(x, y, boxes) {
let dx = 0;
for (const b of boxes) {
if (x >= b.minX && x <= b.maxX && y >= b.minY && y <= b.maxY) dx += b.dxInc;
}
return dx;
}
/******************************************************************
* Geometry shifters
******************************************************************/
function shiftPointsAttr(pointsStr, boxes) {
const nums = pointsStr.trim().replace(/,/g, " ").split(/\s+/).filter(Boolean).map(Number);
if (nums.length < 2 || nums.some(n => !Number.isFinite(n))) return pointsStr;
const out = [];
for (let i = 0; i < nums.length - 1; i += 2) {
const x = nums[i], y = nums[i + 1];
out.push(String(x + dxForPoint(x, y, boxes)), String(y));
}
return out.join(" ");
}
function tokenizePath(d) {
return d.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/g) || [];
}
function shiftPathD(d, boxes) {
const t = tokenizePath(d);
if (!t.length) return d;
let i = 0, cmd = "", x = 0, y = 0, x0 = 0, y0 = 0;
const out = [];
const isCmd = tok => /^[a-zA-Z]$/.test(tok);
const nextNum = () => Number(t[i++]);
const push = v => out.push(v);
const shiftAbs = (px, py) => [px + dxForPoint(px, py, boxes), py];
while (i < t.length) {
if (isCmd(t[i])) cmd = t[i++];
const rel = (cmd === cmd.toLowerCase());
const C = cmd.toUpperCase();
if (C === "M") {
push("M");
const px = nextNum(), py = nextNum();
const ax = rel ? x + px : px, ay = rel ? y + py : py;
const p = shiftAbs(ax, ay);
push(p[0]); push(p[1]);
x = ax; y = ay; x0 = ax; y0 = ay;
while (i < t.length && !isCmd(t[i])) {
push("L");
const lx = nextNum(), ly = nextNum();
const ax2 = rel ? x + lx : lx, ay2 = rel ? y + ly : ly;
const p2 = shiftAbs(ax2, ay2);
push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "L") {
push("L");
while (i < t.length && !isCmd(t[i])) {
const px = nextNum(), py = nextNum();
const ax = rel ? x + px : px, ay = rel ? y + py : py;
const p = shiftAbs(ax, ay);
push(p[0]); push(p[1]);
x = ax; y = ay;
}
continue;
}
if (C === "H") {
push("H");
while (i < t.length && !isCmd(t[i])) {
const px = nextNum();
const ax = rel ? x + px : px;
const p = shiftAbs(ax, y);
push(p[0]);
x = ax;
}
continue;
}
if (C === "V") {
push("V");
while (i < t.length && !isCmd(t[i])) {
const py = nextNum();
const ay = rel ? y + py : py;
push(ay);
y = ay;
}
continue;
}
if (C === "C") {
push("C");
while (i < t.length && !isCmd(t[i])) {
const x1 = nextNum(), y1 = nextNum(), x2 = nextNum(), y2 = nextNum(), x3 = nextNum(), y3 = nextNum();
const ax1 = rel ? x + x1 : x1, ay1 = rel ? y + y1 : y1;
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const ax3 = rel ? x + x3 : x3, ay3 = rel ? y + y3 : y3;
const p1 = shiftAbs(ax1, ay1);
const p2 = shiftAbs(ax2, ay2);
const p3 = shiftAbs(ax3, ay3);
push(p1[0]); push(p1[1]);
push(p2[0]); push(p2[1]);
push(p3[0]); push(p3[1]);
x = ax3; y = ay3;
}
continue;
}
if (C === "S") {
push("S");
while (i < t.length && !isCmd(t[i])) {
const x2 = nextNum(), y2 = nextNum(), x3 = nextNum(), y3 = nextNum();
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const ax3 = rel ? x + x3 : x3, ay3 = rel ? y + y3 : y3;
const p2 = shiftAbs(ax2, ay2);
const p3 = shiftAbs(ax3, ay3);
push(p2[0]); push(p2[1]);
push(p3[0]); push(p3[1]);
x = ax3; y = ay3;
}
continue;
}
if (C === "Q") {
push("Q");
while (i < t.length && !isCmd(t[i])) {
const x1 = nextNum(), y1 = nextNum(), x2 = nextNum(), y2 = nextNum();
const ax1 = rel ? x + x1 : x1, ay1 = rel ? y + y1 : y1;
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const p1 = shiftAbs(ax1, ay1);
const p2 = shiftAbs(ax2, ay2);
push(p1[0]); push(p1[1]);
push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "T") {
push("T");
while (i < t.length && !isCmd(t[i])) {
const x2 = nextNum(), y2 = nextNum();
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const p2 = shiftAbs(ax2, ay2);
push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "A") {
push("A");
while (i < t.length && !isCmd(t[i])) {
const rx = nextNum(), ry = nextNum(), rot = nextNum(), laf = nextNum(), sf = nextNum(), x2 = nextNum(), y2 = nextNum();
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const p2 = shiftAbs(ax2, ay2);
push(rx); push(ry); push(rot); push(laf); push(sf); push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "Z") {
push("Z");
x = x0; y = y0;
continue;
}
return d;
}
return out.join(" ");
}
function shiftRect(el, boxes) {
const x = Number(el.getAttribute("x"));
const y = Number(el.getAttribute("y"));
const w = Number(el.getAttribute("width"));
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(w)) return;
const h = Number(el.getAttribute("height"));
const yMid = Number.isFinite(h) ? (y + h / 2) : y;
const xL2 = x + dxForPoint(x, yMid, boxes);
const xR = x + w;
const xR2 = xR + dxForPoint(xR, yMid, boxes);
el.setAttribute("x", Math.min(xL2, xR2));
el.setAttribute("width", Math.abs(xR2 - xL2));
}
/******************************************************************
* Gradient shifting (zelfde als BU)
******************************************************************/
function parseFillUrl(fill) {
const m = String(fill).match(/^url\((["']?)#([^)'" ]+)\1\)$/);
return m ? m[2] : null;
}
function isClearlyUserSpace(gradEl) {
const units = (gradEl.getAttribute("gradientUnits") || "").trim();
if (units === "userSpaceOnUse") return true;
if (units) return false;
for (const a of ["x1","y1","x2","y2","cx","cy","fx","fy","r"]) {
const v = gradEl.getAttribute(a);
if (v == null) continue;
const n = Number(String(v).trim().replace(",", "."));
if (!Number.isFinite(n)) continue;
if (n < -0.001 || n > 1.001) return true;
}
return false;
}
let gradCloneCounter = 0;
function ensureGradientClone(svg, el, baseGradId) {
const defs = ensureDefs(svg);
if (!el.dataset.gradCloneId) {
gradCloneCounter += 1;
el.dataset.gradCloneId = `${baseGradId}__clone__${gradCloneCounter}`;
}
const cloneId = el.dataset.gradCloneId;
let clone = defs.querySelector(`#${esc(cloneId)}`);
if (!clone) {
const orig = svg.querySelector(`#${esc(baseGradId)}`);
if (!orig) return null;
if (!isClearlyUserSpace(orig)) return null;
clone = orig.cloneNode(true);
clone.setAttribute("id", cloneId);
clone.setAttribute("gradientUnits", "userSpaceOnUse");
clone.setAttribute("data-base-gradient-transform", orig.getAttribute("gradientTransform") || "");
defs.appendChild(clone);
}
el.setAttribute("fill", `url(#${cloneId})`);
return clone;
}
function dxRepresentative(el, boxes) {
const tag = el.tagName.toLowerCase();
if (tag === "rect") {
const x = Number(el.getAttribute("x"));
const y = Number(el.getAttribute("y"));
const h = Number(el.getAttribute("height"));
const yMid = Number.isFinite(h) ? (y + h / 2) : y;
if (!Number.isFinite(x) || !Number.isFinite(yMid)) return 0;
return dxForPoint(x, yMid, boxes);
}
if (tag === "polygon" || tag === "polyline") {
const pts = (el.getAttribute("points") || "").trim().replace(/,/g, " ").split(/\s+/).filter(Boolean).map(Number);
if (pts.length < 2 || pts.some(n => !Number.isFinite(n))) return 0;
return dxForPoint(pts[0], pts[1], boxes);
}
if (tag === "path") {
const d = el.getAttribute("d") || "";
const nums = d.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/g);
if (!nums || nums.length < 2) return 0;
const x = Number(nums[0]), y = Number(nums[1]);
if (!Number.isFinite(x) || !Number.isFinite(y)) return 0;
return dxForPoint(x, y, boxes);
}
return 0;
}
function applyGradientShift(svg, el, dx) {
const fill = el.getAttribute("fill");
if (!fill) return;
const base = parseFillUrl(fill);
if (!base) return;
const clone = ensureGradientClone(svg, el, base);
if (!clone) return;
const baseT = (clone.getAttribute("data-base-gradient-transform") || "").trim();
const t = `translate(${dx},0)`;
clone.setAttribute("gradientTransform", baseT ? `${baseT} ${t}` : t);
}
function applyShiftToGroup(svg, g, boxes) {
if (!g || !boxes || !boxes.length) return;
const gradDx = new Map();
g.querySelectorAll("[fill^='url(']").forEach(el => {
const dx = dxRepresentative(el, boxes);
if (dx) gradDx.set(el, dx);
});
g.querySelectorAll("polygon").forEach(el => {
const p = el.getAttribute("points");
if (p) el.setAttribute("points", shiftPointsAttr(p, boxes));
});
g.querySelectorAll("polyline").forEach(el => {
const p = el.getAttribute("points");
if (p) el.setAttribute("points", shiftPointsAttr(p, boxes));
});
g.querySelectorAll("rect").forEach(el => shiftRect(el, boxes));
g.querySelectorAll("path").forEach(el => {
const d = el.getAttribute("d");
if (d) el.setAttribute("d", shiftPathD(d, boxes));
});
for (const [el, dx] of gradDx.entries()) applyGradientShift(svg, el, dx);
}
function buildAndApplyShifts() {
const svg = svgRoot();
if (!svg) return;
hideNeverShow(svg);
const n = vakCount();
// BI shiftboxen per vak
const items = [];
for (let i = 1; i <= n; i++) {
const gL = svg.querySelector(`#${esc(`vak-${i}-BI-shiftbox-L`)}`);
const gR = svg.querySelector(`#${esc(`vak-${i}-BI-shiftbox-R`)}`);
items.push({
key: `BI:${i}`,
gEl: gL,
exprNow: () => prefixLeft(n, i) - Sponning()
});
items.push({
key: `BI:${i}`,
gEl: gR,
exprNow: () => prefixLeft(n, i) + BI_DMI_scaled(n, i) + Sponning()
});
}
const boxesMap = buildBoxes(items);
// Apply geometry shifts alleen op onze BI-variantgroepen
for (let i = 1; i <= n; i++) {
const boxes = boxesMap.get(`BI:${i}`) || [];
if (!boxes.length) continue;
for (const suf of BI_SUFFIXES) {
const g = svg.querySelector(`#${esc(`vak-${i}-BI-${suf}`)}`);
if (g) applyShiftToGroup(svg, g, boxes);
}
}
// Verplaats shiftbox rects (blijven hidden)
moveBoxes(boxesMap);
hideNeverShow(svg);
}
/******************************************************************
* Scheduling
******************************************************************/
let updateQueued = false;
function runUpdateAll() {
updateQueued = false;
updateVisibility();
buildAndApplyShifts();
}
function updateAll() {
if (updateQueued) return;
updateQueued = true;
queueMicrotask(runUpdateAll);
}
/******************************************************************
* Bind listeners (Elementor friendly)
******************************************************************/
function bind(root = document) {
// VVI selects
root.querySelectorAll(`select[name^="form_fields["][name*="VVI"]`).forEach(el => {
if (el.dataset.biBound === "1") return;
el.dataset.biBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// all numeric inputs (LI/RI/TI*/DMI*/sponning etc.)
root.querySelectorAll(`input[id^="form-field-"]`).forEach(el => {
if (el.dataset.biShiftBound === "1") return;
el.dataset.biShiftBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// vakken radios
root.querySelectorAll(`input[type="radio"][name="form_fields[vakken]"]`).forEach(el => {
if (el.dataset.biVakkenBound === "1") return;
el.dataset.biVakkenBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
}
function boot() {
bind(document);
runUpdateAll();
const mo = new MutationObserver(muts => {
let hasNewHtml = false;
for (const mu of muts) {
mu.addedNodes.forEach(node => {
if (!(node instanceof HTMLElement)) return;
hasNewHtml = true;
bind(node);
});
}
if (hasNewHtml) {
updateAll();
}
});
mo.observe(document.documentElement, { childList: true, subtree: true });
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
else boot();
// Optional: reset baselines (als andere scripts eerst iets aanpassen)
window.rebaseAlleVakkenBI = () => {
const svg = svgRoot();
if (!svg) return;
svg.querySelectorAll(`[id*="-BI-shiftbox-"] rect`).forEach(r => {
r.dataset.baseX = String(Number(r.getAttribute("x") ?? "0"));
delete r.dataset.baseExpr;
});
runUpdateAll();
};
})();
//// VAKVULLING BI #ALLE-VAKKEN ////
(() => {
const SCALE = 3.84615384615;
const SVG_ID = "alle-vakken";
// VVI value -> BI suffix
const VVI_TO_SUFFIX = {
"dubbelglas": "2glas",
"tripleglas": "3glas",
"paneel": "paneel",
"glaspaneel": "glaspaneel",
"deur": "deur",
"kaderdeur": "kaderdeur",
"dubbelkaderdeur": "dubbelkaderdeur",
"draaikiep": "draaikiep",
"vul in": null
};
const BI_SUFFIXES = ["2glas","3glas","paneel","glaspaneel","deur","kaderdeur" ,"dubbelkaderdeur","draaikiep"];
const esc = (s) => CSS.escape(s);
function svgRoot() {
return document.querySelector(`svg#${esc(SVG_ID)}`);
}
function setDisp(el, v) { if (el) el.setAttribute("display", v); }
function ensureDefs(svg) {
let defs = svg.querySelector("defs");
if (!defs) {
defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
svg.insertBefore(defs, svg.firstChild);
}
return defs;
}
function readNumById(id) {
const el = document.getElementById(id);
if (!el) return null;
const v = (el.value ?? "").toString().trim().replace(",", ".");
const n = Number(v);
return Number.isFinite(n) ? n : 0;
}
// Prefer prefixA, else prefixB, else 0
function numField(prefixA, prefixB, suffix) {
const a = readNumById(`form-field-${prefixA}${suffix}`);
if (a != null) return a;
const b = readNumById(`form-field-${prefixB}${suffix}`);
if (b != null) return b;
return 0;
}
function vakCount() {
const checked = document.querySelector('input[type="radio"][name="form_fields[vakken]"]:checked');
if (checked && checked.value) {
const m = String(checked.value).match(/([1-5])/);
if (m) return Number(m[1]);
}
return 5;
}
// VVI select by name form_fields[VVI]
function getVVI(n, i) {
const sel = document.querySelector(`select[name="form_fields[${n}VVI${i}]"]`);
return sel ? String(sel.value || "") : "vul in";
}
function hideNeverShow(svg) {
// BI shiftboxen nooit tonen
svg.querySelectorAll(`[id^="vak-"][id*="-BI-shiftbox-"]`).forEach(el => setDisp(el, "none"));
}
function hideAllBIVariants(svg, i) {
for (const suf of BI_SUFFIXES) {
setDisp(svg.querySelector(`#${esc(`vak-${i}-BI-${suf}`)}`), "none");
}
}
function showGroupAndNestedGs(groupEl) {
if (!groupEl) return;
// shiftboxen sowieso niet tonen
if (String(groupEl.id).includes("-shiftbox-")) { setDisp(groupEl, "none"); return; }
setDisp(groupEl, "block");
groupEl.querySelectorAll("g").forEach(g => {
if (String(g.id).includes("-shiftbox-")) setDisp(g, "none");
else setDisp(g, "block");
});
}
function updateVisibility() {
const svg = svgRoot();
if (!svg) return;
hideNeverShow(svg);
const n = vakCount();
// reset alleen onze BI-groepen
for (let i = 1; i <= 5; i++) hideAllBIVariants(svg, i);
// show voor i<=n op basis van VVI
for (let i = 1; i <= n; i++) {
const value = getVVI(n, i);
const suffix = VVI_TO_SUFFIX[value];
if (!suffix) continue;
const g = svg.querySelector(`#${esc(`vak-${i}-BI-${suffix}`)}`);
showGroupAndNestedGs(g);
}
hideNeverShow(svg);
}
/******************************************************************
* BI position building (LI + DMI/TI cumulatief)
* Alleen DMI geschaald: DMI/SCALE
******************************************************************/
function BI_LI(n) { return numField(`${n}`, "5", "LI"); }
function BI_TI(n, i) { return numField(`${n}`, "5", `TI${i}`); }
function BI_DMI_scaled(n, i) { return (numField(`${n}`, "5", `DMI${i}`) / SCALE); }
function Sponning() { return (readNumById("form-field-BI_sponningbreedte") ?? 0); }
function prefixLeft(n, i) {
let x = BI_LI(n);
if (i >= 2) x += BI_DMI_scaled(n,1) + BI_TI(n,1);
if (i >= 3) x += BI_DMI_scaled(n,2) + BI_TI(n,2);
if (i >= 4) x += BI_DMI_scaled(n,3) + BI_TI(n,3);
if (i >= 5) x += BI_DMI_scaled(n,4) + BI_TI(n,4);
return x;
}
/******************************************************************
* Shiftbox delta-engine
******************************************************************/
function desiredXFromDelta(rect, exprNow) {
const curX = Number(rect.getAttribute("x") ?? "0");
if (rect.dataset.baseX == null) rect.dataset.baseX = String(curX);
if (rect.dataset.baseExpr == null) rect.dataset.baseExpr = String(exprNow);
const baseX = Number(rect.dataset.baseX);
const baseExpr = Number(rect.dataset.baseExpr);
return baseX + (exprNow - baseExpr);
}
function boxFromShiftRect(rect, exprNow) {
const curX = Number(rect.getAttribute("x") ?? "0");
const curY = Number(rect.getAttribute("y") ?? "0");
const w = Number(rect.getAttribute("width") ?? "0");
const h = Number(rect.getAttribute("height") ?? "0");
const desiredX = desiredXFromDelta(rect, exprNow);
return {
rect,
desiredX,
dxInc: desiredX - curX,
minX: curX, maxX: curX + w,
minY: curY, maxY: curY + h,
};
}
function buildBoxes(items) {
const map = new Map(); // key -> boxes[]
for (const it of items) {
if (!it.gEl) continue;
const r = it.gEl.querySelector("rect");
if (!r) continue;
const b = boxFromShiftRect(r, it.exprNow());
if (!map.has(it.key)) map.set(it.key, []);
map.get(it.key).push(b);
}
return map;
}
function moveBoxes(map) {
for (const boxes of map.values()) {
for (const b of boxes) b.rect.setAttribute("x", b.desiredX);
}
}
function dxForPoint(x, y, boxes) {
let dx = 0;
for (const b of boxes) {
if (x >= b.minX && x <= b.maxX && y >= b.minY && y <= b.maxY) dx += b.dxInc;
}
return dx;
}
/******************************************************************
* Geometry shifters
******************************************************************/
function shiftPointsAttr(pointsStr, boxes) {
const nums = pointsStr.trim().replace(/,/g, " ").split(/\s+/).filter(Boolean).map(Number);
if (nums.length < 2 || nums.some(n => !Number.isFinite(n))) return pointsStr;
const out = [];
for (let i = 0; i < nums.length - 1; i += 2) {
const x = nums[i], y = nums[i + 1];
out.push(String(x + dxForPoint(x, y, boxes)), String(y));
}
return out.join(" ");
}
function tokenizePath(d) {
return d.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/g) || [];
}
function shiftPathD(d, boxes) {
const t = tokenizePath(d);
if (!t.length) return d;
let i = 0, cmd = "", x = 0, y = 0, x0 = 0, y0 = 0;
const out = [];
const isCmd = tok => /^[a-zA-Z]$/.test(tok);
const nextNum = () => Number(t[i++]);
const push = v => out.push(v);
const shiftAbs = (px, py) => [px + dxForPoint(px, py, boxes), py];
while (i < t.length) {
if (isCmd(t[i])) cmd = t[i++];
const rel = (cmd === cmd.toLowerCase());
const C = cmd.toUpperCase();
if (C === "M") {
push("M");
const px = nextNum(), py = nextNum();
const ax = rel ? x + px : px, ay = rel ? y + py : py;
const p = shiftAbs(ax, ay);
push(p[0]); push(p[1]);
x = ax; y = ay; x0 = ax; y0 = ay;
while (i < t.length && !isCmd(t[i])) {
push("L");
const lx = nextNum(), ly = nextNum();
const ax2 = rel ? x + lx : lx, ay2 = rel ? y + ly : ly;
const p2 = shiftAbs(ax2, ay2);
push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "L") {
push("L");
while (i < t.length && !isCmd(t[i])) {
const px = nextNum(), py = nextNum();
const ax = rel ? x + px : px, ay = rel ? y + py : py;
const p = shiftAbs(ax, ay);
push(p[0]); push(p[1]);
x = ax; y = ay;
}
continue;
}
if (C === "H") {
push("H");
while (i < t.length && !isCmd(t[i])) {
const px = nextNum();
const ax = rel ? x + px : px;
const p = shiftAbs(ax, y);
push(p[0]);
x = ax;
}
continue;
}
if (C === "V") {
push("V");
while (i < t.length && !isCmd(t[i])) {
const py = nextNum();
const ay = rel ? y + py : py;
push(ay);
y = ay;
}
continue;
}
if (C === "C") {
push("C");
while (i < t.length && !isCmd(t[i])) {
const x1 = nextNum(), y1 = nextNum(), x2 = nextNum(), y2 = nextNum(), x3 = nextNum(), y3 = nextNum();
const ax1 = rel ? x + x1 : x1, ay1 = rel ? y + y1 : y1;
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const ax3 = rel ? x + x3 : x3, ay3 = rel ? y + y3 : y3;
const p1 = shiftAbs(ax1, ay1);
const p2 = shiftAbs(ax2, ay2);
const p3 = shiftAbs(ax3, ay3);
push(p1[0]); push(p1[1]);
push(p2[0]); push(p2[1]);
push(p3[0]); push(p3[1]);
x = ax3; y = ay3;
}
continue;
}
if (C === "S") {
push("S");
while (i < t.length && !isCmd(t[i])) {
const x2 = nextNum(), y2 = nextNum(), x3 = nextNum(), y3 = nextNum();
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const ax3 = rel ? x + x3 : x3, ay3 = rel ? y + y3 : y3;
const p2 = shiftAbs(ax2, ay2);
const p3 = shiftAbs(ax3, ay3);
push(p2[0]); push(p2[1]);
push(p3[0]); push(p3[1]);
x = ax3; y = ay3;
}
continue;
}
if (C === "Q") {
push("Q");
while (i < t.length && !isCmd(t[i])) {
const x1 = nextNum(), y1 = nextNum(), x2 = nextNum(), y2 = nextNum();
const ax1 = rel ? x + x1 : x1, ay1 = rel ? y + y1 : y1;
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const p1 = shiftAbs(ax1, ay1);
const p2 = shiftAbs(ax2, ay2);
push(p1[0]); push(p1[1]);
push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "T") {
push("T");
while (i < t.length && !isCmd(t[i])) {
const x2 = nextNum(), y2 = nextNum();
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const p2 = shiftAbs(ax2, ay2);
push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "A") {
push("A");
while (i < t.length && !isCmd(t[i])) {
const rx = nextNum(), ry = nextNum(), rot = nextNum(), laf = nextNum(), sf = nextNum(), x2 = nextNum(), y2 = nextNum();
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const p2 = shiftAbs(ax2, ay2);
push(rx); push(ry); push(rot); push(laf); push(sf); push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "Z") {
push("Z");
x = x0; y = y0;
continue;
}
return d;
}
return out.join(" ");
}
function shiftRect(el, boxes) {
const x = Number(el.getAttribute("x"));
const y = Number(el.getAttribute("y"));
const w = Number(el.getAttribute("width"));
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(w)) return;
const h = Number(el.getAttribute("height"));
const yMid = Number.isFinite(h) ? (y + h / 2) : y;
const xL2 = x + dxForPoint(x, yMid, boxes);
const xR = x + w;
const xR2 = xR + dxForPoint(xR, yMid, boxes);
el.setAttribute("x", Math.min(xL2, xR2));
el.setAttribute("width", Math.abs(xR2 - xL2));
}
/******************************************************************
* Gradient shifting (zelfde als BU)
******************************************************************/
function parseFillUrl(fill) {
const m = String(fill).match(/^url\((["']?)#([^)'" ]+)\1\)$/);
return m ? m[2] : null;
}
function isClearlyUserSpace(gradEl) {
const units = (gradEl.getAttribute("gradientUnits") || "").trim();
if (units === "userSpaceOnUse") return true;
if (units) return false;
for (const a of ["x1","y1","x2","y2","cx","cy","fx","fy","r"]) {
const v = gradEl.getAttribute(a);
if (v == null) continue;
const n = Number(String(v).trim().replace(",", "."));
if (!Number.isFinite(n)) continue;
if (n < -0.001 || n > 1.001) return true;
}
return false;
}
let gradCloneCounter = 0;
function ensureGradientClone(svg, el, baseGradId) {
const defs = ensureDefs(svg);
if (!el.dataset.gradCloneId) {
gradCloneCounter += 1;
el.dataset.gradCloneId = `${baseGradId}__clone__${gradCloneCounter}`;
}
const cloneId = el.dataset.gradCloneId;
let clone = defs.querySelector(`#${esc(cloneId)}`);
if (!clone) {
const orig = svg.querySelector(`#${esc(baseGradId)}`);
if (!orig) return null;
if (!isClearlyUserSpace(orig)) return null;
clone = orig.cloneNode(true);
clone.setAttribute("id", cloneId);
clone.setAttribute("gradientUnits", "userSpaceOnUse");
clone.setAttribute("data-base-gradient-transform", orig.getAttribute("gradientTransform") || "");
defs.appendChild(clone);
}
el.setAttribute("fill", `url(#${cloneId})`);
return clone;
}
function dxRepresentative(el, boxes) {
const tag = el.tagName.toLowerCase();
if (tag === "rect") {
const x = Number(el.getAttribute("x"));
const y = Number(el.getAttribute("y"));
const h = Number(el.getAttribute("height"));
const yMid = Number.isFinite(h) ? (y + h / 2) : y;
if (!Number.isFinite(x) || !Number.isFinite(yMid)) return 0;
return dxForPoint(x, yMid, boxes);
}
if (tag === "polygon" || tag === "polyline") {
const pts = (el.getAttribute("points") || "").trim().replace(/,/g, " ").split(/\s+/).filter(Boolean).map(Number);
if (pts.length < 2 || pts.some(n => !Number.isFinite(n))) return 0;
return dxForPoint(pts[0], pts[1], boxes);
}
if (tag === "path") {
const d = el.getAttribute("d") || "";
const nums = d.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/g);
if (!nums || nums.length < 2) return 0;
const x = Number(nums[0]), y = Number(nums[1]);
if (!Number.isFinite(x) || !Number.isFinite(y)) return 0;
return dxForPoint(x, y, boxes);
}
return 0;
}
function applyGradientShift(svg, el, dx) {
const fill = el.getAttribute("fill");
if (!fill) return;
const base = parseFillUrl(fill);
if (!base) return;
const clone = ensureGradientClone(svg, el, base);
if (!clone) return;
const baseT = (clone.getAttribute("data-base-gradient-transform") || "").trim();
const t = `translate(${dx},0)`;
clone.setAttribute("gradientTransform", baseT ? `${baseT} ${t}` : t);
}
function applyShiftToGroup(svg, g, boxes) {
if (!g || !boxes || !boxes.length) return;
const gradDx = new Map();
g.querySelectorAll("[fill^='url(']").forEach(el => {
const dx = dxRepresentative(el, boxes);
if (dx) gradDx.set(el, dx);
});
g.querySelectorAll("polygon").forEach(el => {
const p = el.getAttribute("points");
if (p) el.setAttribute("points", shiftPointsAttr(p, boxes));
});
g.querySelectorAll("polyline").forEach(el => {
const p = el.getAttribute("points");
if (p) el.setAttribute("points", shiftPointsAttr(p, boxes));
});
g.querySelectorAll("rect").forEach(el => shiftRect(el, boxes));
g.querySelectorAll("path").forEach(el => {
const d = el.getAttribute("d");
if (d) el.setAttribute("d", shiftPathD(d, boxes));
});
for (const [el, dx] of gradDx.entries()) applyGradientShift(svg, el, dx);
}
function buildAndApplyShifts() {
const svg = svgRoot();
if (!svg) return;
hideNeverShow(svg);
const n = vakCount();
// BI shiftboxen per vak
const items = [];
for (let i = 1; i <= n; i++) {
const gL = svg.querySelector(`#${esc(`vak-${i}-BI-shiftbox-L`)}`);
const gR = svg.querySelector(`#${esc(`vak-${i}-BI-shiftbox-R`)}`);
items.push({
key: `BI:${i}`,
gEl: gL,
exprNow: () => prefixLeft(n, i) - Sponning()
});
items.push({
key: `BI:${i}`,
gEl: gR,
exprNow: () => prefixLeft(n, i) + BI_DMI_scaled(n, i) + Sponning()
});
}
const boxesMap = buildBoxes(items);
// Apply geometry shifts alleen op onze BI-variantgroepen
for (let i = 1; i <= n; i++) {
const boxes = boxesMap.get(`BI:${i}`) || [];
if (!boxes.length) continue;
for (const suf of BI_SUFFIXES) {
const g = svg.querySelector(`#${esc(`vak-${i}-BI-${suf}`)}`);
if (g) applyShiftToGroup(svg, g, boxes);
}
}
// Verplaats shiftbox rects (blijven hidden)
moveBoxes(boxesMap);
hideNeverShow(svg);
}
function updateAll() {
updateVisibility();
buildAndApplyShifts();
}
/******************************************************************
* Bind listeners (Elementor friendly)
******************************************************************/
function bind(root = document) {
// VVI selects
root.querySelectorAll(`select[name^="form_fields["][name*="VVI"]`).forEach(el => {
if (el.dataset.biBound === "1") return;
el.dataset.biBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// all numeric inputs (LI/RI/TI*/DMI*/sponning etc.)
root.querySelectorAll(`input[id^="form-field-"]`).forEach(el => {
if (el.dataset.biShiftBound === "1") return;
el.dataset.biShiftBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// vakken radios
root.querySelectorAll(`input[type="radio"][name="form_fields[vakken]"]`).forEach(el => {
if (el.dataset.biVakkenBound === "1") return;
el.dataset.biVakkenBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
}
function boot() {
bind(document);
updateAll();
const mo = new MutationObserver(muts => {
for (const mu of muts) {
mu.addedNodes.forEach(node => {
if (!(node instanceof HTMLElement)) return;
bind(node);
});
}
updateAll();
});
mo.observe(document.documentElement, { childList: true, subtree: true });
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
else boot();
// Optional: reset baselines (als andere scripts eerst iets aanpassen)
window.rebaseAlleVakkenBI = () => {
const svg = svgRoot();
if (!svg) return;
svg.querySelectorAll(`[id*="-BI-shiftbox-"] rect`).forEach(r => {
r.dataset.baseX = String(Number(r.getAttribute("x") ?? "0"));
delete r.dataset.baseExpr;
});
updateAll();
};
})();
//// VAKVULLING BU/HS #ALLE-VAKKEN ////
(() => {
const SCALE = 3.84615384615;
const SVG_ID = "alle-vakken";
// BU mapping (VVU -> BU variant)
const VVU_TO_BU_SUFFIX = {
"dubbelglas": "2glas-BU",
"tripleglas": "3glas-BU",
"paneel": "paneel",
"glaspaneel": "glaspaneel",
"deur": "deur",
"kaderdeur": "kaderdeur",
"dubbelkaderdeur": "dubbelkaderdeur",
// ✅ deze 3 blijven in mapping, maar worden geometrisch als BU-BI bucket behandeld
"dubbelglas binnen": "2glas-BI",
"tripleglas binnen": "3glas-BI",
"draaikiep": "draaikiep",
"vul in": null,
};
// HS mapping (HS-opties zitten óók in VVU)
const VVU_TO_HS_MODE = {
"linksschuivend": "LS",
"rechtsschuivend": "RS",
"linksrechtsschuivend": "LR",
"ls": "LS",
"rs": "RS",
"lr": "LR",
};
// ✅ BU split: BU-main vs BU-BI
const BU_MAIN_SUFFIXES = ["2glas-BU","3glas-BU","paneel","glaspaneel","deur","kaderdeur","dubbelkaderdeur"];
const BU_BI_SUFFIXES = ["2glas-BI","3glas-BI","draaikiep"];
const HS_MODES = ["LS","RS","LR"];
const esc = (s) => CSS.escape(s);
function svgRoot() {
return document.querySelector(`svg#${esc(SVG_ID)}`);
}
function setDisp(el, v) { if (el) el.setAttribute("display", v); }
function normalizeKey(v) {
return String(v || "")
.trim()
.toLowerCase()
.replace(/\s+/g, "")
.replace(/_/g, "")
.replace(/-/g, "");
}
function vakCount() {
const checked = document.querySelector('input[type="radio"][name="form_fields[vakken]"]:checked');
if (checked && checked.value) {
const m = String(checked.value).match(/([1-5])/);
if (m) return Number(m[1]);
}
return 5;
}
function readInputValueById(id) {
const el = document.getElementById(id);
if (!el) return null;
const v = (el.value ?? "").toString().trim().replace(",", ".");
const n = Number(v);
return Number.isFinite(n) ? n : 0;
}
// Prefer prefixA, else prefixB, else 0
function numField(prefixA, prefixB, suffix) {
const a = readInputValueById(`form-field-${prefixA}${suffix}`);
if (a != null) return a;
const b = readInputValueById(`form-field-${prefixB}${suffix}`);
if (b != null) return b;
return 0;
}
// VVU select by name form_fields[VVU]
function getVVU(n, i) {
const sel = document.querySelector(`select[name="form_fields[${n}VVU${i}]"]`);
return sel ? String(sel.value || "") : "vul in";
}
// ✅ Dorpeltype radio (pakt liefst data-original-value, anders value)
function getDorpelType() {
const checked = document.querySelector('input[type="radio"][name="form_fields[dorpeltype]"]:checked');
if (!checked) return null;
return (checked.getAttribute("data-original-value") || checked.value || "").toString().trim();
}
/******************************************************************
* ✅ NEW: HS verdiept (nok) inputs
******************************************************************/
function getRadioValueByName(name) {
const el = document.querySelector(`input[type="radio"][name="${name}"]:checked`);
return el ? String(el.value || "").trim().toLowerCase() : "";
}
function isVerdieptVJa() {
return getRadioValueByName("form_fields[verdiept_v]") === "ja";
}
function isVerdieptSJa() {
return getRadioValueByName("form_fields[verdiept_s]") === "ja";
}
function nokV() {
return (readInputValueById("form-field-nokdiktevast") ?? 0);
}
function nokS() {
return (readInputValueById("form-field-nokdikteschuif") ?? 0);
}
/******************************************************************
* NEVER SHOW elements (shiftboxes + BU-LR placeholders)
******************************************************************/
function hideNeverShow(svg) {
for (let i = 1; i <= 5; i++) {
setDisp(svg.querySelector(`#${esc(`vak-${i}-BU-LR`)}`), "none");
}
svg.querySelectorAll(`[id*="-BU-shiftbox-"]`).forEach(el => setDisp(el, "none"));
svg.querySelectorAll(`[id*="-HS-shiftbox-"]`).forEach(el => setDisp(el, "none"));
}
/******************************************************************
* VISIBILITY: only whitelist BU & HS groups
******************************************************************/
function showGroupAndNestedGs(groupEl) {
if (!groupEl) return;
const deny = (id) => {
const s = String(id || "");
return s.includes("-shiftbox-") || s.endsWith("-BU-LR") || s.includes("-BU-LR-");
};
if (!deny(groupEl.id)) setDisp(groupEl, "block");
groupEl.querySelectorAll("g").forEach(g => {
if (deny(g.id)) setDisp(g, "none");
else setDisp(g, "block");
});
}
function hideAllBU(svg, i) {
for (const suf of BU_MAIN_SUFFIXES) setDisp(svg.querySelector(`#${esc(`vak-${i}-BU-${suf}`)}`), "none");
for (const suf of BU_BI_SUFFIXES) setDisp(svg.querySelector(`#${esc(`vak-${i}-BU-${suf}`)}`), "none");
}
function hideAllHS(svg, i) {
for (const m of HS_MODES) setDisp(svg.querySelector(`#${esc(`vak-${i}-HS-${m}`)}`), "none");
}
function updateVisibility() {
const svg = svgRoot();
if (!svg) return;
hideNeverShow(svg);
const n = vakCount();
for (let i = 1; i <= 5; i++) {
hideAllBU(svg, i);
hideAllHS(svg, i);
}
for (let i = 1; i <= n; i++) {
const raw = getVVU(n, i);
const key = normalizeKey(raw);
const hsMode = VVU_TO_HS_MODE[key] || null;
let buSuffix = Object.prototype.hasOwnProperty.call(VVU_TO_BU_SUFFIX, raw)
? VVU_TO_BU_SUFFIX[raw]
: null;
const dorpelType = getDorpelType();
if (dorpelType === "KVTPKU" && raw === "kaderdeur") {
buSuffix = "dubbelkaderdeur";
}
if (hsMode) {
const g = svg.querySelector(`#${esc(`vak-${i}-HS-${hsMode}`)}`);
showGroupAndNestedGs(g);
} else if (buSuffix) {
const g = svg.querySelector(`#${esc(`vak-${i}-BU-${buSuffix}`)}`);
showGroupAndNestedGs(g);
}
}
hideNeverShow(svg);
}
/******************************************************************
* SHIFTBOX ENGINE (delta vs baseline)
******************************************************************/
function desiredXFromDelta(rect, exprNow) {
const curX = Number(rect.getAttribute("x") ?? "0");
if (rect.dataset.baseX == null) rect.dataset.baseX = String(curX);
if (rect.dataset.baseExpr == null) rect.dataset.baseExpr = String(exprNow);
const baseX = Number(rect.dataset.baseX);
const baseExpr = Number(rect.dataset.baseExpr);
return baseX + (exprNow - baseExpr);
}
function boxFromShiftRect(rect, exprNow) {
const curX = Number(rect.getAttribute("x") ?? "0");
const curY = Number(rect.getAttribute("y") ?? "0");
const w = Number(rect.getAttribute("width") ?? "0");
const h = Number(rect.getAttribute("height") ?? "0");
const desiredX = desiredXFromDelta(rect, exprNow);
return {
rect,
desiredX,
dxInc: desiredX - curX,
minX: curX, maxX: curX + w,
minY: curY, maxY: curY + h,
};
}
function buildBoxes(items) {
const map = new Map();
for (const it of items) {
if (!it.gEl) continue;
const r = it.gEl.querySelector("rect");
if (!r) continue;
const b = boxFromShiftRect(r, it.exprNow());
if (!map.has(it.key)) map.set(it.key, []);
map.get(it.key).push(b);
}
return map;
}
function moveBoxes(map) {
for (const boxes of map.values()) {
for (const b of boxes) b.rect.setAttribute("x", b.desiredX);
}
}
function dxForPoint(x, y, boxes) {
let dx = 0;
for (const b of boxes) {
if (x >= b.minX && x <= b.maxX && y >= b.minY && y <= b.maxY) dx += b.dxInc;
}
return dx;
}
/******************************************************************
* Geometry shifters
******************************************************************/
function shiftPointsAttr(pointsStr, boxes) {
const nums = pointsStr.trim().replace(/,/g, " ").split(/\s+/).filter(Boolean).map(Number);
if (nums.length < 2 || nums.some(n => !Number.isFinite(n))) return pointsStr;
const out = [];
for (let i = 0; i < nums.length - 1; i += 2) {
const x = nums[i], y = nums[i + 1];
out.push(String(x + dxForPoint(x, y, boxes)), String(y));
}
return out.join(" ");
}
function tokenizePath(d) {
return d.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/g) || [];
}
function shiftPathD(d, boxes) {
const t = tokenizePath(d);
if (!t.length) return d;
let i = 0, cmd = "", x = 0, y = 0, x0 = 0, y0 = 0;
const out = [];
const isCmd = tok => /^[a-zA-Z]$/.test(tok);
const nextNum = () => Number(t[i++]);
const push = v => out.push(v);
const shiftAbs = (px, py) => [px + dxForPoint(px, py, boxes), py];
while (i < t.length) {
if (isCmd(t[i])) cmd = t[i++];
const rel = (cmd === cmd.toLowerCase());
const C = cmd.toUpperCase();
if (C === "M") {
push("M");
const px = nextNum(), py = nextNum();
const ax = rel ? x + px : px, ay = rel ? y + py : py;
const p = shiftAbs(ax, ay);
push(p[0]); push(p[1]);
x = ax; y = ay; x0 = ax; y0 = ay;
while (i < t.length && !isCmd(t[i])) {
push("L");
const lx = nextNum(), ly = nextNum();
const ax2 = rel ? x + lx : lx, ay2 = rel ? y + ly : ly;
const p2 = shiftAbs(ax2, ay2);
push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "L") {
push("L");
while (i < t.length && !isCmd(t[i])) {
const px = nextNum(), py = nextNum();
const ax = rel ? x + px : px, ay = rel ? y + py : py;
const p = shiftAbs(ax, ay);
push(p[0]); push(p[1]);
x = ax; y = ay;
}
continue;
}
if (C === "H") {
push("H");
while (i < t.length && !isCmd(t[i])) {
const px = nextNum();
const ax = rel ? x + px : px;
const p = shiftAbs(ax, y);
push(p[0]);
x = ax;
}
continue;
}
if (C === "V") {
push("V");
while (i < t.length && !isCmd(t[i])) {
const py = nextNum();
const ay = rel ? y + py : py;
push(ay);
y = ay;
}
continue;
}
if (C === "C") {
push("C");
while (i < t.length && !isCmd(t[i])) {
const x1 = nextNum(), y1 = nextNum(), x2 = nextNum(), y2 = nextNum(), x3 = nextNum(), y3 = nextNum();
const ax1 = rel ? x + x1 : x1, ay1 = rel ? y + y1 : y1;
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const ax3 = rel ? x + x3 : x3, ay3 = rel ? y + y3 : y3;
const p1 = shiftAbs(ax1, ay1);
const p2 = shiftAbs(ax2, ay2);
const p3 = shiftAbs(ax3, ay3);
push(p1[0]); push(p1[1]);
push(p2[0]); push(p2[1]);
push(p3[0]); push(p3[1]);
x = ax3; y = ay3;
}
continue;
}
if (C === "S") {
push("S");
while (i < t.length && !isCmd(t[i])) {
const x2 = nextNum(), y2 = nextNum(), x3 = nextNum(), y3 = nextNum();
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const ax3 = rel ? x + x3 : x3, ay3 = rel ? y + y3 : y3;
const p2 = shiftAbs(ax2, ay2);
const p3 = shiftAbs(ax3, ay3);
push(p2[0]); push(p2[1]);
push(p3[0]); push(p3[1]);
x = ax3; y = ay3;
}
continue;
}
if (C === "Q") {
push("Q");
while (i < t.length && !isCmd(t[i])) {
const x1 = nextNum(), y1 = nextNum(), x2 = nextNum(), y2 = nextNum();
const ax1 = rel ? x + x1 : x1, ay1 = rel ? y + y1 : y1;
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const p1 = shiftAbs(ax1, ay1);
const p2 = shiftAbs(ax2, ay2);
push(p1[0]); push(p1[1]);
push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "T") {
push("T");
while (i < t.length && !isCmd(t[i])) {
const x2 = nextNum(), y2 = nextNum();
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const p2 = shiftAbs(ax2, ay2);
push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "A") {
push("A");
while (i < t.length && !isCmd(t[i])) {
const rx = nextNum(), ry = nextNum(), rot = nextNum(), laf = nextNum(), sf = nextNum(), x2 = nextNum(), y2 = nextNum();
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const p2 = shiftAbs(ax2, ay2);
push(rx); push(ry); push(rot); push(laf); push(sf); push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "Z") {
push("Z");
x = x0; y = y0;
continue;
}
return d;
}
return out.join(" ");
}
function shiftRect(el, boxes) {
const x = Number(el.getAttribute("x"));
const y = Number(el.getAttribute("y"));
const w = Number(el.getAttribute("width"));
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(w)) return;
const h = Number(el.getAttribute("height"));
const yMid = Number.isFinite(h) ? (y + h / 2) : y;
const xL2 = x + dxForPoint(x, yMid, boxes);
const xR = x + w;
const xR2 = xR + dxForPoint(xR, yMid, boxes);
el.setAttribute("x", Math.min(xL2, xR2));
el.setAttribute("width", Math.abs(xR2 - xL2));
}
// ---- gradients ----
function ensureDefs(svg) {
let defs = svg.querySelector("defs");
if (!defs) {
defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
svg.insertBefore(defs, svg.firstChild);
}
return defs;
}
function parseFillUrl(fill) {
const m = String(fill).match(/^url\((["']?)#([^)'" ]+)\1\)$/);
return m ? m[2] : null;
}
function getNumericAttr(el, name) {
const v = el.getAttribute(name);
if (v == null) return null;
const n = Number(String(v).trim().replace(",", "."));
return Number.isFinite(n) ? n : null;
}
function isClearlyUserSpace(gradEl) {
const units = (gradEl.getAttribute("gradientUnits") || "").trim();
if (units === "userSpaceOnUse") return true;
if (units) return false;
for (const a of ["x1","y1","x2","y2","cx","cy","fx","fy","r"]) {
const n = getNumericAttr(gradEl, a);
if (n == null) continue;
if (n < -0.001 || n > 1.001) return true;
}
return false;
}
let gradCloneCounter = 0;
function ensureGradientClone(svg, el, baseGradId) {
const defs = ensureDefs(svg);
if (!el.dataset.gradCloneId) {
gradCloneCounter += 1;
el.dataset.gradCloneId = `${baseGradId}__clone__${gradCloneCounter}`;
}
const cloneId = el.dataset.gradCloneId;
let clone = defs.querySelector(`#${esc(cloneId)}`);
if (!clone) {
const orig = svg.querySelector(`#${esc(baseGradId)}`);
if (!orig) return null;
if (!isClearlyUserSpace(orig)) return null;
clone = orig.cloneNode(true);
clone.setAttribute("id", cloneId);
clone.setAttribute("gradientUnits", "userSpaceOnUse");
clone.setAttribute("data-base-gradient-transform", orig.getAttribute("gradientTransform") || "");
defs.appendChild(clone);
}
el.setAttribute("fill", `url(#${cloneId})`);
return clone;
}
function dxRepresentative(el, boxes) {
const tag = el.tagName.toLowerCase();
if (tag === "rect") {
const x = Number(el.getAttribute("x"));
const y = Number(el.getAttribute("y"));
const h = Number(el.getAttribute("height"));
const yMid = Number.isFinite(h) ? (y + h / 2) : y;
if (!Number.isFinite(x) || !Number.isFinite(yMid)) return 0;
return dxForPoint(x, yMid, boxes);
}
if (tag === "polygon" || tag === "polyline") {
const pts = (el.getAttribute("points") || "").trim().replace(/,/g, " ").split(/\s+/).filter(Boolean).map(Number);
if (pts.length < 2 || pts.some(n => !Number.isFinite(n))) return 0;
return dxForPoint(pts[0], pts[1], boxes);
}
if (tag === "path") {
const d = el.getAttribute("d") || "";
const nums = d.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/g);
if (!nums || nums.length < 2) return 0;
const x = Number(nums[0]), y = Number(nums[1]);
if (!Number.isFinite(x) || !Number.isFinite(y)) return 0;
return dxForPoint(x, y, boxes);
}
return 0;
}
function applyGradientShift(svg, el, dx) {
const fill = el.getAttribute("fill");
if (!fill) return;
const base = parseFillUrl(fill);
if (!base) return;
const clone = ensureGradientClone(svg, el, base);
if (!clone) return;
const baseT = (clone.getAttribute("data-base-gradient-transform") || "").trim();
const t = `translate(${dx},0)`;
clone.setAttribute("gradientTransform", baseT ? `${baseT} ${t}` : t);
}
function applyShiftToGroup(svg, g, boxes) {
if (!g || !boxes || !boxes.length) return;
const gradDx = new Map();
g.querySelectorAll("[fill^='url(']").forEach(el => {
const dx = dxRepresentative(el, boxes);
if (dx) gradDx.set(el, dx);
});
g.querySelectorAll("polygon").forEach(el => {
const p = el.getAttribute("points");
if (p) el.setAttribute("points", shiftPointsAttr(p, boxes));
});
g.querySelectorAll("polyline").forEach(el => {
const p = el.getAttribute("points");
if (p) el.setAttribute("points", shiftPointsAttr(p, boxes));
});
g.querySelectorAll("rect").forEach(el => shiftRect(el, boxes));
g.querySelectorAll("path").forEach(el => {
const d = el.getAttribute("d");
if (d) el.setAttribute("d", shiftPathD(d, boxes));
});
for (const [el, dx] of gradDx.entries()) applyGradientShift(svg, el, dx);
}
/******************************************************************
* SHIFT CALCS
******************************************************************/
// BU uses current n* fields (fallback to 5*)
function BU_LU(n) { return numField(`${n}`, "5", "LU"); }
function BU_TU(n, i) { return numField(`${n}`, "5", `TU${i}`); }
function BU_DMU(n, i) { return (numField(`${n}`, "5", `DMU${i}`) / SCALE); }
function buPrefixLeft(n, i) {
let x = BU_LU(n);
if (i >= 2) x += BU_DMU(n,1) + BU_TU(n,1);
if (i >= 3) x += BU_DMU(n,2) + BU_TU(n,2);
if (i >= 4) x += BU_DMU(n,3) + BU_TU(n,3);
if (i >= 5) x += BU_DMU(n,4) + BU_TU(n,4);
return x;
}
// ✅ dynamic sponning per vak, based on current VVU
function sponningBUDefault() {
return readInputValueById("form-field-BU_sponningbreedte") ?? 0;
}
function sponningWissel() {
return readInputValueById("form-field-wisselsponningbreedte") ?? 0;
}
function sponningDraaikiep() {
return readInputValueById("form-field-draaikiepsponningbreedte") ?? 0;
}
function isVVU_BI(raw) {
return raw === "dubbelglas binnen" || raw === "tripleglas binnen";
}
function isVVU_Draaikiep(raw) {
return raw === "draaikiep";
}
function sponningForVak(n, i) {
const raw = getVVU(n, i);
if (isVVU_BI(raw)) return sponningWissel();
if (isVVU_Draaikiep(raw)) return sponningDraaikiep();
return sponningBUDefault();
}
// HS uses current n* fields (fallback to 5*)
function HS_LU(n) { return numField(`${n}`, "5", "LU"); }
function HS_TU(n, i) { return numField(`${n}`, "5", `TU${i}`); }
function HS_DMU(n, i) { return (numField(`${n}`, "5", `DMU${i}`) / SCALE); } // scaled for positions
function hsPrefixLeft(n, i) {
let x = HS_LU(n);
if (i >= 2) x += HS_DMU(n,1) + HS_TU(n,1);
if (i >= 3) x += HS_DMU(n,2) + HS_TU(n,2);
if (i >= 4) x += HS_DMU(n,3) + HS_TU(n,3);
if (i >= 5) x += HS_DMU(n,4) + HS_TU(n,4);
return x;
}
function buildAndApplyShifts() {
const svg = svgRoot();
if (!svg) return;
hideNeverShow(svg);
const n = vakCount();
// ---- BU shiftboxes (vak-i-BU-shiftbox-L/R) with dynamic sponning
const buItems = [];
for (let i = 1; i <= n; i++) {
const gL = svg.querySelector(`#${esc(`vak-${i}-BU-shiftbox-L`)}`);
const gR = svg.querySelector(`#${esc(`vak-${i}-BU-shiftbox-R`)}`);
buItems.push({
key:`BU:${i}`,
gEl:gL,
exprNow: () => buPrefixLeft(n,i) - sponningForVak(n,i)
});
buItems.push({
key:`BU:${i}`,
gEl:gR,
exprNow: () => buPrefixLeft(n,i) + BU_DMU(n,i) + sponningForVak(n,i)
});
}
const buBoxes = buildBoxes(buItems);
// ---- HS shiftboxes (incl. mm-threshold corrections + verdiept nok-correcties)
const hsItems = [];
for (let i = 1; i <= n; i++) {
const left = () => hsPrefixLeft(n, i);
const w = () => HS_DMU(n, i); // scaled for positions
const dmuMm = () => numField(`${n}`, "5", `DMU${i}`); // raw mm for thresholds
const corr200 = () => (dmuMm() >= 200 ? 31.5 : 0); // for L/R
const corr400 = () => (dmuMm() >= 400 ? 47 : 0); // for L1/R1/T1/T3
// Determine HS mode for this vak
const rawVVU = getVVU(n, i);
const mode = VVU_TO_HS_MODE[normalizeKey(rawVVU)] || null;
// ✅ verdiept logic (scaled)
const vOn = () => isVerdieptVJa();
const sOn = () => isVerdieptSJa();
const nv = () => nokV();
const ns = () => nokS();
// LS/RS set (L, T, R)
// Rules:
// - LS + verdiept_v=ja => L - nokV
// - RS + verdiept_v=ja => R + nokV
// - LS + verdiept_s=ja => R + nokS
// - RS + verdiept_s=ja => L - nokS
const extraL = () => {
let dx = 0;
if (mode === "LS" && vOn()) dx += -nv();
if (mode === "RS" && sOn()) dx += -ns();
return dx;
};
const extraR = () => {
let dx = 0;
if (mode === "RS" && vOn()) dx += +nv();
if (mode === "LS" && sOn()) dx += +ns();
return dx;
};
hsItems.push({
key:`HS_LTR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-L`)}`),
exprNow: () => left() + corr200() + extraL()
});
hsItems.push({
key:`HS_LTR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-T`)}`),
exprNow: () => left() + (w()/4)*2
});
hsItems.push({
key:`HS_LTR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-R`)}`),
exprNow: () => left() + w() - corr200() + extraR()
});
// LR set (L1, T1..T3, R1)
// Rule:
// - LR + verdiept_v=ja => L1 - nokV, R1 + nokV
const extraL1 = () => (mode === "LR" && vOn()) ? (-nv()) : 0;
const extraR1 = () => (mode === "LR" && vOn()) ? (+nv()) : 0;
hsItems.push({
key:`HS_LR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-L1`)}`),
exprNow: () => left() + corr400() + extraL1()
});
hsItems.push({
key:`HS_LR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-T1`)}`),
exprNow: () => left() + (w()/4)*1 + (corr400()/2)
});
hsItems.push({
key:`HS_LR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-T2`)}`),
exprNow: () => left() + (w()/4)*2
});
hsItems.push({
key:`HS_LR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-T3`)}`),
exprNow: () => left() + (w()/4)*3 - (corr400()/2)
});
hsItems.push({
key:`HS_LR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-R1`)}`),
exprNow: () => left() + w() - corr400() + extraR1()
});
}
const hsBoxes = buildBoxes(hsItems);
// Apply BU geometry to both BU buckets (BU-main + BU-BI)
for (let i = 1; i <= n; i++) {
const boxes = buBoxes.get(`BU:${i}`) || [];
if (!boxes.length) continue;
for (const suf of BU_MAIN_SUFFIXES) {
const g = svg.querySelector(`#${esc(`vak-${i}-BU-${suf}`)}`);
if (g) applyShiftToGroup(svg, g, boxes);
}
for (const suf of BU_BI_SUFFIXES) {
const g = svg.querySelector(`#${esc(`vak-${i}-BU-${suf}`)}`);
if (g) applyShiftToGroup(svg, g, boxes);
}
}
// Apply HS geometry
for (let i = 1; i <= n; i++) {
const bLTR = hsBoxes.get(`HS_LTR:${i}`) || [];
const bLR = hsBoxes.get(`HS_LR:${i}`) || [];
if (bLTR.length) {
const gLS = svg.querySelector(`#${esc(`vak-${i}-HS-LS`)}`);
const gRS = svg.querySelector(`#${esc(`vak-${i}-HS-RS`)}`);
if (gLS) applyShiftToGroup(svg, gLS, bLTR);
if (gRS) applyShiftToGroup(svg, gRS, bLTR);
}
if (bLR.length) {
const gLR = svg.querySelector(`#${esc(`vak-${i}-HS-LR`)}`);
if (gLR) applyShiftToGroup(svg, gLR, bLR);
}
}
// Move shiftboxes (remain hidden)
moveBoxes(buBoxes);
moveBoxes(hsBoxes);
hideNeverShow(svg);
}
/******************************************************************
* Scheduling
******************************************************************/
let updateQueued = false;
let updateDeferredQueued = false;
function runUpdateAll() {
updateQueued = false;
updateDeferredQueued = false;
updateVisibility();
buildAndApplyShifts();
}
function scheduleUpdateAll() {
if (updateQueued) return;
updateQueued = true;
queueMicrotask(runUpdateAll);
}
function scheduleUpdateAllAfterPaint() {
if (updateDeferredQueued) return;
updateDeferredQueued = true;
requestAnimationFrame(() => {
setTimeout(() => {
runUpdateAll();
}, 0);
});
}
function updateAll() {
scheduleUpdateAll();
}
/******************************************************************
* Bind listeners
******************************************************************/
function bind(root = document) {
// VVU selects
root.querySelectorAll(`select[name^="form_fields["][name*="VVU"]`).forEach(el => {
if (el.dataset.avBound === "1") return;
el.dataset.avBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// all form-field inputs (includes: BU_sponningbreedte, wisselsponningbreedte, draaikiepsponningbreedte, DMU, TU, LU, nokdiktes, etc.)
root.querySelectorAll(`input[id^="form-field-"]`).forEach(el => {
if (el.dataset.avShiftBound === "1") return;
el.dataset.avShiftBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// vakken radio
root.querySelectorAll(`input[type="radio"][name="form_fields[vakken]"]`).forEach(el => {
if (el.dataset.avVakkenBound === "1") return;
el.dataset.avVakkenBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// dorpeltype radio
root.querySelectorAll(`input[type="radio"][name="form_fields[dorpeltype]"]`).forEach(el => {
if (el.dataset.avDorpeltypeBound === "1") return;
el.dataset.avDorpeltypeBound = "1";
el.addEventListener("change", scheduleUpdateAllAfterPaint);
el.addEventListener("input", scheduleUpdateAllAfterPaint);
});
// ✅ verdiept radios (extra expliciet)
root.querySelectorAll(`input[type="radio"][name="form_fields[verdiept_v]"], input[type="radio"][name="form_fields[verdiept_s]"]`).forEach(el => {
if (el.dataset.avVerdieptBound === "1") return;
el.dataset.avVerdieptBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// ✅ Explicit (requested earlier) listeners – redundant but harmless
["form-field-wisselsponningbreedte","form-field-draaikiepsponningbreedte"].forEach(id => {
const el = document.getElementById(id);
if (!el || el.dataset.avExplicitSponningBound === "1") return;
el.dataset.avExplicitSponningBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
}
function boot() {
bind(document);
runUpdateAll();
const mo = new MutationObserver(muts => {
let hasNewHtml = false;
for (const mu of muts) {
mu.addedNodes.forEach(node => {
if (!(node instanceof HTMLElement)) return;
hasNewHtml = true;
bind(node);
});
}
if (hasNewHtml) {
updateAll();
}
});
mo.observe(document.documentElement, { childList: true, subtree: true });
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
else boot();
// Optional: rebase if other scripts set a new "nulstand"
window.rebaseAlleVakken = () => {
const svg = svgRoot();
if (!svg) return;
svg.querySelectorAll(`[id*="-shiftbox-"] rect`).forEach(r => {
r.dataset.baseX = String(Number(r.getAttribute("x") ?? "0"));
delete r.dataset.baseExpr;
});
runUpdateAll();
};
})();
//// VAKVULLING BU/HS #ALLE-VAKKEN ////
(() => {
const SCALE = 3.84615384615;
const SVG_ID = "alle-vakken";
// BU mapping (VVU -> BU variant)
const VVU_TO_BU_SUFFIX = {
"dubbelglas": "2glas-BU",
"tripleglas": "3glas-BU",
"paneel": "paneel",
"glaspaneel": "glaspaneel",
"deur": "deur",
"kaderdeur": "kaderdeur",
"dubbelkaderdeur": "dubbelkaderdeur",
// ✅ deze 3 blijven in mapping, maar worden geometrisch als BU-BI bucket behandeld
"dubbelglas binnen": "2glas-BI",
"tripleglas binnen": "3glas-BI",
"draaikiep": "draaikiep",
"vul in": null,
};
// HS mapping (HS-opties zitten óók in VVU)
const VVU_TO_HS_MODE = {
"linksschuivend": "LS",
"rechtsschuivend": "RS",
"linksrechtsschuivend": "LR",
"ls": "LS",
"rs": "RS",
"lr": "LR",
};
// ✅ BU split: BU-main vs BU-BI
const BU_MAIN_SUFFIXES = ["2glas-BU","3glas-BU","paneel","glaspaneel","deur","kaderdeur","dubbelkaderdeur"];
const BU_BI_SUFFIXES = ["2glas-BI","3glas-BI","draaikiep"];
const HS_MODES = ["LS","RS","LR"];
const esc = (s) => CSS.escape(s);
function svgRoot() {
return document.querySelector(`svg#${esc(SVG_ID)}`);
}
function setDisp(el, v) { if (el) el.setAttribute("display", v); }
function normalizeKey(v) {
return String(v || "")
.trim()
.toLowerCase()
.replace(/\s+/g, "")
.replace(/_/g, "")
.replace(/-/g, "");
}
function vakCount() {
const checked = document.querySelector('input[type="radio"][name="form_fields[vakken]"]:checked');
if (checked && checked.value) {
const m = String(checked.value).match(/([1-5])/);
if (m) return Number(m[1]);
}
return 5;
}
function readInputValueById(id) {
const el = document.getElementById(id);
if (!el) return null;
const v = (el.value ?? "").toString().trim().replace(",", ".");
const n = Number(v);
return Number.isFinite(n) ? n : 0;
}
// Prefer prefixA, else prefixB, else 0
function numField(prefixA, prefixB, suffix) {
const a = readInputValueById(`form-field-${prefixA}${suffix}`);
if (a != null) return a;
const b = readInputValueById(`form-field-${prefixB}${suffix}`);
if (b != null) return b;
return 0;
}
// VVU select by name form_fields[VVU]
function getVVU(n, i) {
const sel = document.querySelector(`select[name="form_fields[${n}VVU${i}]"]`);
return sel ? String(sel.value || "") : "vul in";
}
// ✅ Dorpeltype radio (pakt liefst data-original-value, anders value)
function getDorpelType() {
const checked = document.querySelector('input[type="radio"][name="form_fields[dorpeltype]"]:checked');
if (!checked) return null;
return (checked.getAttribute("data-original-value") || checked.value || "").toString().trim();
}
/******************************************************************
* ✅ NEW: HS verdiept (nok) inputs
******************************************************************/
function getRadioValueByName(name) {
const el = document.querySelector(`input[type="radio"][name="${name}"]:checked`);
return el ? String(el.value || "").trim().toLowerCase() : "";
}
function isVerdieptVJa() {
return getRadioValueByName("form_fields[verdiept_v]") === "ja";
}
function isVerdieptSJa() {
return getRadioValueByName("form_fields[verdiept_s]") === "ja";
}
// ✅ optie A: nokdiktes zijn mm -> schalen naar SVG-units
function nokV() {
return (readInputValueById("form-field-nokdiktevast") ?? 0);
}
function nokS() {
return (readInputValueById("form-field-nokdikteschuif") ?? 0);
}
/******************************************************************
* NEVER SHOW elements (shiftboxes + BU-LR placeholders)
******************************************************************/
function hideNeverShow(svg) {
// BU-LR placeholders
for (let i = 1; i <= 5; i++) {
setDisp(svg.querySelector(`#${esc(`vak-${i}-BU-LR`)}`), "none");
}
// All BU shiftboxes
svg.querySelectorAll(`[id*="-BU-shiftbox-"]`).forEach(el => setDisp(el, "none"));
// All HS shiftboxes
svg.querySelectorAll(`[id*="-HS-shiftbox-"]`).forEach(el => setDisp(el, "none"));
}
/******************************************************************
* VISIBILITY: only whitelist BU & HS groups
******************************************************************/
function showGroupAndNestedGs(groupEl) {
if (!groupEl) return;
const deny = (id) => {
const s = String(id || "");
return s.includes("-shiftbox-") || s.endsWith("-BU-LR") || s.includes("-BU-LR-");
};
if (!deny(groupEl.id)) setDisp(groupEl, "block");
groupEl.querySelectorAll("g").forEach(g => {
if (deny(g.id)) setDisp(g, "none");
else setDisp(g, "block");
});
}
function hideAllBU(svg, i) {
for (const suf of BU_MAIN_SUFFIXES) setDisp(svg.querySelector(`#${esc(`vak-${i}-BU-${suf}`)}`), "none");
for (const suf of BU_BI_SUFFIXES) setDisp(svg.querySelector(`#${esc(`vak-${i}-BU-${suf}`)}`), "none");
}
function hideAllHS(svg, i) {
for (const m of HS_MODES) setDisp(svg.querySelector(`#${esc(`vak-${i}-HS-${m}`)}`), "none");
}
function updateVisibility() {
const svg = svgRoot();
if (!svg) return;
hideNeverShow(svg);
const n = vakCount();
// reset only whitelisted
for (let i = 1; i <= 5; i++) {
hideAllBU(svg, i);
hideAllHS(svg, i);
}
for (let i = 1; i <= n; i++) {
const raw = getVVU(n, i);
const key = normalizeKey(raw);
const hsMode = VVU_TO_HS_MODE[key] || null;
let buSuffix = Object.prototype.hasOwnProperty.call(VVU_TO_BU_SUFFIX, raw)
? VVU_TO_BU_SUFFIX[raw]
: null;
// ✅ extra optie: als dorpeltype = KVTPKU, dan kaderdeur -> dubbelkaderdeur
const dorpelType = getDorpelType();
if (dorpelType === "KVTPKU" && raw === "kaderdeur") {
buSuffix = "dubbelkaderdeur";
}
if (hsMode) {
const g = svg.querySelector(`#${esc(`vak-${i}-HS-${hsMode}`)}`);
showGroupAndNestedGs(g);
} else if (buSuffix) {
const g = svg.querySelector(`#${esc(`vak-${i}-BU-${buSuffix}`)}`);
showGroupAndNestedGs(g);
}
}
hideNeverShow(svg);
}
/******************************************************************
* SHIFTBOX ENGINE (delta vs baseline)
******************************************************************/
function desiredXFromDelta(rect, exprNow) {
const curX = Number(rect.getAttribute("x") ?? "0");
if (rect.dataset.baseX == null) rect.dataset.baseX = String(curX);
if (rect.dataset.baseExpr == null) rect.dataset.baseExpr = String(exprNow);
const baseX = Number(rect.dataset.baseX);
const baseExpr = Number(rect.dataset.baseExpr);
return baseX + (exprNow - baseExpr);
}
function boxFromShiftRect(rect, exprNow) {
const curX = Number(rect.getAttribute("x") ?? "0");
const curY = Number(rect.getAttribute("y") ?? "0");
const w = Number(rect.getAttribute("width") ?? "0");
const h = Number(rect.getAttribute("height") ?? "0");
const desiredX = desiredXFromDelta(rect, exprNow);
return {
rect,
desiredX,
dxInc: desiredX - curX,
minX: curX, maxX: curX + w,
minY: curY, maxY: curY + h,
};
}
function buildBoxes(items) {
const map = new Map(); // key -> boxes[]
for (const it of items) {
if (!it.gEl) continue;
const r = it.gEl.querySelector("rect");
if (!r) continue;
const b = boxFromShiftRect(r, it.exprNow());
if (!map.has(it.key)) map.set(it.key, []);
map.get(it.key).push(b);
}
return map;
}
function moveBoxes(map) {
for (const boxes of map.values()) {
for (const b of boxes) b.rect.setAttribute("x", b.desiredX);
}
}
function dxForPoint(x, y, boxes) {
let dx = 0;
for (const b of boxes) {
if (x >= b.minX && x <= b.maxX && y >= b.minY && y <= b.maxY) dx += b.dxInc;
}
return dx;
}
/******************************************************************
* Geometry shifters
******************************************************************/
function shiftPointsAttr(pointsStr, boxes) {
const nums = pointsStr.trim().replace(/,/g, " ").split(/\s+/).filter(Boolean).map(Number);
if (nums.length < 2 || nums.some(n => !Number.isFinite(n))) return pointsStr;
const out = [];
for (let i = 0; i < nums.length - 1; i += 2) {
const x = nums[i], y = nums[i + 1];
out.push(String(x + dxForPoint(x, y, boxes)), String(y));
}
return out.join(" ");
}
function tokenizePath(d) {
return d.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/g) || [];
}
function shiftPathD(d, boxes) {
const t = tokenizePath(d);
if (!t.length) return d;
let i = 0, cmd = "", x = 0, y = 0, x0 = 0, y0 = 0;
const out = [];
const isCmd = tok => /^[a-zA-Z]$/.test(tok);
const nextNum = () => Number(t[i++]);
const push = v => out.push(v);
const shiftAbs = (px, py) => [px + dxForPoint(px, py, boxes), py];
while (i < t.length) {
if (isCmd(t[i])) cmd = t[i++];
const rel = (cmd === cmd.toLowerCase());
const C = cmd.toUpperCase();
if (C === "M") {
push("M");
const px = nextNum(), py = nextNum();
const ax = rel ? x + px : px, ay = rel ? y + py : py;
const p = shiftAbs(ax, ay);
push(p[0]); push(p[1]);
x = ax; y = ay; x0 = ax; y0 = ay;
while (i < t.length && !isCmd(t[i])) {
push("L");
const lx = nextNum(), ly = nextNum();
const ax2 = rel ? x + lx : lx, ay2 = rel ? y + ly : ly;
const p2 = shiftAbs(ax2, ay2);
push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "L") {
push("L");
while (i < t.length && !isCmd(t[i])) {
const px = nextNum(), py = nextNum();
const ax = rel ? x + px : px, ay = rel ? y + py : py;
const p = shiftAbs(ax, ay);
push(p[0]); push(p[1]);
x = ax; y = ay;
}
continue;
}
if (C === "H") {
push("H");
while (i < t.length && !isCmd(t[i])) {
const px = nextNum();
const ax = rel ? x + px : px;
const p = shiftAbs(ax, y);
push(p[0]);
x = ax;
}
continue;
}
if (C === "V") {
push("V");
while (i < t.length && !isCmd(t[i])) {
const py = nextNum();
const ay = rel ? y + py : py;
push(ay);
y = ay;
}
continue;
}
if (C === "C") {
push("C");
while (i < t.length && !isCmd(t[i])) {
const x1 = nextNum(), y1 = nextNum(), x2 = nextNum(), y2 = nextNum(), x3 = nextNum(), y3 = nextNum();
const ax1 = rel ? x + x1 : x1, ay1 = rel ? y + y1 : y1;
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const ax3 = rel ? x + x3 : x3, ay3 = rel ? y + y3 : y3;
const p1 = shiftAbs(ax1, ay1);
const p2 = shiftAbs(ax2, ay2);
const p3 = shiftAbs(ax3, ay3);
push(p1[0]); push(p1[1]);
push(p2[0]); push(p2[1]);
push(p3[0]); push(p3[1]);
x = ax3; y = ay3;
}
continue;
}
if (C === "S") {
push("S");
while (i < t.length && !isCmd(t[i])) {
const x2 = nextNum(), y2 = nextNum(), x3 = nextNum(), y3 = nextNum();
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const ax3 = rel ? x + x3 : x3, ay3 = rel ? y + y3 : y3;
const p2 = shiftAbs(ax2, ay2);
const p3 = shiftAbs(ax3, ay3);
push(p2[0]); push(p2[1]);
push(p3[0]); push(p3[1]);
x = ax3; y = ay3;
}
continue;
}
if (C === "Q") {
push("Q");
while (i < t.length && !isCmd(t[i])) {
const x1 = nextNum(), y1 = nextNum(), x2 = nextNum(), y2 = nextNum();
const ax1 = rel ? x + x1 : x1, ay1 = rel ? y + y1 : y1;
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const p1 = shiftAbs(ax1, ay1);
const p2 = shiftAbs(ax2, ay2);
push(p1[0]); push(p1[1]);
push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "T") {
push("T");
while (i < t.length && !isCmd(t[i])) {
const x2 = nextNum(), y2 = nextNum();
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const p2 = shiftAbs(ax2, ay2);
push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "A") {
push("A");
while (i < t.length && !isCmd(t[i])) {
const rx = nextNum(), ry = nextNum(), rot = nextNum(), laf = nextNum(), sf = nextNum(), x2 = nextNum(), y2 = nextNum();
const ax2 = rel ? x + x2 : x2, ay2 = rel ? y + y2 : y2;
const p2 = shiftAbs(ax2, ay2);
push(rx); push(ry); push(rot); push(laf); push(sf); push(p2[0]); push(p2[1]);
x = ax2; y = ay2;
}
continue;
}
if (C === "Z") {
push("Z");
x = x0; y = y0;
continue;
}
return d;
}
return out.join(" ");
}
function shiftRect(el, boxes) {
const x = Number(el.getAttribute("x"));
const y = Number(el.getAttribute("y"));
const w = Number(el.getAttribute("width"));
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(w)) return;
const h = Number(el.getAttribute("height"));
const yMid = Number.isFinite(h) ? (y + h / 2) : y;
const xL2 = x + dxForPoint(x, yMid, boxes);
const xR = x + w;
const xR2 = xR + dxForPoint(xR, yMid, boxes);
el.setAttribute("x", Math.min(xL2, xR2));
el.setAttribute("width", Math.abs(xR2 - xL2));
}
// ---- gradients ----
function ensureDefs(svg) {
let defs = svg.querySelector("defs");
if (!defs) {
defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
svg.insertBefore(defs, svg.firstChild);
}
return defs;
}
function parseFillUrl(fill) {
const m = String(fill).match(/^url\((["']?)#([^)'" ]+)\1\)$/);
return m ? m[2] : null;
}
function getNumericAttr(el, name) {
const v = el.getAttribute(name);
if (v == null) return null;
const n = Number(String(v).trim().replace(",", "."));
return Number.isFinite(n) ? n : null;
}
function isClearlyUserSpace(gradEl) {
const units = (gradEl.getAttribute("gradientUnits") || "").trim();
if (units === "userSpaceOnUse") return true;
if (units) return false;
for (const a of ["x1","y1","x2","y2","cx","cy","fx","fy","r"]) {
const n = getNumericAttr(gradEl, a);
if (n == null) continue;
if (n < -0.001 || n > 1.001) return true;
}
return false;
}
let gradCloneCounter = 0;
function ensureGradientClone(svg, el, baseGradId) {
const defs = ensureDefs(svg);
if (!el.dataset.gradCloneId) {
gradCloneCounter += 1;
el.dataset.gradCloneId = `${baseGradId}__clone__${gradCloneCounter}`;
}
const cloneId = el.dataset.gradCloneId;
let clone = defs.querySelector(`#${esc(cloneId)}`);
if (!clone) {
const orig = svg.querySelector(`#${esc(baseGradId)}`);
if (!orig) return null;
if (!isClearlyUserSpace(orig)) return null;
clone = orig.cloneNode(true);
clone.setAttribute("id", cloneId);
clone.setAttribute("gradientUnits", "userSpaceOnUse");
clone.setAttribute("data-base-gradient-transform", orig.getAttribute("gradientTransform") || "");
defs.appendChild(clone);
}
el.setAttribute("fill", `url(#${cloneId})`);
return clone;
}
function dxRepresentative(el, boxes) {
const tag = el.tagName.toLowerCase();
if (tag === "rect") {
const x = Number(el.getAttribute("x"));
const y = Number(el.getAttribute("y"));
const h = Number(el.getAttribute("height"));
const yMid = Number.isFinite(h) ? (y + h / 2) : y;
if (!Number.isFinite(x) || !Number.isFinite(yMid)) return 0;
return dxForPoint(x, yMid, boxes);
}
if (tag === "polygon" || tag === "polyline") {
const pts = (el.getAttribute("points") || "").trim().replace(/,/g, " ").split(/\s+/).filter(Boolean).map(Number);
if (pts.length < 2 || pts.some(n => !Number.isFinite(n))) return 0;
return dxForPoint(pts[0], pts[1], boxes);
}
if (tag === "path") {
const d = el.getAttribute("d") || "";
const nums = d.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/g);
if (!nums || nums.length < 2) return 0;
const x = Number(nums[0]), y = Number(nums[1]);
if (!Number.isFinite(x) || !Number.isFinite(y)) return 0;
return dxForPoint(x, y, boxes);
}
return 0;
}
function applyGradientShift(svg, el, dx) {
const fill = el.getAttribute("fill");
if (!fill) return;
const base = parseFillUrl(fill);
if (!base) return;
const clone = ensureGradientClone(svg, el, base);
if (!clone) return;
const baseT = (clone.getAttribute("data-base-gradient-transform") || "").trim();
const t = `translate(${dx},0)`;
clone.setAttribute("gradientTransform", baseT ? `${baseT} ${t}` : t);
}
function applyShiftToGroup(svg, g, boxes) {
if (!g || !boxes || !boxes.length) return;
const gradDx = new Map();
g.querySelectorAll("[fill^='url(']").forEach(el => {
const dx = dxRepresentative(el, boxes);
if (dx) gradDx.set(el, dx);
});
g.querySelectorAll("polygon").forEach(el => {
const p = el.getAttribute("points");
if (p) el.setAttribute("points", shiftPointsAttr(p, boxes));
});
g.querySelectorAll("polyline").forEach(el => {
const p = el.getAttribute("points");
if (p) el.setAttribute("points", shiftPointsAttr(p, boxes));
});
g.querySelectorAll("rect").forEach(el => shiftRect(el, boxes));
g.querySelectorAll("path").forEach(el => {
const d = el.getAttribute("d");
if (d) el.setAttribute("d", shiftPathD(d, boxes));
});
for (const [el, dx] of gradDx.entries()) applyGradientShift(svg, el, dx);
}
/******************************************************************
* SHIFT CALCS
******************************************************************/
// BU uses current n* fields (fallback to 5*)
function BU_LU(n) { return numField(`${n}`, "5", "LU"); }
function BU_TU(n, i) { return numField(`${n}`, "5", `TU${i}`); }
function BU_DMU(n, i) { return (numField(`${n}`, "5", `DMU${i}`) / SCALE); }
function buPrefixLeft(n, i) {
let x = BU_LU(n);
if (i >= 2) x += BU_DMU(n,1) + BU_TU(n,1);
if (i >= 3) x += BU_DMU(n,2) + BU_TU(n,2);
if (i >= 4) x += BU_DMU(n,3) + BU_TU(n,3);
if (i >= 5) x += BU_DMU(n,4) + BU_TU(n,4);
return x;
}
// ✅ dynamic sponning per vak, based on current VVU
function sponningBUDefault() {
return readInputValueById("form-field-BU_sponningbreedte") ?? 0;
}
function sponningWissel() {
return readInputValueById("form-field-wisselsponningbreedte") ?? 0;
}
function sponningDraaikiep() {
return readInputValueById("form-field-draaikiepsponningbreedte") ?? 0;
}
function isVVU_BI(raw) {
return raw === "dubbelglas binnen" || raw === "tripleglas binnen";
}
function isVVU_Draaikiep(raw) {
return raw === "draaikiep";
}
function sponningForVak(n, i) {
const raw = getVVU(n, i);
if (isVVU_BI(raw)) return sponningWissel();
if (isVVU_Draaikiep(raw)) return sponningDraaikiep();
return sponningBUDefault();
}
// HS uses current n* fields (fallback to 5*)
function HS_LU(n) { return numField(`${n}`, "5", "LU"); }
function HS_TU(n, i) { return numField(`${n}`, "5", `TU${i}`); }
function HS_DMU(n, i) { return (numField(`${n}`, "5", `DMU${i}`) / SCALE); } // scaled for positions
function hsPrefixLeft(n, i) {
let x = HS_LU(n);
if (i >= 2) x += HS_DMU(n,1) + HS_TU(n,1);
if (i >= 3) x += HS_DMU(n,2) + HS_TU(n,2);
if (i >= 4) x += HS_DMU(n,3) + HS_TU(n,3);
if (i >= 5) x += HS_DMU(n,4) + HS_TU(n,4);
return x;
}
function buildAndApplyShifts() {
const svg = svgRoot();
if (!svg) return;
hideNeverShow(svg);
const n = vakCount();
// ---- BU shiftboxes (vak-i-BU-shiftbox-L/R) with dynamic sponning
const buItems = [];
for (let i = 1; i <= n; i++) {
const gL = svg.querySelector(`#${esc(`vak-${i}-BU-shiftbox-L`)}`);
const gR = svg.querySelector(`#${esc(`vak-${i}-BU-shiftbox-R`)}`);
buItems.push({
key:`BU:${i}`,
gEl:gL,
exprNow: () => buPrefixLeft(n,i) - sponningForVak(n,i)
});
buItems.push({
key:`BU:${i}`,
gEl:gR,
exprNow: () => buPrefixLeft(n,i) + BU_DMU(n,i) + sponningForVak(n,i)
});
}
const buBoxes = buildBoxes(buItems);
// ---- HS shiftboxes (incl. mm-threshold corrections + verdiept nok-correcties)
const hsItems = [];
for (let i = 1; i <= n; i++) {
const left = () => hsPrefixLeft(n, i);
const w = () => HS_DMU(n, i); // scaled for positions
const dmuMm = () => numField(`${n}`, "5", `DMU${i}`); // raw mm for thresholds
const corr200 = () => (dmuMm() >= 200 ? 31.5 : 0); // for L/R
const corr400 = () => (dmuMm() >= 400 ? 47 : 0); // for L1/R1/T1/T3
// Determine HS mode for this vak
const rawVVU = getVVU(n, i);
const mode = VVU_TO_HS_MODE[normalizeKey(rawVVU)] || null;
// ✅ verdiept logic (scaled)
const vOn = () => isVerdieptVJa();
const sOn = () => isVerdieptSJa();
const nv = () => nokV();
const ns = () => nokS();
// LS/RS set (L, T, R)
// Rules:
// - LS + verdiept_v=ja => L - nokV
// - RS + verdiept_v=ja => R + nokV
// - LS + verdiept_s=ja => R + nokS
// - RS + verdiept_s=ja => L - nokS
const extraL = () => {
let dx = 0;
if (mode === "LS" && vOn()) dx += -nv();
if (mode === "RS" && sOn()) dx += -ns();
return dx;
};
const extraR = () => {
let dx = 0;
if (mode === "RS" && vOn()) dx += +nv();
if (mode === "LS" && sOn()) dx += +ns();
return dx;
};
hsItems.push({
key:`HS_LTR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-L`)}`),
exprNow: () => left() + corr200() + extraL()
});
hsItems.push({
key:`HS_LTR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-T`)}`),
exprNow: () => left() + (w()/4)*2
});
hsItems.push({
key:`HS_LTR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-R`)}`),
exprNow: () => left() + w() - corr200() + extraR()
});
// LR set (L1, T1..T3, R1)
// Rule:
// - LR + verdiept_v=ja => L1 - nokV, R1 + nokV
const extraL1 = () => (mode === "LR" && vOn()) ? (-nv()) : 0;
const extraR1 = () => (mode === "LR" && vOn()) ? (+nv()) : 0;
hsItems.push({
key:`HS_LR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-L1`)}`),
exprNow: () => left() + corr400() + extraL1()
});
hsItems.push({
key:`HS_LR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-T1`)}`),
exprNow: () => left() + (w()/4)*1 + (corr400()/2)
});
hsItems.push({
key:`HS_LR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-T2`)}`),
exprNow: () => left() + (w()/4)*2
});
hsItems.push({
key:`HS_LR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-T3`)}`),
exprNow: () => left() + (w()/4)*3 - (corr400()/2)
});
hsItems.push({
key:`HS_LR:${i}`,
gEl: svg.querySelector(`#${esc(`vak-${i}-HS-shiftbox-R1`)}`),
exprNow: () => left() + w() - corr400() + extraR1()
});
}
const hsBoxes = buildBoxes(hsItems);
// Apply BU geometry to both BU buckets (BU-main + BU-BI)
for (let i = 1; i <= n; i++) {
const boxes = buBoxes.get(`BU:${i}`) || [];
if (!boxes.length) continue;
for (const suf of BU_MAIN_SUFFIXES) {
const g = svg.querySelector(`#${esc(`vak-${i}-BU-${suf}`)}`);
if (g) applyShiftToGroup(svg, g, boxes);
}
for (const suf of BU_BI_SUFFIXES) {
const g = svg.querySelector(`#${esc(`vak-${i}-BU-${suf}`)}`);
if (g) applyShiftToGroup(svg, g, boxes);
}
}
// Apply HS geometry
for (let i = 1; i <= n; i++) {
const bLTR = hsBoxes.get(`HS_LTR:${i}`) || [];
const bLR = hsBoxes.get(`HS_LR:${i}`) || [];
if (bLTR.length) {
const gLS = svg.querySelector(`#${esc(`vak-${i}-HS-LS`)}`);
const gRS = svg.querySelector(`#${esc(`vak-${i}-HS-RS`)}`);
if (gLS) applyShiftToGroup(svg, gLS, bLTR);
if (gRS) applyShiftToGroup(svg, gRS, bLTR);
}
if (bLR.length) {
const gLR = svg.querySelector(`#${esc(`vak-${i}-HS-LR`)}`);
if (gLR) applyShiftToGroup(svg, gLR, bLR);
}
}
// Move shiftboxes (remain hidden)
moveBoxes(buBoxes);
moveBoxes(hsBoxes);
hideNeverShow(svg);
}
function updateAll() {
updateVisibility();
buildAndApplyShifts();
}
/******************************************************************
* Bind listeners
******************************************************************/
function bind(root = document) {
// VVU selects
root.querySelectorAll(`select[name^="form_fields["][name*="VVU"]`).forEach(el => {
if (el.dataset.avBound === "1") return;
el.dataset.avBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// all form-field inputs (includes: BU_sponningbreedte, wisselsponningbreedte, draaikiepsponningbreedte, DMU, TU, LU, nokdiktes, etc.)
root.querySelectorAll(`input[id^="form-field-"]`).forEach(el => {
if (el.dataset.avShiftBound === "1") return;
el.dataset.avShiftBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// vakken radio
root.querySelectorAll(`input[type="radio"][name="form_fields[vakken]"]`).forEach(el => {
if (el.dataset.avVakkenBound === "1") return;
el.dataset.avVakkenBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// dorpeltype radio
root.querySelectorAll(`input[type="radio"][name="form_fields[dorpeltype]"]`).forEach(el => {
if (el.dataset.avDorpeltypeBound === "1") return;
el.dataset.avDorpeltypeBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// ✅ verdiept radios (extra expliciet)
root.querySelectorAll(`input[type="radio"][name="form_fields[verdiept_v]"], input[type="radio"][name="form_fields[verdiept_s]"]`).forEach(el => {
if (el.dataset.avVerdieptBound === "1") return;
el.dataset.avVerdieptBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
// ✅ Explicit (requested earlier) listeners – redundant but harmless
["form-field-wisselsponningbreedte","form-field-draaikiepsponningbreedte"].forEach(id => {
const el = document.getElementById(id);
if (!el || el.dataset.avExplicitSponningBound === "1") return;
el.dataset.avExplicitSponningBound = "1";
el.addEventListener("change", updateAll);
el.addEventListener("input", updateAll);
});
}
function boot() {
bind(document);
updateAll();
const mo = new MutationObserver(muts => {
for (const mu of muts) {
mu.addedNodes.forEach(node => {
if (!(node instanceof HTMLElement)) return;
bind(node);
});
}
updateAll();
});
mo.observe(document.documentElement, { childList: true, subtree: true });
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
else boot();
// Optional: rebase if other scripts set a new "nulstand"
window.rebaseAlleVakken = () => {
const svg = svgRoot();
if (!svg) return;
svg.querySelectorAll(`[id*="-shiftbox-"] rect`).forEach(r => {
r.dataset.baseX = String(Number(r.getAttribute("x") ?? "0"));
delete r.dataset.baseExpr;
});
updateAll();
};
})();
//// HS — VASTE DEURDIKTE #DORPEL ////
(() => {
if (window.__HS_VASTL_V12_GEOM__) return;
window.__HS_VASTL_V12_GEOM__ = true;
const DEURDIKTE_ID = 'form-field-deurdikte';
const TUSSENMAAT_ID = 'form-field-hstussenmaat';
const SHIFTBOX_ID = 'x-shiftbox-vast-L';
const DOOR_GROUP_ID = 'HS-vastedeur';
// ===== Schaalfactoren =====
const K_DEURDIKTE_RECT = -1.5; // +1 deurdikte => shiftbox x -= 1.5
const K_DEURDIKTE_PATH = -1.0; // +1 deurdikte => deurvulling punten (in box) x -= 1.0
const K_TUSSENMAAT_RECT = -1.0; // +1 tussenmaat => shiftbox x -= 1.0 (deurvulling: 0)
const toNum = (v) => {
const n = Number(v);
return Number.isFinite(n) ? n : null;
};
const clamp = (n, min, max) => {
if (!Number.isFinite(n)) return null;
if (Number.isFinite(min)) n = Math.max(min, n);
if (Number.isFinite(max)) n = Math.min(max, n);
return n;
};
// ---------- SVG targeting ----------
function getShiftRect() {
const g = document.getElementById(SHIFTBOX_ID);
return g ? g.querySelector('rect') : null;
}
// v12: pak de “grote deurvulling” path (gradient fill) in #HS-vastedeur
function getDoorFillPath() {
const g = document.getElementById(DOOR_GROUP_ID);
if (!g) return null;
const paths = [...g.querySelectorAll(':scope > path[fill^="url("][d]')];
if (!paths.length) return null;
// neem de langste d (meestal het paneel)
let best = paths[0];
let bestLen = (best.getAttribute('d') || '').length;
for (const p of paths) {
const len = (p.getAttribute('d') || '').length;
if (len > bestLen) { best = p; bestLen = len; }
}
return best;
}
function adjustRectX(rectEl, dx) {
if (!rectEl || !dx) return;
const x0 = toNum(rectEl.getAttribute('x'));
if (x0 === null) return;
rectEl.setAttribute('x', String(x0 + dx));
}
// ---------- translate helpers ----------
function parseTranslate(transformStr) {
const m = /translate\(\s*([-\d.]+)(?:[\s,]+([-\d.]+))?\s*\)/.exec(transformStr || '');
if (!m) return { x: 0, y: 0 };
return { x: Number(m[1]), y: m[2] !== undefined ? Number(m[2]) : 0 };
}
// worldOffsetX = som van translateX op element + ouders tot svg
function getWorldOffsetXFromElement(el) {
let x = 0;
let cur = el;
while (cur && cur.tagName) {
const t = cur.getAttribute && cur.getAttribute('transform');
if (t && /translate\(/.test(t)) x += (parseTranslate(t).x || 0);
cur = cur.parentElement;
if (cur && cur.tagName && cur.tagName.toLowerCase() === 'svg') break;
}
return x;
}
// ---------- PATH helpers ----------
function parseTokens(d) {
return d.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/g);
}
function round3(v) { return Math.round(v * 1000) / 1000; }
// Normaliseer path 1x naar absolute commands
function normalizePathToAbsoluteOnce(pathEl) {
if (!pathEl) return;
if (pathEl.dataset.absNormalized === '1') return;
const d = pathEl.getAttribute('d');
if (!d) return;
const tokens = parseTokens(d);
if (!tokens || !tokens.length) return;
let i = 0, curX = 0, curY = 0, lastCmd = '';
const out = [];
const specs = { M:2, L:2, T:2, H:1, V:1, C:6, S:4, Q:4, A:7, Z:0 };
const take = () => tokens[i++];
const isCmd = (t) => /^[a-zA-Z]$/.test(t);
const toAbs = (x, y, rel) => rel ? [x + curX, y + curY] : [x, y];
while (i < tokens.length) {
let cmd = take();
if (!isCmd(cmd)) { i--; cmd = lastCmd; } else { lastCmd = cmd; }
const ABS = cmd.toUpperCase();
const rel = cmd !== ABS;
if (ABS === 'Z') { out.push('Z'); continue; }
const nPer = specs[ABS];
if (!nPer) continue;
let buf = [];
while (i < tokens.length && !isCmd(tokens[i])) {
buf.push(parseFloat(take()));
if (buf.length % nPer === 0) {
const n = buf.splice(buf.length - nPer, nPer);
switch (ABS) {
case 'M':
case 'L':
case 'T': {
let [x, y] = toAbs(n[0], n[1], rel);
out.push(ABS, round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'H': {
let x = rel ? curX + n[0] : n[0];
out.push('H', round3(x));
curX = x;
break;
}
case 'V': {
let y = rel ? curY + n[0] : n[0];
out.push('V', round3(y));
curY = y;
break;
}
case 'C': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x2, y2] = toAbs(n[2], n[3], rel);
let [x, y ] = toAbs(n[4], n[5], rel);
out.push('C',
round3(x1), round3(y1),
round3(x2), round3(y2),
round3(x), round3(y)
);
curX = x; curY = y;
break;
}
case 'S': {
let [x2, y2] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
out.push('S', round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'Q': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
out.push('Q', round3(x1), round3(y1), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'A': {
const rx=n[0], ry=n[1], rot=n[2], laf=n[3], sf=n[4];
let [x, y] = toAbs(n[5], n[6], rel);
out.push('A', rx, ry, rot, laf, sf, round3(x), round3(y));
curX = x; curY = y;
break;
}
}
}
}
}
pathEl.setAttribute('d', out.join(' ').replace(/\s+/g, ' ').trim());
pathEl.dataset.absNormalized = '1';
}
/**
* Shift local X-coördinaten als WORLD-X binnen [minX,maxX] valt:
* worldX = localX + worldOffsetX
*/
function shiftPathXInsideBox_World(pathEl, minX, maxX, dx, worldOffsetX) {
if (!pathEl || !dx) return;
const d = pathEl.getAttribute('d');
if (!d) return;
const tokens = parseTokens(d);
if (!tokens || !tokens.length) return;
let i = 0, curX = 0, curY = 0, lastCmd = '';
const out = [];
const specs = { M:2, L:2, T:2, H:1, V:1, C:6, S:4, Q:4, A:7, Z:0 };
const take = () => tokens[i++];
const isCmd = (t) => /^[a-zA-Z]$/.test(t);
const insideWorld = (localX) => {
const worldX = localX + worldOffsetX;
return worldX >= minX && worldX <= maxX;
};
while (i < tokens.length) {
let cmd = take();
if (!isCmd(cmd)) { i--; cmd = lastCmd; } else { lastCmd = cmd; }
const ABS = cmd.toUpperCase();
const rel = cmd !== ABS;
if (ABS === 'Z') { out.push('Z'); continue; }
const nPer = specs[ABS];
if (!nPer) continue;
const toAbs = (x, y) => rel ? [x + curX, y + curY] : [x, y];
let buf = [];
while (i < tokens.length && !isCmd(tokens[i])) {
buf.push(parseFloat(take()));
if (buf.length % nPer === 0) {
const n = buf.splice(buf.length - nPer, nPer);
switch (ABS) {
case 'M':
case 'L':
case 'T': {
let [x, y] = toAbs(n[0], n[1]);
if (insideWorld(x)) x += dx;
out.push(ABS, round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'H': {
let x = rel ? curX + n[0] : n[0];
if (insideWorld(x)) x += dx;
out.push('H', round3(x));
curX = x;
break;
}
case 'V': {
let y = rel ? curY + n[0] : n[0];
out.push('V', round3(y));
curY = y;
break;
}
case 'C': {
let [x1, y1] = toAbs(n[0], n[1]);
let [x2, y2] = toAbs(n[2], n[3]);
let [x, y ] = toAbs(n[4], n[5]);
if (insideWorld(x1)) x1 += dx;
if (insideWorld(x2)) x2 += dx;
if (insideWorld(x )) x += dx;
out.push('C',
round3(x1), round3(y1),
round3(x2), round3(y2),
round3(x), round3(y)
);
curX = x; curY = y;
break;
}
case 'S': {
let [x2, y2] = toAbs(n[0], n[1]);
let [x, y ] = toAbs(n[2], n[3]);
if (insideWorld(x2)) x2 += dx;
if (insideWorld(x )) x += dx;
out.push('S', round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'Q': {
let [x1, y1] = toAbs(n[0], n[1]);
let [x, y ] = toAbs(n[2], n[3]);
if (insideWorld(x1)) x1 += dx;
if (insideWorld(x )) x += dx;
out.push('Q', round3(x1), round3(y1), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'A': {
const rx=n[0], ry=n[1], rot=n[2], laf=n[3], sf=n[4];
let [x, y] = toAbs(n[5], n[6]);
if (insideWorld(x)) x += dx;
out.push('A', rx, ry, rot, laf, sf, round3(x), round3(y));
curX = x; curY = y;
break;
}
}
}
}
}
pathEl.setAttribute('d', out.join(' ').replace(/\s+/g, ' ').trim());
}
// ---------- Apply dx ----------
function applyDx(dxRect, dxPath) {
if (!dxRect && !dxPath) return;
const rect = getShiftRect();
if (!rect) return;
// bounds vóór verplaatsen (selectie)
const x0 = toNum(rect.getAttribute('x'));
const w = toNum(rect.getAttribute('width'));
if (x0 === null || w === null) return;
const minX = x0;
const maxX = x0 + w;
// v12: deurpaneel path vervormen (geen clippath meer)
if (dxPath) {
const doorPath = getDoorFillPath();
if (doorPath) {
normalizePathToAbsoluteOnce(doorPath);
const worldOffsetX = getWorldOffsetXFromElement(doorPath);
shiftPathXInsideBox_World(doorPath, minX, maxX, dxPath, worldOffsetX);
}
}
// shiftbox rect
if (dxRect) adjustRectX(rect, dxRect);
}
// ---------- Bind input deltas ----------
function bindNumberDelta(inputEl, prevKey, multRect, multPath) {
if (!inputEl) return;
const minVal = toNum(inputEl.getAttribute('min'));
const maxVal = toNum(inputEl.getAttribute('max'));
if (inputEl.dataset[prevKey] == null) {
const v0 = clamp(toNum(inputEl.value), minVal, maxVal);
inputEl.dataset[prevKey] = String(v0 ?? 0);
}
const handler = () => {
const newVal = clamp(toNum(inputEl.value), minVal, maxVal);
if (newVal === null) return;
const prevVal = toNum(inputEl.dataset[prevKey]);
if (prevVal === null) { inputEl.dataset[prevKey] = String(newVal); return; }
const delta = newVal - prevVal;
if (!delta) return;
applyDx(delta * multRect, delta * multPath);
inputEl.dataset[prevKey] = String(newVal);
};
inputEl.addEventListener('input', handler);
inputEl.addEventListener('change', handler);
}
function setupOnce() {
if (document.documentElement.dataset.hsVastLGeomV12Bound === '1') return true;
const rect = getShiftRect();
const deurdikte = document.getElementById(DEURDIKTE_ID);
const tussenmaat = document.getElementById(TUSSENMAAT_ID);
if (!rect || !deurdikte || !tussenmaat) return false;
// ✅ deurdikte: rect 1.5x, path 1.0x
bindNumberDelta(deurdikte, 'prevDeurdikteForVastL', K_DEURDIKTE_RECT, K_DEURDIKTE_PATH);
// ✅ hstussenmaat: alleen rect 1.0x
bindNumberDelta(tussenmaat, 'prevTussenmaatForVastL', K_TUSSENMAAT_RECT, 0);
document.documentElement.dataset.hsVastLGeomV12Bound = '1';
return true;
}
if (setupOnce()) return;
const obs = new MutationObserver(() => {
if (setupOnce()) obs.disconnect();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
window.addEventListener('load', () => { setupOnce(); }, { once: true });
})();
//// HS — VASTE DEURDIKTE #DORPEL ////
(() => {
if (window.__HS_VASTL_V12_GEOM__) return;
window.__HS_VASTL_V12_GEOM__ = true;
const DEURDIKTE_ID = 'form-field-deurdikte';
const TUSSENMAAT_ID = 'form-field-hstussenmaat';
const SHIFTBOX_ID = 'x-shiftbox-vast-L';
const DOOR_GROUP_ID = 'HS-vastedeur';
// ===== Schaalfactoren =====
const K_DEURDIKTE_RECT = -1.5; // +1 deurdikte => shiftbox x -= 1.5
const K_DEURDIKTE_PATH = -1.0; // +1 deurdikte => deurvulling punten (in box) x -= 1.0
const K_TUSSENMAAT_RECT = -1.0; // +1 tussenmaat => shiftbox x -= 1.0 (deurvulling: 0)
const toNum = (v) => {
const n = Number(v);
return Number.isFinite(n) ? n : null;
};
const clamp = (n, min, max) => {
if (!Number.isFinite(n)) return null;
if (Number.isFinite(min)) n = Math.max(min, n);
if (Number.isFinite(max)) n = Math.min(max, n);
return n;
};
// ---------- SVG targeting ----------
function getShiftRect() {
const g = document.getElementById(SHIFTBOX_ID);
return g ? g.querySelector('rect') : null;
}
// v12: pak de “grote deurvulling” path (gradient fill) in #HS-vastedeur
function getDoorFillPath() {
const g = document.getElementById(DOOR_GROUP_ID);
if (!g) return null;
const paths = [...g.querySelectorAll(':scope > path[fill^="url("][d]')];
if (!paths.length) return null;
// neem de langste d (meestal het paneel)
let best = paths[0];
let bestLen = (best.getAttribute('d') || '').length;
for (const p of paths) {
const len = (p.getAttribute('d') || '').length;
if (len > bestLen) { best = p; bestLen = len; }
}
return best;
}
function adjustRectX(rectEl, dx) {
if (!rectEl || !dx) return;
const x0 = toNum(rectEl.getAttribute('x'));
if (x0 === null) return;
rectEl.setAttribute('x', String(x0 + dx));
}
// ---------- translate helpers ----------
function parseTranslate(transformStr) {
const m = /translate\(\s*([-\d.]+)(?:[\s,]+([-\d.]+))?\s*\)/.exec(transformStr || '');
if (!m) return { x: 0, y: 0 };
return { x: Number(m[1]), y: m[2] !== undefined ? Number(m[2]) : 0 };
}
// worldOffsetX = som van translateX op element + ouders tot svg
function getWorldOffsetXFromElement(el) {
let x = 0;
let cur = el;
while (cur && cur.tagName) {
const t = cur.getAttribute && cur.getAttribute('transform');
if (t && /translate\(/.test(t)) x += (parseTranslate(t).x || 0);
cur = cur.parentElement;
if (cur && cur.tagName && cur.tagName.toLowerCase() === 'svg') break;
}
return x;
}
// ---------- PATH helpers ----------
function parseTokens(d) {
return d.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/g);
}
function round3(v) { return Math.round(v * 1000) / 1000; }
// Normaliseer path 1x naar absolute commands
function normalizePathToAbsoluteOnce(pathEl) {
if (!pathEl) return;
if (pathEl.dataset.absNormalized === '1') return;
const d = pathEl.getAttribute('d');
if (!d) return;
const tokens = parseTokens(d);
if (!tokens || !tokens.length) return;
let i = 0, curX = 0, curY = 0, lastCmd = '';
const out = [];
const specs = { M:2, L:2, T:2, H:1, V:1, C:6, S:4, Q:4, A:7, Z:0 };
const take = () => tokens[i++];
const isCmd = (t) => /^[a-zA-Z]$/.test(t);
const toAbs = (x, y, rel) => rel ? [x + curX, y + curY] : [x, y];
while (i < tokens.length) {
let cmd = take();
if (!isCmd(cmd)) { i--; cmd = lastCmd; } else { lastCmd = cmd; }
const ABS = cmd.toUpperCase();
const rel = cmd !== ABS;
if (ABS === 'Z') { out.push('Z'); continue; }
const nPer = specs[ABS];
if (!nPer) continue;
let buf = [];
while (i < tokens.length && !isCmd(tokens[i])) {
buf.push(parseFloat(take()));
if (buf.length % nPer === 0) {
const n = buf.splice(buf.length - nPer, nPer);
switch (ABS) {
case 'M':
case 'L':
case 'T': {
let [x, y] = toAbs(n[0], n[1], rel);
out.push(ABS, round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'H': {
let x = rel ? curX + n[0] : n[0];
out.push('H', round3(x));
curX = x;
break;
}
case 'V': {
let y = rel ? curY + n[0] : n[0];
out.push('V', round3(y));
curY = y;
break;
}
case 'C': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x2, y2] = toAbs(n[2], n[3], rel);
let [x, y ] = toAbs(n[4], n[5], rel);
out.push('C',
round3(x1), round3(y1),
round3(x2), round3(y2),
round3(x), round3(y)
);
curX = x; curY = y;
break;
}
case 'S': {
let [x2, y2] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
out.push('S', round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'Q': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
out.push('Q', round3(x1), round3(y1), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'A': {
const rx=n[0], ry=n[1], rot=n[2], laf=n[3], sf=n[4];
let [x, y] = toAbs(n[5], n[6], rel);
out.push('A', rx, ry, rot, laf, sf, round3(x), round3(y));
curX = x; curY = y;
break;
}
}
}
}
}
pathEl.setAttribute('d', out.join(' ').replace(/\s+/g, ' ').trim());
pathEl.dataset.absNormalized = '1';
}
/**
* Shift local X-coördinaten als WORLD-X binnen [minX,maxX] valt:
* worldX = localX + worldOffsetX
*/
function shiftPathXInsideBox_World(pathEl, minX, maxX, dx, worldOffsetX) {
if (!pathEl || !dx) return;
const d = pathEl.getAttribute('d');
if (!d) return;
const tokens = parseTokens(d);
if (!tokens || !tokens.length) return;
let i = 0, curX = 0, curY = 0, lastCmd = '';
const out = [];
const specs = { M:2, L:2, T:2, H:1, V:1, C:6, S:4, Q:4, A:7, Z:0 };
const take = () => tokens[i++];
const isCmd = (t) => /^[a-zA-Z]$/.test(t);
const insideWorld = (localX) => {
const worldX = localX + worldOffsetX;
return worldX >= minX && worldX <= maxX;
};
while (i < tokens.length) {
let cmd = take();
if (!isCmd(cmd)) { i--; cmd = lastCmd; } else { lastCmd = cmd; }
const ABS = cmd.toUpperCase();
const rel = cmd !== ABS;
if (ABS === 'Z') { out.push('Z'); continue; }
const nPer = specs[ABS];
if (!nPer) continue;
const toAbs = (x, y) => rel ? [x + curX, y + curY] : [x, y];
let buf = [];
while (i < tokens.length && !isCmd(tokens[i])) {
buf.push(parseFloat(take()));
if (buf.length % nPer === 0) {
const n = buf.splice(buf.length - nPer, nPer);
switch (ABS) {
case 'M':
case 'L':
case 'T': {
let [x, y] = toAbs(n[0], n[1]);
if (insideWorld(x)) x += dx;
out.push(ABS, round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'H': {
let x = rel ? curX + n[0] : n[0];
if (insideWorld(x)) x += dx;
out.push('H', round3(x));
curX = x;
break;
}
case 'V': {
let y = rel ? curY + n[0] : n[0];
out.push('V', round3(y));
curY = y;
break;
}
case 'C': {
let [x1, y1] = toAbs(n[0], n[1]);
let [x2, y2] = toAbs(n[2], n[3]);
let [x, y ] = toAbs(n[4], n[5]);
if (insideWorld(x1)) x1 += dx;
if (insideWorld(x2)) x2 += dx;
if (insideWorld(x )) x += dx;
out.push('C',
round3(x1), round3(y1),
round3(x2), round3(y2),
round3(x), round3(y)
);
curX = x; curY = y;
break;
}
case 'S': {
let [x2, y2] = toAbs(n[0], n[1]);
let [x, y ] = toAbs(n[2], n[3]);
if (insideWorld(x2)) x2 += dx;
if (insideWorld(x )) x += dx;
out.push('S', round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'Q': {
let [x1, y1] = toAbs(n[0], n[1]);
let [x, y ] = toAbs(n[2], n[3]);
if (insideWorld(x1)) x1 += dx;
if (insideWorld(x )) x += dx;
out.push('Q', round3(x1), round3(y1), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'A': {
const rx=n[0], ry=n[1], rot=n[2], laf=n[3], sf=n[4];
let [x, y] = toAbs(n[5], n[6]);
if (insideWorld(x)) x += dx;
out.push('A', rx, ry, rot, laf, sf, round3(x), round3(y));
curX = x; curY = y;
break;
}
}
}
}
}
pathEl.setAttribute('d', out.join(' ').replace(/\s+/g, ' ').trim());
}
// ---------- Apply dx ----------
function applyDx(dxRect, dxPath) {
if (!dxRect && !dxPath) return;
const rect = getShiftRect();
if (!rect) return;
// bounds vóór verplaatsen (selectie)
const x0 = toNum(rect.getAttribute('x'));
const w = toNum(rect.getAttribute('width'));
if (x0 === null || w === null) return;
const minX = x0;
const maxX = x0 + w;
// v12: deurpaneel path vervormen (geen clippath meer)
if (dxPath) {
const doorPath = getDoorFillPath();
if (doorPath) {
normalizePathToAbsoluteOnce(doorPath);
const worldOffsetX = getWorldOffsetXFromElement(doorPath);
shiftPathXInsideBox_World(doorPath, minX, maxX, dxPath, worldOffsetX);
}
}
// shiftbox rect
if (dxRect) adjustRectX(rect, dxRect);
}
// ---------- Bind input deltas ----------
function bindNumberDelta(inputEl, prevKey, multRect, multPath) {
if (!inputEl) return;
const minVal = toNum(inputEl.getAttribute('min'));
const maxVal = toNum(inputEl.getAttribute('max'));
if (inputEl.dataset[prevKey] == null) {
const v0 = clamp(toNum(inputEl.value), minVal, maxVal);
inputEl.dataset[prevKey] = String(v0 ?? 0);
}
const handler = () => {
const newVal = clamp(toNum(inputEl.value), minVal, maxVal);
if (newVal === null) return;
const prevVal = toNum(inputEl.dataset[prevKey]);
if (prevVal === null) { inputEl.dataset[prevKey] = String(newVal); return; }
const delta = newVal - prevVal;
if (!delta) return;
applyDx(delta * multRect, delta * multPath);
inputEl.dataset[prevKey] = String(newVal);
};
inputEl.addEventListener('input', handler);
inputEl.addEventListener('change', handler);
}
function setupOnce() {
if (document.documentElement.dataset.hsVastLGeomV12Bound === '1') return true;
const rect = getShiftRect();
const deurdikte = document.getElementById(DEURDIKTE_ID);
const tussenmaat = document.getElementById(TUSSENMAAT_ID);
if (!rect || !deurdikte || !tussenmaat) return false;
// ✅ deurdikte: rect 1.5x, path 1.0x
bindNumberDelta(deurdikte, 'prevDeurdikteForVastL', K_DEURDIKTE_RECT, K_DEURDIKTE_PATH);
// ✅ hstussenmaat: alleen rect 1.0x
bindNumberDelta(tussenmaat, 'prevTussenmaatForVastL', K_TUSSENMAAT_RECT, 0);
document.documentElement.dataset.hsVastLGeomV12Bound = '1';
return true;
}
if (setupOnce()) return;
const obs = new MutationObserver(() => {
if (setupOnce()) obs.disconnect();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
window.addEventListener('load', () => { setupOnce(); }, { once: true });
})();
//// HS SCHUIFDEURDIKTE #DORPEL ////
(() => {
if (window.__HS_SCHUIF_GEOM_V12__) return;
window.__HS_SCHUIF_GEOM_V12__ = true;
const INPUT_ID = 'form-field-deurdikte';
const ROOT_SVG_ID = 'dorpel';
const SHIFTBOX_L_ID = 'x-shiftbox-schuif-L';
const SHIFTBOX_R_ID = 'x-shiftbox-schuif-R';
const SCHUIF_GROUP_ID = 'HS-schuif';
const PREV_KEY = 'hsSchuifPrev';
const BOUND_KEY = 'hsSchuifGeomV12Bound';
const toNum = (v) => {
const n = Number(v);
return Number.isFinite(n) ? n : null;
};
const clamp = (n, min, max) => {
if (!Number.isFinite(n)) return null;
if (Number.isFinite(min)) n = Math.max(min, n);
if (Number.isFinite(max)) n = Math.min(max, n);
return n;
};
function getRootSvg() {
const svg = document.getElementById(ROOT_SVG_ID);
if (!svg || svg.tagName.toLowerCase() !== 'svg') return null;
return svg;
}
function escId(id) {
if (window.CSS && CSS.escape) return CSS.escape(id);
return id.replace(/([ !"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~])/g, '\\$1');
}
function byId(root, id) {
if (!root) return null;
return root.querySelector('#' + escId(id));
}
function getShiftRectByGroupId(root, groupId) {
const g = byId(root, groupId);
if (!g) return null;
return g.querySelector('rect');
}
function adjustRectX(rectEl, dx) {
if (!rectEl || !dx) return;
const x0 = toNum(rectEl.getAttribute('x'));
if (x0 === null) return;
rectEl.setAttribute('x', String(x0 + dx));
}
// -------- translate helpers (WORLD offset) ----------
function parseTranslate(transformStr) {
const m = /translate\(\s*([-\d.]+)(?:[\s,]+([-\d.]+))?\s*\)/.exec(transformStr || '');
if (!m) return { x: 0, y: 0 };
return { x: Number(m[1]), y: m[2] !== undefined ? Number(m[2]) : 0 };
}
function getWorldOffsetXFromElement(el) {
let x = 0;
let cur = el;
while (cur && cur.tagName) {
const t = cur.getAttribute && cur.getAttribute('transform');
if (t && /translate\(/.test(t)) x += (parseTranslate(t).x || 0);
cur = cur.parentElement;
if (cur && cur.tagName && cur.tagName.toLowerCase() === 'svg') break;
}
return x;
}
// -------- PATH helpers ----------
function parseTokens(d) {
return d.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/g);
}
function round3(v) { return Math.round(v * 1000) / 1000; }
function normalizePathToAbsoluteOnce(pathEl) {
if (!pathEl) return;
if (pathEl.dataset.absNormalized === '1') return;
const d = pathEl.getAttribute('d');
if (!d) return;
const tokens = parseTokens(d);
if (!tokens || !tokens.length) return;
let i = 0, curX = 0, curY = 0, lastCmd = '';
const out = [];
const specs = { M:2, L:2, T:2, H:1, V:1, C:6, S:4, Q:4, A:7, Z:0 };
const take = () => tokens[i++];
const isCmd = (t) => /^[a-zA-Z]$/.test(t);
const toAbs = (x, y, rel) => rel ? [x + curX, y + curY] : [x, y];
while (i < tokens.length) {
let cmd = take();
if (!isCmd(cmd)) { i--; cmd = lastCmd; } else { lastCmd = cmd; }
const ABS = cmd.toUpperCase();
const rel = cmd !== ABS;
if (ABS === 'Z') { out.push('Z'); continue; }
const nPer = specs[ABS];
if (!nPer) continue;
let buf = [];
while (i < tokens.length && !isCmd(tokens[i])) {
buf.push(parseFloat(take()));
if (buf.length % nPer === 0) {
const n = buf.splice(buf.length - nPer, nPer);
switch (ABS) {
case 'M':
case 'L':
case 'T': {
let [x, y] = toAbs(n[0], n[1], rel);
out.push(ABS, round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'H': {
let x = rel ? curX + n[0] : n[0];
out.push('H', round3(x));
curX = x;
break;
}
case 'V': {
let y = rel ? curY + n[0] : n[0];
out.push('V', round3(y));
curY = y;
break;
}
case 'C': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x2, y2] = toAbs(n[2], n[3], rel);
let [x, y ] = toAbs(n[4], n[5], rel);
out.push('C', round3(x1), round3(y1), round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'S': {
let [x2, y2] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
out.push('S', round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'Q': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
out.push('Q', round3(x1), round3(y1), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'A': {
const rx = n[0], ry = n[1], rot = n[2], laf = n[3], sf = n[4];
let [x, y] = toAbs(n[5], n[6], rel);
out.push('A', rx, ry, rot, laf, sf, round3(x), round3(y));
curX = x; curY = y;
break;
}
}
}
}
}
pathEl.setAttribute('d', out.join(' ').replace(/\s+/g, ' ').trim());
pathEl.dataset.absNormalized = '1';
}
function dxForRangesWorld(localX, ranges, worldOffsetX) {
const worldX = localX + worldOffsetX;
for (const r of ranges) {
if (worldX >= r.minX && worldX <= r.maxX) return r.dx;
}
return 0;
}
function shiftPathXByRanges_World(pathEl, ranges, worldOffsetX) {
if (!pathEl || !ranges || !ranges.length) return;
const d = pathEl.getAttribute('d');
if (!d) return;
const tokens = parseTokens(d);
if (!tokens || !tokens.length) return;
let i = 0, curX = 0, curY = 0, lastCmd = '';
const out = [];
const specs = { M:2, L:2, T:2, H:1, V:1, C:6, S:4, Q:4, A:7, Z:0 };
const take = () => tokens[i++];
const isCmd = (t) => /^[a-zA-Z]$/.test(t);
const toAbs = (x, y, rel) => rel ? [x + curX, y + curY] : [x, y];
while (i < tokens.length) {
let cmd = take();
if (!isCmd(cmd)) { i--; cmd = lastCmd; } else { lastCmd = cmd; }
const ABS = cmd.toUpperCase();
const rel = cmd !== ABS;
if (ABS === 'Z') { out.push('Z'); continue; }
const nPer = specs[ABS];
if (!nPer) continue;
let buf = [];
while (i < tokens.length && !isCmd(tokens[i])) {
buf.push(parseFloat(take()));
if (buf.length % nPer === 0) {
const n = buf.splice(buf.length - nPer, nPer);
switch (ABS) {
case 'M':
case 'L':
case 'T': {
let [x, y] = toAbs(n[0], n[1], rel);
x += dxForRangesWorld(x, ranges, worldOffsetX);
out.push(ABS, round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'H': {
let x = rel ? curX + n[0] : n[0];
x += dxForRangesWorld(x, ranges, worldOffsetX);
out.push('H', round3(x));
curX = x;
break;
}
case 'V': {
let y = rel ? curY + n[0] : n[0];
out.push('V', round3(y));
curY = y;
break;
}
case 'C': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x2, y2] = toAbs(n[2], n[3], rel);
let [x, y ] = toAbs(n[4], n[5], rel);
x1 += dxForRangesWorld(x1, ranges, worldOffsetX);
x2 += dxForRangesWorld(x2, ranges, worldOffsetX);
x += dxForRangesWorld(x, ranges, worldOffsetX);
out.push('C', round3(x1), round3(y1), round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'S': {
let [x2, y2] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
x2 += dxForRangesWorld(x2, ranges, worldOffsetX);
x += dxForRangesWorld(x, ranges, worldOffsetX);
out.push('S', round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'Q': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
x1 += dxForRangesWorld(x1, ranges, worldOffsetX);
x += dxForRangesWorld(x, ranges, worldOffsetX);
out.push('Q', round3(x1), round3(y1), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'A': {
const rx = n[0], ry = n[1], rot = n[2], laf = n[3], sf = n[4];
let [x, y] = toAbs(n[5], n[6], rel);
x += dxForRangesWorld(x, ranges, worldOffsetX);
out.push('A', rx, ry, rot, laf, sf, round3(x), round3(y));
curX = x; curY = y;
break;
}
}
}
}
}
pathEl.setAttribute('d', out.join(' ').replace(/\s+/g, ' ').trim());
}
function getSchuifDoorFillPaths(root) {
const g = byId(root, SCHUIF_GROUP_ID);
if (!g) return [];
const candidates = [...g.querySelectorAll(':scope > path[fill^="url("][d]')].filter(p => p.id !== 'HS_x5F_alu');
if (!candidates.length) return [];
let best = candidates[0];
let bestLen = (best.getAttribute('d') || '').length;
for (const p of candidates) {
const len = (p.getAttribute('d') || '').length;
if (len > bestLen) { best = p; bestLen = len; }
}
return [best];
}
function setupOnce(input) {
if (!input || input.dataset[BOUND_KEY] === '1') return false;
input.dataset[BOUND_KEY] = '1';
const root = getRootSvg();
if (!root) return false;
const minVal = toNum(input.getAttribute('min'));
const maxVal = toNum(input.getAttribute('max'));
if (input.dataset[PREV_KEY] == null) {
const v0 = clamp(toNum(input.value), minVal, maxVal);
input.dataset[PREV_KEY] = String(v0 ?? 0);
}
const handler = () => {
const newVal = clamp(toNum(input.value), minVal, maxVal);
if (newVal === null) return;
const prevVal = toNum(input.dataset[PREV_KEY]);
if (prevVal === null) { input.dataset[PREV_KEY] = String(newVal); return; }
const delta = newVal - prevVal;
if (!delta) return;
const half = delta / 2;
const rectL = getShiftRectByGroupId(root, SHIFTBOX_L_ID);
const rectR = getShiftRectByGroupId(root, SHIFTBOX_R_ID);
if (!rectL || !rectR) return;
const xL = toNum(rectL.getAttribute('x'));
const wL = toNum(rectL.getAttribute('width'));
const xR = toNum(rectR.getAttribute('x'));
const wR = toNum(rectR.getAttribute('width'));
if (xL === null || wL === null || xR === null || wR === null) return;
const ranges = [
{ minX: xL, maxX: xL + wL, dx: -half },
{ minX: xR, maxX: xR + wR, dx: +half },
];
const paths = getSchuifDoorFillPaths(root);
for (const p of paths) {
normalizePathToAbsoluteOnce(p);
const worldOffsetX = getWorldOffsetXFromElement(p);
shiftPathXByRanges_World(p, ranges, worldOffsetX);
}
adjustRectX(rectL, -half);
adjustRectX(rectR, +half);
document.dispatchEvent(new CustomEvent('hs:applyTussenmaatDelta', {
detail: { diff: half }
}));
input.dataset[PREV_KEY] = String(newVal);
};
input.addEventListener('input', handler);
input.addEventListener('change', handler);
return true;
}
function tryInit() {
const input = document.getElementById(INPUT_ID);
if (!input) return false;
return setupOnce(input);
}
if (tryInit()) return;
const obs = new MutationObserver(() => {
if (tryInit()) obs.disconnect();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
window.addEventListener('load', () => { tryInit(); }, { once: true });
})();
//// Tussenmaat HS deuren ////
(() => {
const INPUT_ID = 'form-field-hstussenmaat';
// === AANGEPASTE indices (zoals gevraagd) ===
// Binnen #neut*:
// - LR-group -> indices 5,7
// - T-group -> indices 17,19
const INDICES_NEUT_LR = [5, 7];
const INDICES_NEUT_T = [17, 19];
// Binnen #vastbasis* -> indices 1,3,5,7,9,11
const INDICES_VASTBASIS = [1, 3, 5, 7, 9, 11];
// Group-detectie ongewijzigd
const isLRGroup = (gid) => gid.includes('HS-L-V') || gid.includes('HS-R-V');
const T_KEYS = ['HS-T1-UV','HS-T1-VU','HS-T2-UV','HS-T2-VU','HS-T3-UV','HS-T3-VU','HS-T4-UV','HS-T4-VU'];
const isTGroup = (gid) => T_KEYS.some(k => gid.includes(k));
const toNum = (v) => {
const n = Number(v);
return Number.isFinite(n) ? n : null;
};
// -------------------- POLYGON helpers --------------------
const parsePoints = (s) => (s || '').trim().split(/[\s,]+/).map(Number);
const serializePoints = (arr) => arr.join(' ');
function shiftPolygonByDiff(poly, flatIndices, diff) {
if (!diff) return;
const ptsStr = poly.getAttribute('points');
if (!ptsStr) return;
const pts = parsePoints(ptsStr);
let changed = false;
for (const idx of flatIndices) {
if (idx >= 0 && idx < pts.length && Number.isFinite(pts[idx])) {
pts[idx] += diff;
changed = true;
}
}
if (changed) poly.setAttribute('points', serializePoints(pts));
}
// -------------------- RECT helper --------------------
function shiftRectYByDiff(rect, diff) {
if (!diff) return;
const y0 = toNum(rect.getAttribute('y'));
if (y0 === null) return;
rect.setAttribute('y', String(y0 + diff));
}
// -------------------- TRANSLATE helpers --------------------
function parseTranslate(transformStr) {
const m = /translate\(\s*([-\d.]+)(?:[\s,]+([-\d.]+))?\s*\)/.exec(transformStr || '');
if (!m) return { x: 0, y: 0 };
return { x: Number(m[1]), y: m[2] !== undefined ? Number(m[2]) : 0 };
}
function setTranslate(el, x, y) {
const t = el.getAttribute('transform') || '';
const re = /translate\(\s*([-\d.]+)(?:[\s,]+([-\d.]+))?\s*\)/;
if (re.test(t)) {
el.setAttribute('transform', t.replace(re, `translate(${x},${y})`));
} else {
el.setAttribute('transform', t ? `translate(${x},${y}) ${t}` : `translate(${x},${y})`);
}
}
function shiftTranslateYByDiff(el, diff) {
if (!diff) return;
const { x, y } = parseTranslate(el.getAttribute('transform'));
setTranslate(el, x, y + diff);
}
// Shift rect by translate-y if present, otherwise by y attribute
function shiftRectYOrTranslateYByDiff(rect, diff) {
if (!diff || !rect) return;
const t = rect.getAttribute('transform') || '';
if (/translate\(/.test(t)) {
shiftTranslateYByDiff(rect, diff);
return;
}
const y0 = toNum(rect.getAttribute('y'));
if (y0 === null) return;
rect.setAttribute('y', String(y0 + diff));
}
// -------------------- GRADIENT helpers --------------------
function parseUrlId(fillValue) {
const m = /url\(\s*#([^)]+)\s*\)/.exec(fillValue || '');
return m ? m[1] : null;
}
function shiftGradientYByDiff(svg, gradientId, diff) {
if (!svg || !gradientId || !diff) return;
const grad = svg.querySelector(`#${CSS.escape(gradientId)}`);
if (!grad) return;
const gt = grad.getAttribute('gradientTransform') || '';
if (/translate\(/.test(gt)) {
const { x, y } = parseTranslate(gt);
const re = /translate\(\s*([-\d.]+)(?:[\s,]+([-\d.]+))?\s*\)/;
const newGT = re.test(gt)
? gt.replace(re, `translate(${x},${y + diff})`)
: `translate(${x},${y + diff}) ${gt}`.trim();
grad.setAttribute('gradientTransform', newGT);
return;
}
const y1 = toNum(grad.getAttribute('y1'));
const y2 = toNum(grad.getAttribute('y2'));
if (y1 !== null) grad.setAttribute('y1', String(y1 + diff));
if (y2 !== null) grad.setAttribute('y2', String(y2 + diff));
const cy = toNum(grad.getAttribute('cy'));
const fy = toNum(grad.getAttribute('fy'));
if (cy !== null) grad.setAttribute('cy', String(cy + diff));
if (fy !== null) grad.setAttribute('fy', String(fy + diff));
}
// -------------------- PATH helpers --------------------
function parseTokens(d) {
return d?.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/g);
}
function round3(v) { return Math.round(v * 1000) / 1000; }
function getDirectChild(g, tagName) {
for (const node of g.children) {
if (node.tagName && node.tagName.toLowerCase() === tagName.toLowerCase()) return node;
}
return null;
}
function normalizePathToAbsoluteOnce(pathEl) {
if (!pathEl) return;
if (pathEl.dataset.absNormalized === '1') return;
const d = pathEl.getAttribute('d');
if (!d) return;
const tokens = parseTokens(d);
if (!tokens || !tokens.length) return;
let i = 0, curX = 0, curY = 0, lastCmd = '';
const out = [];
const specs = { M:2, L:2, T:2, H:1, V:1, C:6, S:4, Q:4, A:7, Z:0 };
const take = () => tokens[i++];
const isCmd = (t) => /^[a-zA-Z]$/.test(t);
const toAbs = (x, y, rel) => rel ? [x + curX, y + curY] : [x, y];
while (i < tokens.length) {
let cmd = take();
if (!isCmd(cmd)) { i--; cmd = lastCmd; } else { lastCmd = cmd; }
const ABS = cmd.toUpperCase();
const rel = cmd !== ABS;
if (ABS === 'Z') { out.push('Z'); continue; }
const nPer = specs[ABS];
if (!nPer) continue;
let buf = [];
while (i < tokens.length && !isCmd(tokens[i])) {
buf.push(parseFloat(take()));
if (buf.length % nPer === 0) {
const n = buf.splice(buf.length - nPer, nPer);
switch (ABS) {
case 'M':
case 'L':
case 'T': {
let [x, y] = toAbs(n[0], n[1], rel);
out.push(ABS, round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'H': {
let x = rel ? curX + n[0] : n[0];
out.push('H', round3(x));
curX = x;
break;
}
case 'V': {
let y = rel ? curY + n[0] : n[0];
out.push('V', round3(y));
curY = y;
break;
}
case 'C': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x2, y2] = toAbs(n[2], n[3], rel);
let [x, y ] = toAbs(n[4], n[5], rel);
out.push('C',
round3(x1), round3(y1),
round3(x2), round3(y2),
round3(x), round3(y)
);
curX = x; curY = y;
break;
}
case 'S': {
let [x2, y2] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
out.push('S', round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'Q': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
out.push('Q', round3(x1), round3(y1), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'A': {
const rx = n[0], ry = n[1], rot = n[2], laf = n[3], sf = n[4];
let [x, y] = toAbs(n[5], n[6], rel);
out.push('A', rx, ry, rot, laf, sf, round3(x), round3(y));
curX = x; curY = y;
break;
}
}
}
}
}
pathEl.setAttribute('d', out.join(' ').replace(/\s+/g, ' ').trim());
pathEl.dataset.absNormalized = '1';
}
function adjustPathXInsideBox_World(pathEl, minX, maxX, deltaX, worldOffsetX) {
if (!pathEl || !deltaX) return;
const d = pathEl.getAttribute('d');
if (!d) return;
const tokens = parseTokens(d);
if (!tokens || !tokens.length) return;
let i = 0, curX = 0, curY = 0, lastCmd = '';
const out = [];
const specs = { M:2, L:2, T:2, H:1, V:1, C:6, S:4, Q:4, A:7, Z:0 };
const take = () => tokens[i++];
const isCmd = (t) => /^[a-zA-Z]$/.test(t);
const insideWorld = (localX) => {
const worldX = localX + worldOffsetX;
return worldX >= minX && worldX <= maxX;
};
while (i < tokens.length) {
let cmd = take();
if (!isCmd(cmd)) { i--; cmd = lastCmd; } else { lastCmd = cmd; }
const ABS = cmd.toUpperCase();
const rel = cmd !== ABS;
if (ABS === 'Z') { out.push('Z'); continue; }
const nPer = specs[ABS];
if (!nPer) continue;
const toAbs = (x, y) => rel ? [x + curX, y + curY] : [x, y];
let buf = [];
while (i < tokens.length && !isCmd(tokens[i])) {
buf.push(parseFloat(take()));
if (buf.length % nPer === 0) {
const n = buf.splice(buf.length - nPer, nPer);
switch (ABS) {
case 'M':
case 'L':
case 'T': {
let [x, y] = toAbs(n[0], n[1]);
if (insideWorld(x)) x += deltaX;
out.push(ABS, round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'H': {
let x = rel ? curX + n[0] : n[0];
if (insideWorld(x)) x += deltaX;
out.push('H', round3(x));
curX = x;
break;
}
case 'V': {
let y = rel ? curY + n[0] : n[0];
out.push('V', round3(y));
curY = y;
break;
}
case 'C': {
let [x1, y1] = toAbs(n[0], n[1]);
let [x2, y2] = toAbs(n[2], n[3]);
let [x, y ] = toAbs(n[4], n[5]);
if (insideWorld(x1)) x1 += deltaX;
if (insideWorld(x2)) x2 += deltaX;
if (insideWorld(x )) x += deltaX;
out.push('C',
round3(x1), round3(y1),
round3(x2), round3(y2),
round3(x), round3(y)
);
curX = x; curY = y;
break;
}
case 'S': {
let [x2, y2] = toAbs(n[0], n[1]);
let [x, y ] = toAbs(n[2], n[3]);
if (insideWorld(x2)) x2 += deltaX;
if (insideWorld(x )) x += deltaX;
out.push('S', round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'Q': {
let [x1, y1] = toAbs(n[0], n[1]);
let [x, y ] = toAbs(n[2], n[3]);
if (insideWorld(x1)) x1 += deltaX;
if (insideWorld(x )) x += deltaX;
out.push('Q', round3(x1), round3(y1), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'A': {
const rx = n[0], ry = n[1], rot = n[2], laf = n[3], sf = n[4];
let [x, y] = toAbs(n[5], n[6]);
if (insideWorld(x)) x += deltaX;
out.push('A', rx, ry, rot, laf, sf, round3(x), round3(y));
curX = x; curY = y;
break;
}
}
}
}
}
pathEl.setAttribute('d', out.join(' ').replace(/\s+/g, ' ').trim());
}
// -------------------- Door logic (svg#dorpel) --------------------
function applyDoorDiff(diff, translateFactor = 1) {
if (!diff) return;
const g = document.querySelector('svg g#HS-vastedeur');
if (!g) return;
const svg = g.ownerSVGElement || document.querySelector('svg');
if (!svg) return;
const shiftRect = svg.querySelector('#x-shiftbox-sponning-HS-deur rect');
if (!shiftRect) return;
const minX = toNum(shiftRect.getAttribute('x'));
const w = toNum(shiftRect.getAttribute('width'));
if (minX === null || w === null) return;
const maxX = minX + w;
const { x: txBefore, y: tyBefore } = parseTranslate(g.getAttribute('transform'));
const path = g.querySelector(':scope > path#HSvullat');
if (path) normalizePathToAbsoluteOnce(path);
const deltaX = +(diff * translateFactor);
if (path) {
adjustPathXInsideBox_World(path, minX, maxX, deltaX, txBefore);
}
const firstRect = getDirectChild(g, 'rect');
if (firstRect) {
const x0 = toNum(firstRect.getAttribute('x'));
if (x0 !== null) {
const worldX = x0 + txBefore;
if (worldX >= minX && worldX <= maxX) {
firstRect.setAttribute('x', String(x0 + deltaX));
}
}
}
setTranslate(g, txBefore - (diff * translateFactor), tyBefore);
}
// -------------------- Extra logic (svg#alle-vakken) --------------------
function applyDiffToAlleVakken(diff) {
if (!diff) return;
const svg = document.querySelector('svg#alle-vakken');
if (!svg) return;
const vakSelector =
'g[id^="vak-"][id*="-HS-LS"], g[id^="vak-"][id*="-HS-RS"], g[id^="vak-"][id*="-HS-LR"]';
svg.querySelectorAll(vakSelector).forEach((g) => {
const rects = g.querySelectorAll('rect[data-name="vastedeur"], rect[id^="vastedeur"]');
rects.forEach((r) => {
shiftRectYOrTranslateYByDiff(r, diff);
const fill = r.getAttribute('fill') || '';
const gradId = parseUrlId(fill);
if (gradId) shiftGradientYByDiff(svg, gradId, diff);
});
});
}
// -------------------- Main apply --------------------
function applyDiffToSvg(diff, correctedYFactor = 1, doorTranslateFactor = 1) {
const correctedDiff = diff * correctedYFactor;
// 1) NEUT polygons: per group indices
document.querySelectorAll('svg polygon[id^="neut"]').forEach((poly) => {
const gid = poly.closest('g[id]')?.id || '';
const indices = isLRGroup(gid) ? INDICES_NEUT_LR : (isTGroup(gid) ? INDICES_NEUT_T : null);
if (!indices) return;
shiftPolygonByDiff(poly, indices, correctedDiff);
});
// 2) VASTBASIS polygons: indices 1,3,5,7,9,11
document.querySelectorAll('svg polygon[id^="vastbasis"]').forEach((poly) => {
shiftPolygonByDiff(poly, INDICES_VASTBASIS, correctedDiff);
});
// 3) Vastnok rects (dorpel svg)
document
.querySelectorAll('svg rect[data-name="vastnok"], svg rect[id^="vastnok"]')
.forEach((r) => shiftRectYByDiff(r, correctedDiff));
// 4) Door (dorpel svg)
applyDoorDiff(diff, doorTranslateFactor);
// 5) Extra: alle-vakken svg
applyDiffToAlleVakken(diff);
}
// ====== Onderhuidse delta vanuit andere scripts (bv. deurdikte) ======
document.addEventListener('hs:applyTussenmaatDelta', (e) => {
const diff = e?.detail?.diff;
if (!Number.isFinite(diff) || diff === 0) return;
// Vanuit form_fields[deurdikte]:
// neut / vastbasis / vastnok blijven factor x2.
// #HS-vastedeur translate moet ook x2, zodat stapjes 0,5 -> 1 worden.
applyDiffToSvg(diff, 2, 2);
});
// -------------------- Input (number) delta --------------------
function clamp(n, min, max) {
if (!Number.isFinite(n)) return null;
if (Number.isFinite(min)) n = Math.max(min, n);
if (Number.isFinite(max)) n = Math.min(max, n);
return n;
}
function init() {
const input = document.getElementById(INPUT_ID);
if (!input) return;
const minVal = toNum(input.getAttribute('min'));
const maxVal = toNum(input.getAttribute('max'));
if (input.dataset.prev == null) {
const v0 = clamp(toNum(input.value), minVal, maxVal);
input.dataset.prev = String(v0 ?? 0);
}
const handler = () => {
const newValRaw = toNum(input.value);
const newVal = clamp(newValRaw, minVal, maxVal);
if (newVal === null) return;
const prevVal = toNum(input.dataset.prev);
if (prevVal === null) { input.dataset.prev = String(newVal); return; }
const diff = newVal - prevVal;
if (!diff) return;
// Vanuit form_fields[hstussenmaat]:
// normale factor 1 voor neut / vastbasis / vastnok.
applyDiffToSvg(diff, 1);
input.dataset.prev = String(newVal);
};
input.addEventListener('input', handler);
input.addEventListener('change', handler);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();
//// Tussenmaat HS deuren ////
(() => {
const INPUT_ID = 'form-field-hstussenmaat';
// === AANGEPASTE indices (zoals gevraagd) ===
// Binnen #neut*:
// - LR-group -> indices 5,7
// - T-group -> indices 17,19
const INDICES_NEUT_LR = [5, 7];
const INDICES_NEUT_T = [17, 19];
// Binnen #vastbasis* -> indices 1,3,5,7,9,11
const INDICES_VASTBASIS = [1, 3, 5, 7, 9, 11];
// Group-detectie ongewijzigd
const isLRGroup = (gid) => gid.includes('HS-L-V') || gid.includes('HS-R-V');
const T_KEYS = ['HS-T1-UV','HS-T1-VU','HS-T2-UV','HS-T2-VU','HS-T3-UV','HS-T3-VU','HS-T4-UV','HS-T4-VU'];
const isTGroup = (gid) => T_KEYS.some(k => gid.includes(k));
const toNum = (v) => {
const n = Number(v);
return Number.isFinite(n) ? n : null;
};
// -------------------- POLYGON helpers --------------------
const parsePoints = (s) => (s || '').trim().split(/[\s,]+/).map(Number);
const serializePoints = (arr) => arr.join(' ');
function shiftPolygonByDiff(poly, flatIndices, diff) {
if (!diff) return;
const ptsStr = poly.getAttribute('points');
if (!ptsStr) return;
const pts = parsePoints(ptsStr);
let changed = false;
for (const idx of flatIndices) {
if (idx >= 0 && idx < pts.length && Number.isFinite(pts[idx])) {
pts[idx] += diff;
changed = true;
}
}
if (changed) poly.setAttribute('points', serializePoints(pts));
}
// -------------------- RECT helper --------------------
function shiftRectYByDiff(rect, diff) {
if (!diff) return;
const y0 = toNum(rect.getAttribute('y'));
if (y0 === null) return;
rect.setAttribute('y', String(y0 + diff));
}
// -------------------- TRANSLATE helpers --------------------
function parseTranslate(transformStr) {
const m = /translate\(\s*([-\d.]+)(?:[\s,]+([-\d.]+))?\s*\)/.exec(transformStr || '');
if (!m) return { x: 0, y: 0 };
return { x: Number(m[1]), y: m[2] !== undefined ? Number(m[2]) : 0 };
}
function setTranslate(el, x, y) {
const t = el.getAttribute('transform') || '';
const re = /translate\(\s*([-\d.]+)(?:[\s,]+([-\d.]+))?\s*\)/;
if (re.test(t)) {
el.setAttribute('transform', t.replace(re, `translate(${x},${y})`));
} else {
el.setAttribute('transform', t ? `translate(${x},${y}) ${t}` : `translate(${x},${y})`);
}
}
function shiftTranslateYByDiff(el, diff) {
if (!diff) return;
const { x, y } = parseTranslate(el.getAttribute('transform'));
setTranslate(el, x, y + diff);
}
// Shift rect by translate-y if present, otherwise by y attribute
function shiftRectYOrTranslateYByDiff(rect, diff) {
if (!diff || !rect) return;
const t = rect.getAttribute('transform') || '';
if (/translate\(/.test(t)) {
shiftTranslateYByDiff(rect, diff);
return;
}
const y0 = toNum(rect.getAttribute('y'));
if (y0 === null) return;
rect.setAttribute('y', String(y0 + diff));
}
// -------------------- GRADIENT helpers --------------------
function parseUrlId(fillValue) {
const m = /url\(\s*#([^)]+)\s*\)/.exec(fillValue || '');
return m ? m[1] : null;
}
function shiftGradientYByDiff(svg, gradientId, diff) {
if (!svg || !gradientId || !diff) return;
const grad = svg.querySelector(`#${CSS.escape(gradientId)}`);
if (!grad) return;
const gt = grad.getAttribute('gradientTransform') || '';
if (/translate\(/.test(gt)) {
const { x, y } = parseTranslate(gt);
const re = /translate\(\s*([-\d.]+)(?:[\s,]+([-\d.]+))?\s*\)/;
const newGT = re.test(gt)
? gt.replace(re, `translate(${x},${y + diff})`)
: `translate(${x},${y + diff}) ${gt}`.trim();
grad.setAttribute('gradientTransform', newGT);
return;
}
const y1 = toNum(grad.getAttribute('y1'));
const y2 = toNum(grad.getAttribute('y2'));
if (y1 !== null) grad.setAttribute('y1', String(y1 + diff));
if (y2 !== null) grad.setAttribute('y2', String(y2 + diff));
const cy = toNum(grad.getAttribute('cy'));
const fy = toNum(grad.getAttribute('fy'));
if (cy !== null) grad.setAttribute('cy', String(cy + diff));
if (fy !== null) grad.setAttribute('fy', String(fy + diff));
}
// -------------------- PATH helpers --------------------
function parseTokens(d) {
return d?.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/g);
}
function round3(v) { return Math.round(v * 1000) / 1000; }
function getDirectChild(g, tagName) {
for (const node of g.children) {
if (node.tagName && node.tagName.toLowerCase() === tagName.toLowerCase()) return node;
}
return null;
}
function normalizePathToAbsoluteOnce(pathEl) {
if (!pathEl) return;
if (pathEl.dataset.absNormalized === '1') return;
const d = pathEl.getAttribute('d');
if (!d) return;
const tokens = parseTokens(d);
if (!tokens || !tokens.length) return;
let i = 0, curX = 0, curY = 0, lastCmd = '';
const out = [];
const specs = { M:2, L:2, T:2, H:1, V:1, C:6, S:4, Q:4, A:7, Z:0 };
const take = () => tokens[i++];
const isCmd = (t) => /^[a-zA-Z]$/.test(t);
const toAbs = (x, y, rel) => rel ? [x + curX, y + curY] : [x, y];
while (i < tokens.length) {
let cmd = take();
if (!isCmd(cmd)) { i--; cmd = lastCmd; } else { lastCmd = cmd; }
const ABS = cmd.toUpperCase();
const rel = cmd !== ABS;
if (ABS === 'Z') { out.push('Z'); continue; }
const nPer = specs[ABS];
if (!nPer) continue;
let buf = [];
while (i < tokens.length && !isCmd(tokens[i])) {
buf.push(parseFloat(take()));
if (buf.length % nPer === 0) {
const n = buf.splice(buf.length - nPer, nPer);
switch (ABS) {
case 'M':
case 'L':
case 'T': {
let [x, y] = toAbs(n[0], n[1], rel);
out.push(ABS, round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'H': {
let x = rel ? curX + n[0] : n[0];
out.push('H', round3(x));
curX = x;
break;
}
case 'V': {
let y = rel ? curY + n[0] : n[0];
out.push('V', round3(y));
curY = y;
break;
}
case 'C': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x2, y2] = toAbs(n[2], n[3], rel);
let [x, y ] = toAbs(n[4], n[5], rel);
out.push('C',
round3(x1), round3(y1),
round3(x2), round3(y2),
round3(x), round3(y)
);
curX = x; curY = y;
break;
}
case 'S': {
let [x2, y2] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
out.push('S', round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'Q': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
out.push('Q', round3(x1), round3(y1), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'A': {
const rx = n[0], ry = n[1], rot = n[2], laf = n[3], sf = n[4];
let [x, y] = toAbs(n[5], n[6], rel);
out.push('A', rx, ry, rot, laf, sf, round3(x), round3(y));
curX = x; curY = y;
break;
}
}
}
}
}
pathEl.setAttribute('d', out.join(' ').replace(/\s+/g, ' ').trim());
pathEl.dataset.absNormalized = '1';
}
function adjustPathXInsideBox_World(pathEl, minX, maxX, deltaX, worldOffsetX) {
if (!pathEl || !deltaX) return;
const d = pathEl.getAttribute('d');
if (!d) return;
const tokens = parseTokens(d);
if (!tokens || !tokens.length) return;
let i = 0, curX = 0, curY = 0, lastCmd = '';
const out = [];
const specs = { M:2, L:2, T:2, H:1, V:1, C:6, S:4, Q:4, A:7, Z:0 };
const take = () => tokens[i++];
const isCmd = (t) => /^[a-zA-Z]$/.test(t);
const insideWorld = (localX) => {
const worldX = localX + worldOffsetX;
return worldX >= minX && worldX <= maxX;
};
while (i < tokens.length) {
let cmd = take();
if (!isCmd(cmd)) { i--; cmd = lastCmd; } else { lastCmd = cmd; }
const ABS = cmd.toUpperCase();
const rel = cmd !== ABS;
if (ABS === 'Z') { out.push('Z'); continue; }
const nPer = specs[ABS];
if (!nPer) continue;
const toAbs = (x, y) => rel ? [x + curX, y + curY] : [x, y];
let buf = [];
while (i < tokens.length && !isCmd(tokens[i])) {
buf.push(parseFloat(take()));
if (buf.length % nPer === 0) {
const n = buf.splice(buf.length - nPer, nPer);
switch (ABS) {
case 'M':
case 'L':
case 'T': {
let [x, y] = toAbs(n[0], n[1]);
if (insideWorld(x)) x += deltaX;
out.push(ABS, round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'H': {
let x = rel ? curX + n[0] : n[0];
if (insideWorld(x)) x += deltaX;
out.push('H', round3(x));
curX = x;
break;
}
case 'V': {
let y = rel ? curY + n[0] : n[0];
out.push('V', round3(y));
curY = y;
break;
}
case 'C': {
let [x1, y1] = toAbs(n[0], n[1]);
let [x2, y2] = toAbs(n[2], n[3]);
let [x, y ] = toAbs(n[4], n[5]);
if (insideWorld(x1)) x1 += deltaX;
if (insideWorld(x2)) x2 += deltaX;
if (insideWorld(x )) x += deltaX;
out.push('C',
round3(x1), round3(y1),
round3(x2), round3(y2),
round3(x), round3(y)
);
curX = x; curY = y;
break;
}
case 'S': {
let [x2, y2] = toAbs(n[0], n[1]);
let [x, y ] = toAbs(n[2], n[3]);
if (insideWorld(x2)) x2 += deltaX;
if (insideWorld(x )) x += deltaX;
out.push('S', round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'Q': {
let [x1, y1] = toAbs(n[0], n[1]);
let [x, y ] = toAbs(n[2], n[3]);
if (insideWorld(x1)) x1 += deltaX;
if (insideWorld(x )) x += deltaX;
out.push('Q', round3(x1), round3(y1), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'A': {
const rx = n[0], ry = n[1], rot = n[2], laf = n[3], sf = n[4];
let [x, y] = toAbs(n[5], n[6]);
if (insideWorld(x)) x += deltaX;
out.push('A', rx, ry, rot, laf, sf, round3(x), round3(y));
curX = x; curY = y;
break;
}
}
}
}
}
pathEl.setAttribute('d', out.join(' ').replace(/\s+/g, ' ').trim());
}
// -------------------- Door logic (svg#dorpel) --------------------
function applyDoorDiff(diff) {
if (!diff) return;
const g = document.querySelector('svg g#HS-vastedeur');
if (!g) return;
const svg = g.ownerSVGElement || document.querySelector('svg');
if (!svg) return;
const shiftRect = svg.querySelector('#x-shiftbox-sponning-HS-deur rect');
if (!shiftRect) return;
const minX = toNum(shiftRect.getAttribute('x'));
const w = toNum(shiftRect.getAttribute('width'));
if (minX === null || w === null) return;
const maxX = minX + w;
const { x: txBefore, y: tyBefore } = parseTranslate(g.getAttribute('transform'));
const path = g.querySelector(':scope > path#HSvullat');
if (path) normalizePathToAbsoluteOnce(path);
const deltaX = +diff;
if (path) {
adjustPathXInsideBox_World(path, minX, maxX, deltaX, txBefore);
}
const firstRect = getDirectChild(g, 'rect');
if (firstRect) {
const x0 = toNum(firstRect.getAttribute('x'));
if (x0 !== null) {
const worldX = x0 + txBefore;
if (worldX >= minX && worldX <= maxX) {
firstRect.setAttribute('x', String(x0 + deltaX));
}
}
}
setTranslate(g, txBefore - diff, tyBefore);
}
// -------------------- Extra logic (svg#alle-vakken) --------------------
function applyDiffToAlleVakken(diff) {
if (!diff) return;
const svg = document.querySelector('svg#alle-vakken');
if (!svg) return;
const vakSelector =
'g[id^="vak-"][id*="-HS-LS"], g[id^="vak-"][id*="-HS-RS"], g[id^="vak-"][id*="-HS-LR"]';
svg.querySelectorAll(vakSelector).forEach((g) => {
const rects = g.querySelectorAll('rect[data-name="vastedeur"], rect[id^="vastedeur"]');
rects.forEach((r) => {
shiftRectYOrTranslateYByDiff(r, diff);
const fill = r.getAttribute('fill') || '';
const gradId = parseUrlId(fill);
if (gradId) shiftGradientYByDiff(svg, gradId, diff);
});
});
}
// -------------------- Main apply --------------------
function applyDiffToSvg(diff, correctedYFactor = 1) {
const correctedDiff = diff * correctedYFactor;
// 1) NEUT polygons: per group indices
document.querySelectorAll('svg polygon[id^="neut"]').forEach((poly) => {
const gid = poly.closest('g[id]')?.id || '';
const indices = isLRGroup(gid) ? INDICES_NEUT_LR : (isTGroup(gid) ? INDICES_NEUT_T : null);
if (!indices) return;
shiftPolygonByDiff(poly, indices, correctedDiff);
});
// 2) VASTBASIS polygons: indices 1,3,5,7,9,11
document.querySelectorAll('svg polygon[id^="vastbasis"]').forEach((poly) => {
shiftPolygonByDiff(poly, INDICES_VASTBASIS, correctedDiff);
});
// 3) Vastnok rects (dorpel svg)
document
.querySelectorAll('svg rect[data-name="vastnok"], svg rect[id^="vastnok"]')
.forEach((r) => shiftRectYByDiff(r, correctedDiff));
// 4) Door (dorpel svg)
applyDoorDiff(diff);
// 5) Extra: alle-vakken svg
applyDiffToAlleVakken(diff);
}
// ====== Onderhuidse delta vanuit andere scripts (bv. deurdikte) ======
document.addEventListener('hs:applyTussenmaatDelta', (e) => {
const diff = e?.detail?.diff;
if (!Number.isFinite(diff) || diff === 0) return;
// Vanuit form_fields[deurdikte]:
// neut / vastbasis / vastnok moeten factor x2 blijven bewegen.
applyDiffToSvg(diff, 2);
});
// -------------------- Input (number) delta --------------------
function clamp(n, min, max) {
if (!Number.isFinite(n)) return null;
if (Number.isFinite(min)) n = Math.max(min, n);
if (Number.isFinite(max)) n = Math.min(max, n);
return n;
}
function init() {
const input = document.getElementById(INPUT_ID);
if (!input) return;
const minVal = toNum(input.getAttribute('min'));
const maxVal = toNum(input.getAttribute('max'));
if (input.dataset.prev == null) {
const v0 = clamp(toNum(input.value), minVal, maxVal);
input.dataset.prev = String(v0 ?? 0);
}
const handler = () => {
const newValRaw = toNum(input.value);
const newVal = clamp(newValRaw, minVal, maxVal);
if (newVal === null) return;
const prevVal = toNum(input.dataset.prev);
if (prevVal === null) { input.dataset.prev = String(newVal); return; }
const diff = newVal - prevVal;
if (!diff) return;
// Vanuit form_fields[hstussenmaat]:
// normale factor 1 voor neut / vastbasis / vastnok.
applyDiffToSvg(diff, 1);
input.dataset.prev = String(newVal);
};
input.addEventListener('input', handler);
input.addEventListener('change', handler);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();
//// Tussenmaat HS deuren ////
(() => {
const INPUT_ID = 'form-field-hstussenmaat';
// === AANGEPASTE indices (zoals gevraagd) ===
// Binnen #neut*:
// - LR-group -> indices 5,7
// - T-group -> indices 17,19
const INDICES_NEUT_LR = [5, 7];
const INDICES_NEUT_T = [17, 19];
// Binnen #vastbasis* -> indices 1,3,5,7,9,11
const INDICES_VASTBASIS = [1, 3, 5, 7, 9, 11];
// Group-detectie ongewijzigd
const isLRGroup = (gid) => gid.includes('HS-L-V') || gid.includes('HS-R-V');
const T_KEYS = ['HS-T1-UV','HS-T1-VU','HS-T2-UV','HS-T2-VU','HS-T3-UV','HS-T3-VU','HS-T4-UV','HS-T4-VU'];
const isTGroup = (gid) => T_KEYS.some(k => gid.includes(k));
const toNum = (v) => {
const n = Number(v);
return Number.isFinite(n) ? n : null;
};
// -------------------- POLYGON helpers --------------------
const parsePoints = (s) => (s || '').trim().split(/[\s,]+/).map(Number);
const serializePoints = (arr) => arr.join(' ');
function shiftPolygonByDiff(poly, flatIndices, diff) {
if (!diff) return;
const ptsStr = poly.getAttribute('points');
if (!ptsStr) return;
const pts = parsePoints(ptsStr);
let changed = false;
for (const idx of flatIndices) {
if (idx >= 0 && idx < pts.length && Number.isFinite(pts[idx])) {
pts[idx] += diff;
changed = true;
}
}
if (changed) poly.setAttribute('points', serializePoints(pts));
}
// -------------------- RECT helper --------------------
function shiftRectYByDiff(rect, diff) {
if (!diff) return;
const y0 = toNum(rect.getAttribute('y'));
if (y0 === null) return;
rect.setAttribute('y', String(y0 + diff));
}
// -------------------- TRANSLATE helpers --------------------
function parseTranslate(transformStr) {
const m = /translate\(\s*([-\d.]+)(?:[\s,]+([-\d.]+))?\s*\)/.exec(transformStr || '');
if (!m) return { x: 0, y: 0 };
return { x: Number(m[1]), y: m[2] !== undefined ? Number(m[2]) : 0 };
}
function setTranslate(el, x, y) {
const t = el.getAttribute('transform') || '';
const re = /translate\(\s*([-\d.]+)(?:[\s,]+([-\d.]+))?\s*\)/;
if (re.test(t)) {
el.setAttribute('transform', t.replace(re, `translate(${x},${y})`));
} else {
el.setAttribute('transform', t ? `translate(${x},${y}) ${t}` : `translate(${x},${y})`);
}
}
function shiftTranslateYByDiff(el, diff) {
if (!diff) return;
const { x, y } = parseTranslate(el.getAttribute('transform'));
setTranslate(el, x, y + diff);
}
// Shift rect by translate-y if present, otherwise by y attribute
function shiftRectYOrTranslateYByDiff(rect, diff) {
if (!diff || !rect) return;
const t = rect.getAttribute('transform') || '';
if (/translate\(/.test(t)) {
shiftTranslateYByDiff(rect, diff);
return;
}
const y0 = toNum(rect.getAttribute('y'));
if (y0 === null) return;
rect.setAttribute('y', String(y0 + diff));
}
// -------------------- GRADIENT helpers --------------------
function parseUrlId(fillValue) {
const m = /url\(\s*#([^)]+)\s*\)/.exec(fillValue || '');
return m ? m[1] : null;
}
function shiftGradientYByDiff(svg, gradientId, diff) {
if (!svg || !gradientId || !diff) return;
const grad = svg.querySelector(`#${CSS.escape(gradientId)}`);
if (!grad) return;
const gt = grad.getAttribute('gradientTransform') || '';
if (/translate\(/.test(gt)) {
const { x, y } = parseTranslate(gt);
const re = /translate\(\s*([-\d.]+)(?:[\s,]+([-\d.]+))?\s*\)/;
const newGT = re.test(gt)
? gt.replace(re, `translate(${x},${y + diff})`)
: `translate(${x},${y + diff}) ${gt}`.trim();
grad.setAttribute('gradientTransform', newGT);
return;
}
const y1 = toNum(grad.getAttribute('y1'));
const y2 = toNum(grad.getAttribute('y2'));
if (y1 !== null) grad.setAttribute('y1', String(y1 + diff));
if (y2 !== null) grad.setAttribute('y2', String(y2 + diff));
const cy = toNum(grad.getAttribute('cy'));
const fy = toNum(grad.getAttribute('fy'));
if (cy !== null) grad.setAttribute('cy', String(cy + diff));
if (fy !== null) grad.setAttribute('fy', String(fy + diff));
}
// -------------------- PATH helpers --------------------
function parseTokens(d) {
return d?.match(/[a-zA-Z]|-?\d*\.?\d+(?:e[-+]?\d+)?/g);
}
function round3(v) { return Math.round(v * 1000) / 1000; }
function getDirectChild(g, tagName) {
for (const node of g.children) {
if (node.tagName && node.tagName.toLowerCase() === tagName.toLowerCase()) return node;
}
return null;
}
function normalizePathToAbsoluteOnce(pathEl) {
if (!pathEl) return;
if (pathEl.dataset.absNormalized === '1') return;
const d = pathEl.getAttribute('d');
if (!d) return;
const tokens = parseTokens(d);
if (!tokens || !tokens.length) return;
let i = 0, curX = 0, curY = 0, lastCmd = '';
const out = [];
const specs = { M:2, L:2, T:2, H:1, V:1, C:6, S:4, Q:4, A:7, Z:0 };
const take = () => tokens[i++];
const isCmd = (t) => /^[a-zA-Z]$/.test(t);
const toAbs = (x, y, rel) => rel ? [x + curX, y + curY] : [x, y];
while (i < tokens.length) {
let cmd = take();
if (!isCmd(cmd)) { i--; cmd = lastCmd; } else { lastCmd = cmd; }
const ABS = cmd.toUpperCase();
const rel = cmd !== ABS;
if (ABS === 'Z') { out.push('Z'); continue; }
const nPer = specs[ABS];
if (!nPer) continue;
let buf = [];
while (i < tokens.length && !isCmd(tokens[i])) {
buf.push(parseFloat(take()));
if (buf.length % nPer === 0) {
const n = buf.splice(buf.length - nPer, nPer);
switch (ABS) {
case 'M':
case 'L':
case 'T': {
let [x, y] = toAbs(n[0], n[1], rel);
out.push(ABS, round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'H': {
let x = rel ? curX + n[0] : n[0];
out.push('H', round3(x));
curX = x;
break;
}
case 'V': {
let y = rel ? curY + n[0] : n[0];
out.push('V', round3(y));
curY = y;
break;
}
case 'C': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x2, y2] = toAbs(n[2], n[3], rel);
let [x, y ] = toAbs(n[4], n[5], rel);
out.push('C',
round3(x1), round3(y1),
round3(x2), round3(y2),
round3(x), round3(y)
);
curX = x; curY = y;
break;
}
case 'S': {
let [x2, y2] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
out.push('S', round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'Q': {
let [x1, y1] = toAbs(n[0], n[1], rel);
let [x, y ] = toAbs(n[2], n[3], rel);
out.push('Q', round3(x1), round3(y1), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'A': {
const rx = n[0], ry = n[1], rot = n[2], laf = n[3], sf = n[4];
let [x, y] = toAbs(n[5], n[6], rel);
out.push('A', rx, ry, rot, laf, sf, round3(x), round3(y));
curX = x; curY = y;
break;
}
}
}
}
}
pathEl.setAttribute('d', out.join(' ').replace(/\s+/g, ' ').trim());
pathEl.dataset.absNormalized = '1';
}
function adjustPathXInsideBox_World(pathEl, minX, maxX, deltaX, worldOffsetX) {
if (!pathEl || !deltaX) return;
const d = pathEl.getAttribute('d');
if (!d) return;
const tokens = parseTokens(d);
if (!tokens || !tokens.length) return;
let i = 0, curX = 0, curY = 0, lastCmd = '';
const out = [];
const specs = { M:2, L:2, T:2, H:1, V:1, C:6, S:4, Q:4, A:7, Z:0 };
const take = () => tokens[i++];
const isCmd = (t) => /^[a-zA-Z]$/.test(t);
const insideWorld = (localX) => {
const worldX = localX + worldOffsetX;
return worldX >= minX && worldX <= maxX;
};
while (i < tokens.length) {
let cmd = take();
if (!isCmd(cmd)) { i--; cmd = lastCmd; } else { lastCmd = cmd; }
const ABS = cmd.toUpperCase();
const rel = cmd !== ABS;
if (ABS === 'Z') { out.push('Z'); continue; }
const nPer = specs[ABS];
if (!nPer) continue;
const toAbs = (x, y) => rel ? [x + curX, y + curY] : [x, y];
let buf = [];
while (i < tokens.length && !isCmd(tokens[i])) {
buf.push(parseFloat(take()));
if (buf.length % nPer === 0) {
const n = buf.splice(buf.length - nPer, nPer);
switch (ABS) {
case 'M':
case 'L':
case 'T': {
let [x, y] = toAbs(n[0], n[1]);
if (insideWorld(x)) x += deltaX;
out.push(ABS, round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'H': {
let x = rel ? curX + n[0] : n[0];
if (insideWorld(x)) x += deltaX;
out.push('H', round3(x));
curX = x;
break;
}
case 'V': {
let y = rel ? curY + n[0] : n[0];
out.push('V', round3(y));
curY = y;
break;
}
case 'C': {
let [x1, y1] = toAbs(n[0], n[1]);
let [x2, y2] = toAbs(n[2], n[3]);
let [x, y ] = toAbs(n[4], n[5]);
if (insideWorld(x1)) x1 += deltaX;
if (insideWorld(x2)) x2 += deltaX;
if (insideWorld(x )) x += deltaX;
out.push('C',
round3(x1), round3(y1),
round3(x2), round3(y2),
round3(x), round3(y)
);
curX = x; curY = y;
break;
}
case 'S': {
let [x2, y2] = toAbs(n[0], n[1]);
let [x, y ] = toAbs(n[2], n[3]);
if (insideWorld(x2)) x2 += deltaX;
if (insideWorld(x )) x += deltaX;
out.push('S', round3(x2), round3(y2), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'Q': {
let [x1, y1] = toAbs(n[0], n[1]);
let [x, y ] = toAbs(n[2], n[3]);
if (insideWorld(x1)) x1 += deltaX;
if (insideWorld(x )) x += deltaX;
out.push('Q', round3(x1), round3(y1), round3(x), round3(y));
curX = x; curY = y;
break;
}
case 'A': {
const rx = n[0], ry = n[1], rot = n[2], laf = n[3], sf = n[4];
let [x, y] = toAbs(n[5], n[6]);
if (insideWorld(x)) x += deltaX;
out.push('A', rx, ry, rot, laf, sf, round3(x), round3(y));
curX = x; curY = y;
break;
}
}
}
}
}
pathEl.setAttribute('d', out.join(' ').replace(/\s+/g, ' ').trim());
}
// -------------------- Door logic (svg#dorpel) --------------------
function applyDoorDiff(diff) {
if (!diff) return;
const g = document.querySelector('svg g#HS-vastedeur');
if (!g) return;
const svg = g.ownerSVGElement || document.querySelector('svg');
if (!svg) return;
const shiftRect = svg.querySelector('#x-shiftbox-sponning-HS-deur rect');
if (!shiftRect) return;
const minX = toNum(shiftRect.getAttribute('x'));
const w = toNum(shiftRect.getAttribute('width'));
if (minX === null || w === null) return;
const maxX = minX + w;
const { x: txBefore, y: tyBefore } = parseTranslate(g.getAttribute('transform'));
const path = g.querySelector(':scope > path#HSvullat');
if (path) normalizePathToAbsoluteOnce(path);
const deltaX = +diff;
if (path) {
adjustPathXInsideBox_World(path, minX, maxX, deltaX, txBefore);
}
const firstRect = getDirectChild(g, 'rect');
if (firstRect) {
const x0 = toNum(firstRect.getAttribute('x'));
if (x0 !== null) {
const worldX = x0 + txBefore;
if (worldX >= minX && worldX <= maxX) {
firstRect.setAttribute('x', String(x0 + deltaX));
}
}
}
setTranslate(g, txBefore - diff, tyBefore);
}
// -------------------- Extra logic (svg#alle-vakken) --------------------
function applyDiffToAlleVakken(diff) {
if (!diff) return;
const svg = document.querySelector('svg#alle-vakken');
if (!svg) return;
const vakSelector =
'g[id^="vak-"][id*="-HS-LS"], g[id^="vak-"][id*="-HS-RS"], g[id^="vak-"][id*="-HS-LR"]';
svg.querySelectorAll(vakSelector).forEach((g) => {
const rects = g.querySelectorAll('rect[data-name="vastedeur"], rect[id^="vastedeur"]');
rects.forEach((r) => {
shiftRectYOrTranslateYByDiff(r, diff);
const fill = r.getAttribute('fill') || '';
const gradId = parseUrlId(fill);
if (gradId) shiftGradientYByDiff(svg, gradId, diff);
});
});
}
// -------------------- Main apply --------------------
function applyDiffToSvg(diff) {
// 1) NEUT polygons: per group indices
document.querySelectorAll('svg polygon[id^="neut"]').forEach((poly) => {
const gid = poly.closest('g[id]')?.id || '';
const indices = isLRGroup(gid) ? INDICES_NEUT_LR : (isTGroup(gid) ? INDICES_NEUT_T : null);
if (!indices) return;
shiftPolygonByDiff(poly, indices, diff);
});
// 2) VASTBASIS polygons: indices 1,3,5,7,9,11
document.querySelectorAll('svg polygon[id^="vastbasis"]').forEach((poly) => {
shiftPolygonByDiff(poly, INDICES_VASTBASIS, diff);
});
// 3) Vastnok rects (dorpel svg)
document
.querySelectorAll('svg rect[data-name="vastnok"], svg rect[id^="vastnok"]')
.forEach((r) => shiftRectYByDiff(r, diff));
// 4) Door (dorpel svg)
applyDoorDiff(diff);
// 5) Extra: alle-vakken svg
applyDiffToAlleVakken(diff);
}
// ====== Onderhuidse delta vanuit andere scripts (bv. deurdikte) ======
document.addEventListener('hs:applyTussenmaatDelta', (e) => {
const diff = e?.detail?.diff;
if (!Number.isFinite(diff) || diff === 0) return;
applyDiffToSvg(diff);
});
// -------------------- Input (number) delta --------------------
function clamp(n, min, max) {
if (!Number.isFinite(n)) return null;
if (Number.isFinite(min)) n = Math.max(min, n);
if (Number.isFinite(max)) n = Math.min(max, n);
return n;
}
function init() {
const input = document.getElementById(INPUT_ID);
if (!input) return;
const minVal = toNum(input.getAttribute('min'));
const maxVal = toNum(input.getAttribute('max'));
if (input.dataset.prev == null) {
const v0 = clamp(toNum(input.value), minVal, maxVal);
input.dataset.prev = String(v0 ?? 0);
}
const handler = () => {
const newValRaw = toNum(input.value);
const newVal = clamp(newValRaw, minVal, maxVal);
if (newVal === null) return;
const prevVal = toNum(input.dataset.prev);
if (prevVal === null) { input.dataset.prev = String(newVal); return; }
const diff = newVal - prevVal;
if (!diff) return;
applyDiffToSvg(diff);
input.dataset.prev = String(newVal);
};
input.addEventListener('input', handler);
input.addEventListener('change', handler);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();
//// POPUPS BOORGATEN + AWATERING icm C3-STRIP ////
const CFG_C3 = {
svgId: 'dorpel',
inputSelectors: [
'#form-field-dorpeldikte',
'#form-field-dorpelbreedte',
'input[name="form_fields[c3strip]"]',
'input[name="form_fields[dorpeltype]"]',
'input[name="form_fields[aantal_loodsponningen]"]',
// ✅ nieuw: vakken keuze triggert ook een re-check
'input[name="form_fields[vakken]"]'
],
// ✅ gate: boorgaten-popup pas tonen nadat vakken is gekozen
vakkenSelector: 'input[name="form_fields[vakken]"]',
popupBoorgatenSelector: '#message-popup-BRG-C3',
popupAfwateringsgatenSelector: '#message-popup-AFW-C3',
c3Selector: '#C3-strip',
boorgatenSelector: '[id^="boorgat-"]',
afwaterMargeSelectors: [
'#BUVD_x5F_afwater_x5F_marge',
'#BIV_x5F_afwater_x5F_marge'
],
clearance: 0,
debug: false
};
/* ---------- Popups ---------- */
function togglePopup(sel, show) {
const el = document.querySelector(sel);
if (!el) return;
el.style.display = show ? 'block' : 'none';
}
/* ---------- AABB (boorgaten) ---------- */
function boxesTouchOrOverlap(a, b, clearance) {
return !(
a.right < b.left + clearance ||
a.left > b.right - clearance ||
a.bottom < b.top + clearance ||
a.top > b.bottom - clearance
);
}
function getBBoxAbs(el) {
const ctm = el.getCTM();
if (!ctm) return null;
const b = el.getBBox();
const pts = [
{ x: b.x, y: b.y },
{ x: b.x + b.width, y: b.y },
{ x: b.x + b.width, y: b.y + b.height },
{ x: b.x, y: b.y + b.height }
].map(p => ({
x: p.x * ctm.a + p.y * ctm.c + ctm.e,
y: p.x * ctm.b + p.y * ctm.d + ctm.f
}));
const xs = pts.map(p => p.x);
const ys = pts.map(p => p.y);
return {
left: Math.min(...xs),
right: Math.max(...xs),
top: Math.min(...ys),
bottom: Math.max(...ys)
};
}
/* ---------- SAT (rotatie-bewust) ---------- */
function getBoxCornersAbs(el) {
const ctm = el.getCTM();
if (!ctm) return null;
const b = el.getBBox();
const cornersLocal = [
{ x: b.x, y: b.y },
{ x: b.x + b.width, y: b.y },
{ x: b.x + b.width, y: b.y + b.height },
{ x: b.x, y: b.y + b.height }
];
return cornersLocal.map(p => ({
x: p.x * ctm.a + p.y * ctm.c + ctm.e,
y: p.x * ctm.b + p.y * ctm.d + ctm.f
}));
}
function projectOnAxis(points, axis) {
let min = Infinity, max = -Infinity;
for (const p of points) {
const d = p.x * axis.x + p.y * axis.y;
if (d < min) min = d;
if (d > max) max = d;
}
return { min, max };
}
function rectOverlapSAT(pointsA, pointsB, clearance = 0) {
const axes = [];
const addAxesFrom = (pts) => {
for (let i = 0; i < 2; i++) {
const p1 = pts[i], p2 = pts[(i + 1) % 4];
const nx = -(p2.y - p1.y);
const ny = (p2.x - p1.x);
const len = Math.hypot(nx, ny) || 1;
const ax = { x: nx / len, y: ny / len };
const key = ax.x.toFixed(6) + ',' + ax.y.toFixed(6);
if (!axes.some(a => a.k === key)) axes.push({ ...ax, k: key });
}
};
addAxesFrom(pointsA);
addAxesFrom(pointsB);
for (const axis of axes) {
const a = projectOnAxis(pointsA, axis);
const b = projectOnAxis(pointsB, axis);
if (a.max < b.min - clearance || b.max < a.min - clearance) return false;
}
return true;
}
/* ---------- Misc ---------- */
function isHiddenSvg(el) {
if (!el) return true;
const dispAttr = (el.getAttribute('display') || '').toLowerCase();
if (dispAttr === 'none') return true;
const dispCss = (getComputedStyle(el).display || '').toLowerCase();
return dispCss === 'none';
}
function getSelectedDorpeltypeOriginal() {
const checked = document.querySelector('input[name="form_fields[dorpeltype]"]:checked');
if (!checked) return null;
return (checked.dataset.originalValue || checked.value || '').toUpperCase();
}
// ✅ nieuw: is er een vakken-keuze gemaakt?
function hasVakkenSelection() {
return !!document.querySelector(`${CFG_C3.vakkenSelector}:checked`);
}
/* ---------- Core check ---------- */
function checkC3CollisionsAndToggle() {
const svg = document.getElementById(CFG_C3.svgId);
if (!svg) return;
const c3Group = svg.querySelector(CFG_C3.c3Selector);
const c3Rect = c3Group ? c3Group.querySelector('rect') : null;
if (!c3Group || !c3Rect || isHiddenSvg(c3Group)) {
togglePopup(CFG_C3.popupBoorgatenSelector, false);
togglePopup(CFG_C3.popupAfwateringsgatenSelector, false);
return;
}
const c3Box = getBBoxAbs(c3Rect);
// ---- 1) Boorgaten (AABB) ----
const boorgaten = [...svg.querySelectorAll(CFG_C3.boorgatenSelector)];
let hitBoorgat = false;
if (c3Box && boorgaten.length) {
for (const g of boorgaten) {
const gb = getBBoxAbs(g);
if (gb && boxesTouchOrOverlap(c3Box, gb, CFG_C3.clearance)) {
hitBoorgat = true;
break;
}
}
}
// ✅ Gate: popup pas tonen na vakken-selectie
togglePopup(CFG_C3.popupBoorgatenSelector, hitBoorgat && hasVakkenSelection());
// ---- 2) Afwater-marge (SAT) ----
const originalType = getSelectedDorpeltypeOriginal();
const excluded = ['KVT', 'HS'];
if (excluded.includes(originalType)) {
togglePopup(CFG_C3.popupAfwateringsgatenSelector, false);
return;
}
const c3Pts = getBoxCornersAbs(c3Rect);
let hitAfwater = false;
if (c3Pts) {
const selectorsToCheck = [];
if (originalType === 'BUVD') selectorsToCheck.push('#BUVD_x5F_afwater_x5F_marge');
else if (originalType === 'BI' || originalType === 'BIV') selectorsToCheck.push('#BIV_x5F_afwater_x5F_marge');
else selectorsToCheck.push(...CFG_C3.afwaterMargeSelectors);
for (const sel of selectorsToCheck) {
const margeEl = svg.querySelector(sel);
if (!margeEl) continue;
const margePts = getBoxCornersAbs(margeEl);
if (!margePts) continue;
if (rectOverlapSAT(c3Pts, margePts, CFG_C3.clearance)) {
hitAfwater = true;
if (CFG_C3.debug) console.log('[C3] HIT afwater marge:', sel);
break;
}
}
}
// afwater-popup blijft zoals het was (geen vakken-gate)
togglePopup(CFG_C3.popupAfwateringsgatenSelector, hitAfwater);
}
/* ---------- Wiring (throttled) ---------- */
function setupC3Alerts() {
let rafPending = false;
const requestCheck = () => {
if (rafPending) return;
rafPending = true;
requestAnimationFrame(() => {
rafPending = false;
checkC3CollisionsAndToggle();
});
};
CFG_C3.inputSelectors.forEach(sel => {
document.querySelectorAll(sel).forEach(el => {
el.addEventListener('input', requestCheck);
el.addEventListener('change', requestCheck);
});
});
document.addEventListener('change', requestCheck);
const formRoot = document.querySelector('.elementor-form');
if (formRoot) {
const mo = new MutationObserver(requestCheck);
mo.observe(formRoot, { childList: true, subtree: true, attributes: true });
}
requestCheck();
}
document.addEventListener('DOMContentLoaded', setupC3Alerts);
document.addEventListener("DOMContentLoaded", function () {
function checkDropdowns() {
// Commenting out sessionStorage limit for dtstimber
//let popupShown = sessionStorage.getItem("popupShown");
//if (popupShown) return;
let dropdowns = document.querySelectorAll("select[name*='VVU']:not(.cfef-hidden), select[name*='VVI']:not(.cfef-hidden)");
let visibleDropdowns = Array.from(dropdowns).filter(select => select.offsetParent !== null);
let allFilled = visibleDropdowns.every(select => select.value.toLowerCase() !== 'vul in');
if (allFilled && visibleDropdowns.length > 0) {
showPopup();
simulateMouseMovement();
}
}
function showPopup() {
let popup = document.getElementById("tooltip-popup");
if (popup) {
setTimeout(() => {
popup.style.display = "block";
}, 50); // ⬅️ Delay forces Firefox to properly register the display change
document.addEventListener("click", hidePopup, { once: true });
setTimeout(hidePopup, 5000);
}
}
function hidePopup() {
let popup = document.getElementById("tooltip-popup");
if (popup) {
popup.style.display = "none";
}
}
function simulateMouseMovement() {
let hitboxes = document.querySelectorAll("svg#alle-vakken .hitbox");
if (hitboxes.length === 0) return;
let positions = Array.from(hitboxes).map(hitbox => {
let rect = hitbox.getBoundingClientRect();
return { x: rect.x, width: rect.width };
});
positions.sort((a, b) => a.x - b.x); // Sort hitboxes by X position
let startX = positions[0].x;
let endX = positions[positions.length - 1].x + positions[positions.length - 1].width;
let svg = document.querySelector("svg#alle-vakken");
if (!svg) return;
let cursor = document.createElement("div");
cursor.innerHTML = `
`;
cursor.style.position = "fixed";
cursor.style.zIndex = "10000";
document.body.appendChild(cursor);
let duration = 2500; // 1.5 seconds per movement
let frameRate = 60; // Smooth animation
let frameTime = 1000 / frameRate;
let steps = (duration / frameTime);
function easeInOutQuad(t) {
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
}
function animateCursor(start, end, callback) {
let step = 0;
let moveCursor = setInterval(() => {
let progress = easeInOutQuad(step / steps);
let currentX = start + (end - start) * progress + 100;
let currentY = svg.getBoundingClientRect().top + 20 + 30;
cursor.style.left = `${currentX}px`;
cursor.style.top = `${currentY}px`;
step++;
if (step > steps) {
clearInterval(moveCursor);
if (callback) callback();
}
}, frameTime);
}
animateCursor(startX, endX, () => {
animateCursor(endX, startX, () => {
document.body.removeChild(cursor);
});
});
}
document.addEventListener("change", function (event) {
if (event.target.matches("select[name*='VVU'], select[name*='VVI']")) {
checkDropdowns();
}
});
});