Browser-BIM (cad): semantisches Modell, abgeleitete 2D/3D-Sichten, Zeichenwerkzeuge
Standalone-Browser-Port von DOSSIER. Enthaelt das semantische Modell mit Plan-/3D-Ableitung, Zeichen- und Editierwerkzeuge, Rhino-artiges Befehlssystem, dockbares Panel-System, Resource-Manager, DXF/.lin/.pat-Import, i18n (de/en) sowie Projektdokumentation und Probe-Harness.
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
// Numerische Verifikation des 2D-Kernels (esbuild → node). Kein UI nötig.
|
||||
import {
|
||||
segmentIntersect,
|
||||
offsetPolyline,
|
||||
offsetSegment,
|
||||
trimSegment,
|
||||
extendSegment,
|
||||
filletCorner,
|
||||
pointSegmentDistance,
|
||||
lineCircleIntersect,
|
||||
circleCircleIntersect,
|
||||
signedArea,
|
||||
isCCW,
|
||||
joinChains,
|
||||
removeSegment,
|
||||
splitAtIntersections,
|
||||
splitClosedByChord,
|
||||
splitPolylineAtParam,
|
||||
vecEqual,
|
||||
} from "../src/geometry/kernel2d.ts";
|
||||
|
||||
let pass = 0;
|
||||
let fail = 0;
|
||||
const approx = (a: number, b: number, eps = 1e-6) => Math.abs(a - b) <= eps;
|
||||
function check(name: string, cond: boolean, detail?: unknown) {
|
||||
if (cond) { pass++; console.log(" ok " + name); }
|
||||
else { fail++; console.log(" FAIL " + name, JSON.stringify(detail)); }
|
||||
}
|
||||
|
||||
// 1) Strecken-Schnitt: X durch (1,1)
|
||||
{
|
||||
const h = segmentIntersect({x:0,y:0},{x:2,y:2},{x:0,y:2},{x:2,y:0});
|
||||
check("segmentIntersect cross at (1,1)", !!h && approx(h!.point.x,1) && approx(h!.point.y,1) && approx(h!.t,0.5), h);
|
||||
}
|
||||
// 2) parallele Strecken → null
|
||||
{
|
||||
const h = segmentIntersect({x:0,y:0},{x:2,y:0},{x:0,y:1},{x:2,y:1});
|
||||
check("segmentIntersect parallel -> null", h === null);
|
||||
}
|
||||
// 3) kein Schnitt innerhalb der Segmente → null
|
||||
{
|
||||
const h = segmentIntersect({x:0,y:0},{x:1,y:0},{x:2,y:-1},{x:2,y:1});
|
||||
check("segmentIntersect outside -> null", h === null);
|
||||
}
|
||||
// 4) Offset einer Strecke nach links (d>0): +y
|
||||
{
|
||||
const [a,b] = offsetSegment({x:0,y:0},{x:1,y:0},0.5);
|
||||
check("offsetSegment left +0.5y", approx(a.y,0.5) && approx(b.y,0.5) && approx(a.x,0) && approx(b.x,1), {a,b});
|
||||
}
|
||||
// 5) Offset geschlossenes Quadrat: Konvention +leftNormal zeigt bei CCW NACH
|
||||
// INNEN (CONVENTIONS.md), also d>0 = innen → (1,1)..(3,3); d<0 = außen → (-1,-1)..(5,5).
|
||||
{
|
||||
const sq = [{x:0,y:0},{x:4,y:0},{x:4,y:4},{x:0,y:4}]; // CCW
|
||||
const inn = offsetPolyline(sq, 1, true);
|
||||
const okIn = inn.length===4 &&
|
||||
approx(inn[0].x,1)&&approx(inn[0].y,1)&&
|
||||
approx(inn[1].x,3)&&approx(inn[1].y,1)&&
|
||||
approx(inn[2].x,3)&&approx(inn[2].y,3)&&
|
||||
approx(inn[3].x,1)&&approx(inn[3].y,3);
|
||||
check("offsetPolyline square inward by 1 (CCW +n=inside)", okIn, inn);
|
||||
const out = offsetPolyline(sq, -1, true);
|
||||
const okOut = out.length===4 && approx(out[0].x,-1)&&approx(out[0].y,-1)&&approx(out[2].x,5)&&approx(out[2].y,5);
|
||||
check("offsetPolyline square outward by 1 (d<0)", okOut, out);
|
||||
}
|
||||
// 6) Offset offene L-Polylinie (Gehrung am Innenknick)
|
||||
{
|
||||
const L = [{x:0,y:0},{x:4,y:0},{x:4,y:4}];
|
||||
const off = offsetPolyline(L, 1, false); // links
|
||||
// Endkanten verschoben; mittlerer Punkt = Gehrung bei (3,1) für links-offset
|
||||
check("offsetPolyline open L mitre", off.length===3 && approx(off[1].x,3) && approx(off[1].y,1), off);
|
||||
}
|
||||
// 7) Trim: Strecke 0..4 von Cutter bei x=1 und x=3, Pick im Mittelstück → 2 Reststücke
|
||||
{
|
||||
const cutters = [
|
||||
{pts:[{x:1,y:-1},{x:1,y:1}], closed:false},
|
||||
{pts:[{x:3,y:-1},{x:3,y:1}], closed:false},
|
||||
];
|
||||
const rem = trimSegment({x:0,y:0},{x:4,y:0},cutters,{x:2,y:0});
|
||||
const ok = rem.length===2 &&
|
||||
approx(rem[0][0].x,0)&&approx(rem[0][1].x,1)&&
|
||||
approx(rem[1][0].x,3)&&approx(rem[1][1].x,4);
|
||||
check("trimSegment removes middle piece", ok, rem);
|
||||
}
|
||||
// 8) Extend: Strecke 0..1 auf x-Achse, Ende bis Cutter bei x=3
|
||||
{
|
||||
const cutters=[{pts:[{x:3,y:-1},{x:3,y:1}],closed:false}];
|
||||
const ex = extendSegment({x:0,y:0},{x:1,y:0},"end",cutters);
|
||||
check("extendSegment to x=3", !!ex && approx(ex![1].x,3) && approx(ex![1].y,0), ex);
|
||||
}
|
||||
// 9) Extend start-Ende rückwärts bis x=-2
|
||||
{
|
||||
const cutters=[{pts:[{x:-2,y:-1},{x:-2,y:1}],closed:false}];
|
||||
const ex = extendSegment({x:0,y:0},{x:1,y:0},"start",cutters);
|
||||
check("extendSegment start to x=-2", !!ex && approx(ex![0].x,-2), ex);
|
||||
}
|
||||
// 10) Fillet 90°-Ecke, r=1 → setback 1, center (1,1)
|
||||
{
|
||||
const f = filletCorner({x:0,y:0},{x:5,y:0},{x:0,y:5},1);
|
||||
const ok = !!f && approx(f!.tangentA.x,1)&&approx(f!.tangentA.y,0)&&
|
||||
approx(f!.tangentB.x,0)&&approx(f!.tangentB.y,1)&&
|
||||
approx(f!.center.x,1)&&approx(f!.center.y,1)&&approx(f!.radius,1);
|
||||
check("filletCorner 90deg r1", ok, f);
|
||||
}
|
||||
// 11) Fillet kollinear → null
|
||||
{
|
||||
const f = filletCorner({x:0,y:0},{x:1,y:0},{x:-1,y:0},1);
|
||||
check("filletCorner collinear -> null", f === null);
|
||||
}
|
||||
// 12) pointSegmentDistance
|
||||
{
|
||||
check("pointSegmentDistance perpendicular", approx(pointSegmentDistance({x:1,y:2},{x:0,y:0},{x:4,y:0}),2));
|
||||
}
|
||||
|
||||
// 13) Linie durch Kreis (Mittelpunkt-Sekante) → 2 Punkte ±r
|
||||
{
|
||||
const ps = lineCircleIntersect({x:-5,y:0},{x:5,y:0},{x:0,y:0},2);
|
||||
const xs = ps.map(p=>p.x).sort((a,b)=>a-b);
|
||||
check("lineCircleIntersect secant", ps.length===2 && approx(xs[0],-2) && approx(xs[1],2), ps);
|
||||
}
|
||||
// 14) Zwei Kreise (Abstand 2, r=√2 je) schneiden sich bei (1,±1)
|
||||
{
|
||||
const ps = circleCircleIntersect({x:0,y:0},Math.SQRT2,{x:2,y:0},Math.SQRT2);
|
||||
const ok = ps.length===2 && ps.every(p=>approx(Math.abs(p.x),1)&&approx(Math.abs(p.y),1)) && approx(ps[0].x,1);
|
||||
check("circleCircleIntersect at (1,±1)", ok, ps);
|
||||
}
|
||||
// 15) Wicklung
|
||||
{
|
||||
const ccw = [{x:0,y:0},{x:4,y:0},{x:4,y:4},{x:0,y:4}];
|
||||
check("signedArea/isCCW", approx(signedArea(ccw),16) && isCCW(ccw) && !isCCW([...ccw].reverse()));
|
||||
}
|
||||
|
||||
// ── Split / Join / Segment-Löschen ───────────────────────────────────────────
|
||||
const V = (x: number, y: number) => ({ x, y });
|
||||
|
||||
// 16) splitPolylineAtParam: offene Linie → 2 Stücke an (5,0)
|
||||
{
|
||||
const parts = splitPolylineAtParam([V(0,0),V(10,0)], false, 0, 0.5);
|
||||
check("splitPolyline open -> 2 pieces", parts.length===2 && approx(parts[0][1].x,5) && approx(parts[1][0].x,5), parts);
|
||||
}
|
||||
// 17) splitPolylineAtParam: geschlossenes Quadrat → eine offene Kette von/bis Schnitt
|
||||
{
|
||||
const sq=[V(0,0),V(4,0),V(4,4),V(0,4)];
|
||||
const parts = splitPolylineAtParam(sq, true, 0, 0.5);
|
||||
const c = parts[0];
|
||||
check("splitPolyline closed -> 1 open chain", parts.length===1 && vecEqual(c[0],V(2,0)) && vecEqual(c[c.length-1],V(2,0)), c);
|
||||
}
|
||||
// 18) splitClosedByChord: Quadrat unten/oben → 2 geschlossene Hälften
|
||||
{
|
||||
const sq=[V(0,0),V(4,0),V(4,4),V(0,4)];
|
||||
const res = splitClosedByChord(sq, 0, 0.5, 2, 0.5);
|
||||
const has=(p:{x:number;y:number}[],q:{x:number;y:number})=>p.some(r=>vecEqual(r,q));
|
||||
const ok = !!res && res[0].length>=3 && res[1].length>=3 &&
|
||||
has(res![0],V(2,0)) && has(res![0],V(2,4)) && has(res![1],V(2,0)) && has(res![1],V(2,4));
|
||||
check("splitClosedByChord -> 2 closed halves", ok, res);
|
||||
check("splitClosedByChord same edge -> null", splitClosedByChord(sq,1,0.5,1,0.7)===null);
|
||||
}
|
||||
// 19) removeSegment: geschlossenes Quadrat → offen, Start nach Kante
|
||||
{
|
||||
const sq=[V(0,0),V(4,0),V(4,4),V(0,4)];
|
||||
const r = removeSegment(sq, true, 0);
|
||||
check("removeSegment closed -> open chain", r.closed===false && r.pts.length===4 && vecEqual(r.pts[0],V(4,0)), r);
|
||||
}
|
||||
// 20) splitAtIntersections: Rechteck × querende Linie → 2 geschlossene Hälften
|
||||
{
|
||||
const rect=[V(0,0),V(4,0),V(4,4),V(0,4)];
|
||||
const cutter=[V(2,-1),V(2,5)];
|
||||
const pieces = splitAtIntersections(rect, true, [cutter]);
|
||||
check("splitAtIntersections rect×line -> 2 closed", pieces.length===2 && pieces[0].length>=3 && pieces[1].length>=3, pieces);
|
||||
}
|
||||
// 21) splitAtIntersections: offene Linie × Cutter → 2 Stücke an (5,0)
|
||||
{
|
||||
const pieces = splitAtIntersections([V(0,0),V(10,0)], false, [[V(5,-2),V(5,2)]]);
|
||||
check("splitAtIntersections open line -> 2 pieces", pieces.length===2 && approx(pieces[0][pieces[0].length-1].x,5), pieces);
|
||||
}
|
||||
// 22) joinChains: zwei Linien mit gemeinsamem Endpunkt → eine Polylinie
|
||||
{
|
||||
const out = joinChains([{pts:[V(0,0),V(5,0)],closed:false},{pts:[V(5,0),V(5,5)],closed:false}]);
|
||||
check("joinChains 2 chains -> 1 polyline (3 pts)", out.length===1 && out[0].pts.length===3 && out[0].closed===false, out);
|
||||
}
|
||||
// 23) joinChains: vier Kanten schließen sich → eine geschlossene Schleife
|
||||
{
|
||||
const out = joinChains([
|
||||
{pts:[V(0,0),V(4,0)],closed:false},
|
||||
{pts:[V(4,0),V(4,4)],closed:false},
|
||||
{pts:[V(4,4),V(0,4)],closed:false},
|
||||
{pts:[V(0,4),V(0,0)],closed:false},
|
||||
]);
|
||||
check("joinChains loop -> closed ring (4 pts)", out.length===1 && out[0].closed===true && out[0].pts.length===4, out);
|
||||
}
|
||||
|
||||
console.log(`\nKERNEL2D: ${pass} ok, ${fail} fail`);
|
||||
if (fail > 0) process.exit(1);
|
||||
Reference in New Issue
Block a user