v0.7.1: UI-overhaul, ribbon vervangen door de indeling van CAD/BIM-pakketten met elke functie op één plek: bovenbalk met menu Bestand (nieuw, openen, opslaan, exporteren, projectgegevens, kosten & tijden) · Beeld · Help, de ontwerpen als tabs en de knop Opslaan; links de structuur met inklapbare groepen (onthouden, groep met de selectie klapt vanzelf open); werkbalk boven de viewports met gereedschap (Selecteren V, Binnenwand W, Ruimte R, Inrichting I, opties van het gereedschap ernaast, Unit toevoegen) en weergave (één Lagen-menu met bouwfase voor 2D en 3D, Omgeving, Doorsnede X, Render, Lopen L, Rapport 4, 2D + 3D / 2D / 3D met 1/2/3); rechts het eigenschappenpaneel (Inspector: selectie met kruimelpad, of de woning met kerncijfers en inklapbare secties Gevel, Dak, Binnen, Verlichting, Elektra, Constructie); statusbalk met hint en meldingen; wand- en vloer/dak-editor in dezelfde indeling (paneel rechts met tab Resultaat, terug met ← Woning of Esc); sneltoetsenoverzicht (?), Ctrl+O, Ctrl+\ panelen verbergen; rapport als masonry; dubbele functies weg (lagenbalk, Beeld-tab en Weergave-filter, export en kosten op twee plekken, Snel nieuw); oude onderdelen gelijkgetrokken (dialoogknoppen, materiaalknoppen, chassis als schakelaar, eenheden); weergavelagen (ViewLayers) i.p.v. bouwfasen, elk los aan/uit; render-export met Blender-script voor de lampen; elektraleidingen boven een vals plafond en haaks binnen de units bij een L/T-vorm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
adb3c74f85
commit
ab0dca9bb5
+117
-17
@@ -143,9 +143,30 @@ window.ElektraModel = (() => {
|
||||
return null;
|
||||
}
|
||||
|
||||
function makeRouter(units) {
|
||||
// rooms: leidingen lopen boven een vals plafond, niet tegen de onderkant van een hoog dak.
|
||||
function makeRouter(units, rooms) {
|
||||
const unitAt = (x, z) => units.find(u => x >= u.rect.x0 - 1 && x <= u.rect.x1 + 1 && z >= u.rect.z0 - 1 && z <= u.rect.z1 + 1) || null;
|
||||
const ceilAt = (x, z) => {
|
||||
// Vals plafond (mm boven vloer) van de ruimte op (x,z): undefined = geen ruimte (binnenwand, naad), null = geen vals plafond.
|
||||
const roomPlenum = (x, z) => { const r = rooms && rooms.roomAt ? rooms.roomAt(x, z) : null; return r ? (r.ceilingActive ? r.ceiling : null) : undefined; };
|
||||
// Absoluut; in een binnenwand het laagste valse plafond ernaast: de leiding gaat daar door de wand, niet omhoog.
|
||||
const plenumAt = (x, z) => {
|
||||
const u = unitAt(x, z);
|
||||
if (!u) return null;
|
||||
let v = roomPlenum(x, z);
|
||||
if (v === undefined) {
|
||||
v = null;
|
||||
[[1, 0], [-1, 0], [0, 1], [0, -1]].forEach(([dx, dz]) => {
|
||||
for (const d of [60, 120, 200, 300]) {
|
||||
const o = roomPlenum(x + dx * d, z + dz * d);
|
||||
if (o === undefined) continue;
|
||||
if (o !== null) v = v === null ? o : Math.min(v, o);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
return v === null ? null : u.layout.floorTop + v;
|
||||
};
|
||||
const roofCeilAt = (x, z) => {
|
||||
let u = unitAt(x, z);
|
||||
if (!u) u = units.reduce((best, c) => {
|
||||
const dx = Math.max(c.rect.x0 - x, 0, x - c.rect.x1), dz = Math.max(c.rect.z0 - z, 0, z - c.rect.z1);
|
||||
@@ -155,24 +176,103 @@ window.ElektraModel = (() => {
|
||||
const r = u.rect;
|
||||
return u.roof.ceilingAt(Math.max(r.x0 + 1, Math.min(r.x1 - 1, x)), Math.max(r.z0 + 1, Math.min(r.z1 - 1, z)));
|
||||
};
|
||||
// Leidingvlak: het valse plafond waar dat lager ligt dan het dak, anders de onderkant van het dak.
|
||||
const plenumOn = (x, z) => { const v = plenumAt(x, z); return v !== null && v < roofCeilAt(x, z) - 1 ? v : null; };
|
||||
const ceilAt = (x, z) => { const v = plenumOn(x, z); return v !== null ? v : roofCeilAt(x, z); };
|
||||
const inPlenum = (x, z, y) => y < roofCeilAt(x, z) - 50;
|
||||
// Over het plafond het vlak volgen: knikken in het dak (nok, kil) als tussenpunt, de overgang vals plafond/dak
|
||||
// verticaal op de grens (in de wand), zodat de buis nooit los door de ruimte of buiten het dak loopt.
|
||||
function followCeiling(pts) {
|
||||
const same = (u, v) => (u === null && v === null) || (u !== null && v !== null && Math.abs(u - v) < 1);
|
||||
const out = [pts[0]];
|
||||
pts.slice(1).forEach((q, i) => {
|
||||
const p = pts[i], len = Math.hypot(q.x - p.x, q.z - p.z);
|
||||
if (p.ceil && q.ceil && !p.floor && !q.floor && len > 1) {
|
||||
const at = (t) => ({ x: p.x + (q.x - p.x) * t, z: p.z + (q.z - p.z) * t });
|
||||
const hOf = (pt, st) => st !== null ? st : roofCeilAt(pt.x, pt.z);
|
||||
const mk = (pt, y) => Object.assign({}, p, pt, { y, wall: null, ceil: true, floor: false, plenum: inPlenum(pt.x, pt.z, y) });
|
||||
const n = Math.max(2, Math.ceil(len / 100));
|
||||
const S = Array.from({ length: n + 1 }, (_, k) => { const pt = at(k / n), st = plenumOn(pt.x, pt.z); return { t: k / n, pt, st, y: hOf(pt, st) }; });
|
||||
for (let k = 1; k <= n; k++) {
|
||||
const A = S[k - 1], B = S[k];
|
||||
if (!same(A.st, B.st)) {
|
||||
let lo = A.t, hi = B.t;
|
||||
for (let it = 0; it < 10; it++) { const m = (lo + hi) / 2, pm = at(m); if (same(plenumOn(pm.x, pm.z), A.st)) lo = m; else hi = m; }
|
||||
const pb = at((lo + hi) / 2);
|
||||
out.push(mk(pb, hOf(pb, A.st)), mk(pb, hOf(pb, B.st)));
|
||||
} else if (k < n && same(B.st, S[k + 1].st) && Math.abs(A.y - 2 * B.y + S[k + 1].y) > 2) out.push(mk(B.pt, B.y));
|
||||
}
|
||||
}
|
||||
out.push(q);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
const floorAt = (x, z) => { const u = unitAt(x, z) || units[0]; return u.layout.floorTop; };
|
||||
const clampTo = (v, lo, hi) => lo > hi ? (lo + hi) / 2 : Math.max(lo, Math.min(hi, v));
|
||||
const segInside = (p, q) => {
|
||||
const n = Math.max(1, Math.ceil(Math.hypot(q.x - p.x, q.z - p.z) / 50));
|
||||
for (let k = 0; k <= n; k++) if (!unitAt(p.x + (q.x - p.x) * k / n, p.z + (q.z - p.z) * k / n)) return false;
|
||||
return true;
|
||||
};
|
||||
// Punt buiten de woning (gevellamp, dakrandspot): eerst loodrecht naar de dichtstbijzijnde unit.
|
||||
const clampIn = (p) => {
|
||||
if (unitAt(p.x, p.z)) return null;
|
||||
let best = null;
|
||||
units.forEach(u => {
|
||||
const r = u.rect, x = clampTo(p.x, r.x0 + 1, r.x1 - 1), z = clampTo(p.z, r.z0 + 1, r.z1 - 1), d = Math.hypot(x - p.x, z - p.z);
|
||||
if (!best || d < best.d) best = { x, z, d };
|
||||
});
|
||||
return best && { x: best.x, z: best.z };
|
||||
};
|
||||
// Knikpunten in de plattegrond tussen a en b (exclusief), haaks en binnen de units: bij een L- of T-vorm mag de
|
||||
// knik niet in de lege hoek buiten de woning vallen. Eerst x dan z (zoals altijd), anders z dan x, anders via een unit.
|
||||
function corners(a, b, viaCeiling) {
|
||||
const A = clampIn(a), B = clampIn(b), pa = A || a, pb = B || b;
|
||||
const cands = [[{ x: pb.x, z: pa.z }], [{ x: pa.x, z: pb.z }]];
|
||||
units.forEach(u => {
|
||||
const r = u.rect, xm = clampTo((pa.x + pb.x) / 2, r.x0 + 150, r.x1 - 150), zm = clampTo((pa.z + pb.z) / 2, r.z0 + 150, r.z1 - 150);
|
||||
cands.push([{ x: xm, z: pa.z }, { x: xm, z: pb.z }], [{ x: pa.x, z: zm }, { x: pb.x, z: zm }]);
|
||||
});
|
||||
let best = null;
|
||||
cands.forEach(c => {
|
||||
const pts = [pa].concat(c, [pb]);
|
||||
if (!pts.slice(1).every((q, i) => segInside(pts[i], q))) return;
|
||||
let len = pts.slice(1).reduce((t, q, i) => t + Math.abs(q.x - pts[i].x) + Math.abs(q.z - pts[i].z), 0);
|
||||
// Over het plafond telt op- en neergaan mee: liever door een laag vals plafond dan over de nok.
|
||||
if (viaCeiling) pts.slice(1).forEach((q, i) => {
|
||||
const p = pts[i], m = Math.max(1, Math.ceil(Math.hypot(q.x - p.x, q.z - p.z) / 200));
|
||||
let prev = ceilAt(p.x, p.z);
|
||||
for (let k = 1; k <= m; k++) { const y = ceilAt(p.x + (q.x - p.x) * k / m, p.z + (q.z - p.z) * k / m); len += Math.abs(y - prev); prev = y; }
|
||||
});
|
||||
if (!best || len < best.len - 1) best = { len, c };
|
||||
});
|
||||
return [A].concat(best ? best.c : cands[0], [B]).filter(Boolean)
|
||||
.filter(q => Math.hypot(q.x - a.x, q.z - a.z) > 0.5 && Math.hypot(q.x - b.x, q.z - b.z) > 0.5);
|
||||
}
|
||||
const strictlyInside = (x, z) => units.some(u => x > u.rect.x0 + 5 && x < u.rect.x1 - 5 && z > u.rect.z0 + 5 && z < u.rect.z1 - 5);
|
||||
function path(a, b) {
|
||||
const P = (p, wall, ceil, floor) => ({ x: p.x, y: p.y, z: p.z, wall: wall || null, ceil: !!ceil, floor: !!floor });
|
||||
const P = (p, wall, ceil, floor) => ({ x: p.x, y: p.y, z: p.z, wall: wall || null, ceil: !!ceil, floor: !!floor, plenum: !!ceil && inPlenum(p.x, p.z, p.y) });
|
||||
if (a.mount === 'vloer' || b.mount === 'vloer') {
|
||||
const fa = floorAt(a.x, a.z) - FLOOR_DEPTH, fb = floorAt(b.x, b.z) - FLOOR_DEPTH;
|
||||
return [P(a, a.face, false, a.mount === 'vloer'), P({ x: a.x, y: fa, z: a.z }, a.face, false, true), P({ x: b.x, y: fa, z: a.z }, null, false, true),
|
||||
P({ x: b.x, y: fb, z: b.z }, b.face, false, true), P(b, b.face, false, b.mount === 'vloer')];
|
||||
return [P(a, a.face, false, a.mount === 'vloer'), P({ x: a.x, y: fa, z: a.z }, a.face, false, true)]
|
||||
.concat(corners(a, b).map(c => P({ x: c.x, y: fa, z: c.z }, null, false, true)),
|
||||
[P({ x: b.x, y: fb, z: b.z }, b.face, false, true), P(b, b.face, false, b.mount === 'vloer')]);
|
||||
}
|
||||
if (a.face && a.face === b.face) return [P(a, a.face), P({ x: a.x, y: b.y, z: a.z }, a.face), P(b, b.face)];
|
||||
if (a.mount === 'buiten' && b.mount === 'buiten') return [P(a), P({ x: b.x, y: a.y, z: a.z }), P(b)];
|
||||
const top = (p) => p.mount === 'plafond' ? p.y : ceilAt(p.x, p.z);
|
||||
// Gevellamp naar gevellamp: langs de gevel, bij een binnenhoek niet dwars door de woning.
|
||||
if (a.mount === 'buiten' && b.mount === 'buiten') {
|
||||
const c = strictlyInside(b.x, a.z) ? { x: a.x, z: b.z } : { x: b.x, z: a.z };
|
||||
return [P(a), P({ x: c.x, y: a.y, z: c.z }), P(b)];
|
||||
}
|
||||
// Wandpunt: de plafondhoogte iets de ruimte in meten (de wand zelf hoort bij geen ruimte).
|
||||
const top = (p) => p.mount === 'plafond' ? p.y : p.face && p.face.n ? ceilAt(p.x + p.face.n.x * 50, p.z + p.face.n.z * 50) : ceilAt(p.x, p.z);
|
||||
const ya = top(a), yb = top(b), dropB = Math.abs(yb - b.y) > 1;
|
||||
const pts = [P(a, a.face, a.mount === 'plafond')];
|
||||
if (Math.abs(ya - a.y) > 1) pts.push(P({ x: a.x, y: ya, z: a.z }, a.face, true));
|
||||
pts.push(P({ x: b.x, y: ceilAt(b.x, a.z), z: a.z }, null, true));
|
||||
corners(a, b, true).forEach(c => pts.push(P({ x: c.x, y: ceilAt(c.x, c.z), z: c.z }, null, true)));
|
||||
pts.push(P({ x: b.x, y: yb, z: b.z }, dropB ? b.face : null, true));
|
||||
if (dropB) pts.push(P(b, b.face, false));
|
||||
return pts;
|
||||
return followCeiling(pts);
|
||||
}
|
||||
const length = (pts) => pts.slice(1).reduce((s, q, i) => s + Math.hypot(q.x - pts[i].x, q.y - pts[i].y, q.z - pts[i].z), 0);
|
||||
const cost = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.z - b.z) + Math.abs(a.y - b.y);
|
||||
@@ -201,7 +301,7 @@ window.ElektraModel = (() => {
|
||||
groups: [], runs: [], cables: [], wires: [], conduits: [], materials: [], warnings: [], items: [], faces: []
|
||||
};
|
||||
if (!s.enabled || !units.length) return out;
|
||||
const R = makeRouter(units);
|
||||
const R = makeRouter(units, rooms);
|
||||
const faces = {};
|
||||
units.forEach(u => { faces[u.id] = facesOf(u, interior); });
|
||||
const faceOfPiece = (u, p) => faces[u.id].find(f => f.piece === p && f.side === -1);
|
||||
@@ -735,13 +835,13 @@ window.ElektraModel = (() => {
|
||||
conduit
|
||||
};
|
||||
}
|
||||
function build3D(el, materials, stage) {
|
||||
// rough: buizen en inbouwdozen (in de wand en boven het plafond); uit bij de render-export.
|
||||
function build3D(el, materials, layers, rough) {
|
||||
const toM = 0.001, group = new THREE.Group();
|
||||
if (!el || !el.enabled) return group;
|
||||
const mats = materials || createMaterials();
|
||||
mats.plateMaterial.color.setHex(PLATE_COLORS[el.settings.plateColor].hex);
|
||||
const st = BuildStages.clamp(stage), show = (key) => BuildStages.shows(st, key);
|
||||
const rough = show('elektra') && !show('beplating');
|
||||
if (!ViewLayers.shows(layers, 'elektra')) return group;
|
||||
const box = (w, h, d, c, rotY, mat) => {
|
||||
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w * toM, h * toM, d * toM), mat);
|
||||
mesh.position.set(c.x * toM, c.y * toM, c.z * toM);
|
||||
@@ -750,18 +850,18 @@ window.ElektraModel = (() => {
|
||||
return mesh;
|
||||
};
|
||||
const k = el.meterkast;
|
||||
if (k && show('elektra')) {
|
||||
if (k) {
|
||||
box(k.w, k.h, k.d, k.center, k.rotY, mats.kastMaterial);
|
||||
const front = facePoint(k.face, k.u, k.y + k.h / 2, k.d + 2);
|
||||
box(k.w - 30, k.h - 30, 4, front, k.rotY, mats.deurMaterial);
|
||||
}
|
||||
if (show('afwerking')) {
|
||||
if (ViewLayers.shows(layers, 'afwerking')) {
|
||||
el.combos.forEach(c => box(c.width, PLATE, 10, c.center, c.rotY, mats.plateMaterial));
|
||||
el.wcds.forEach(w => box(WCD_W, PLATE, 12, w.center, w.rotY, mats.plateMaterial));
|
||||
el.apps.forEach(a => box(PLATE, PLATE, 12, a.center, a.rotY, mats.plateMaterial));
|
||||
el.floorWcds.forEach(w => box(130, 8, 130, { x: w.x, y: w.y + 4, z: w.z }, 0, mats.plateMaterial));
|
||||
}
|
||||
if (!rough) return group;
|
||||
if (rough === false) return group;
|
||||
|
||||
const up = new THREE.Vector3(0, 1, 0);
|
||||
const shift = (p) => ({
|
||||
@@ -859,7 +959,7 @@ window.ElektraModel = (() => {
|
||||
} else {
|
||||
n += bom.filter(bp => VERTICAL_KINDS.includes(bp.kind) && between(bp.x + f.W / 2, a.u, b.u) && Math.abs(a.y - bp.y) <= bp.length / 2).length;
|
||||
}
|
||||
} else if (!vertical && p.ceil && q.ceil) {
|
||||
} else if (!vertical && p.ceil && q.ceil && !(p.plenum && q.plenum)) {
|
||||
const mid = { x: (p.x + q.x) / 2, z: (p.z + q.z) / 2 }, u = R.unitAt(mid.x, mid.z);
|
||||
if (u) u.roof.planes.forEach(pl => {
|
||||
const dm = roofDeckMap(u, pl), c = clipPlan(p, q, dm.bounds);
|
||||
|
||||
Reference in New Issue
Block a user