Skip to content

Commit 78c5102

Browse files
authored
fix(fixTJunctions): fixes issues with fixTJunctions (#63)
* fixes internals for some corner cases of fixTJunctions (see jscad/scad-api#26) * adds tests * split out fixTJunction & cleanup
1 parent 84360bd commit 78c5102

File tree

3 files changed

+581
-318
lines changed

3 files changed

+581
-318
lines changed

src/CSG.js

Lines changed: 10 additions & 318 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const CAG = require('./CAG') // FIXME: circular dependency !
1515

1616
const Properties = require('./Properties')
1717
const {Connector} = require('./connectors')
18+
const fixTJunctions = require('./utils/fixTJunctions')
1819
// let {fromPolygons} = require('./CSGMakers') // FIXME: circular dependency !
1920

2021
/** Class CSG
@@ -318,7 +319,7 @@ CSG.prototype = {
318319
// the result is a true expansion of the solid
319320
// If false, returns only the shell
320321
expandedShell: function (radius, resolution, unionWithThis) {
321-
//const {sphere} = require('./primitives3d') // FIXME: circular dependency !
322+
// const {sphere} = require('./primitives3d') // FIXME: circular dependency !
322323
let csg = this.reTesselated()
323324
let result
324325
if (unionWithThis) {
@@ -732,13 +733,13 @@ CSG.prototype = {
732733
shareds.push(p.shared)
733734
}
734735
})
735-
let numVerticesPerPolygon = new Uint32Array(numpolygons),
736-
polygonSharedIndexes = new Uint32Array(numpolygons),
737-
polygonVertices = new Uint32Array(numpolygonvertices),
738-
polygonPlaneIndexes = new Uint32Array(numpolygons),
739-
vertexData = new Float64Array(numvertices * 3),
740-
planeData = new Float64Array(numplanes * 4),
741-
polygonVerticesIndex = 0
736+
let numVerticesPerPolygon = new Uint32Array(numpolygons)
737+
let polygonSharedIndexes = new Uint32Array(numpolygons)
738+
let polygonVertices = new Uint32Array(numpolygonvertices)
739+
let polygonPlaneIndexes = new Uint32Array(numpolygons)
740+
let vertexData = new Float64Array(numvertices * 3)
741+
let planeData = new Float64Array(numplanes * 4)
742+
let polygonVerticesIndex = 0
742743
for (let polygonindex = 0; polygonindex < numpolygons; ++polygonindex) {
743744
let p = csg.polygons[polygonindex]
744745
numVerticesPerPolygon[polygonindex] = p.vertices.length
@@ -897,316 +898,8 @@ CSG.prototype = {
897898
return cut3d.projectToOrthoNormalBasis(orthobasis)
898899
},
899900

900-
/*
901-
fixTJunctions:
902-
903-
Suppose we have two polygons ACDB and EDGF:
904-
905-
A-----B
906-
| |
907-
| E--F
908-
| | |
909-
C-----D--G
910-
911-
Note that vertex E forms a T-junction on the side BD. In this case some STL slicers will complain
912-
that the solid is not watertight. This is because the watertightness check is done by checking if
913-
each side DE is matched by another side ED.
914-
915-
This function will return a new solid with ACDB replaced by ACDEB
916-
917-
Note that this can create polygons that are slightly non-convex (due to rounding errors). Therefore the result should
918-
not be used for further CSG operations!
919-
*/
920901
fixTJunctions: function () {
921-
let csg = this.canonicalized()
922-
let sidemap = {}
923-
for (let polygonindex = 0; polygonindex < csg.polygons.length; polygonindex++) {
924-
let polygon = csg.polygons[polygonindex]
925-
let numvertices = polygon.vertices.length
926-
if (numvertices >= 3) // should be true
927-
{
928-
let vertex = polygon.vertices[0]
929-
let vertextag = vertex.getTag()
930-
for (let vertexindex = 0; vertexindex < numvertices; vertexindex++) {
931-
let nextvertexindex = vertexindex + 1
932-
if (nextvertexindex === numvertices) nextvertexindex = 0
933-
let nextvertex = polygon.vertices[nextvertexindex]
934-
let nextvertextag = nextvertex.getTag()
935-
let sidetag = vertextag + '/' + nextvertextag
936-
let reversesidetag = nextvertextag + '/' + vertextag
937-
if (reversesidetag in sidemap) {
938-
// this side matches the same side in another polygon. Remove from sidemap:
939-
let ar = sidemap[reversesidetag]
940-
ar.splice(-1, 1)
941-
if (ar.length === 0) {
942-
delete sidemap[reversesidetag]
943-
}
944-
} else {
945-
let sideobj = {
946-
vertex0: vertex,
947-
vertex1: nextvertex,
948-
polygonindex: polygonindex
949-
}
950-
if (!(sidetag in sidemap)) {
951-
sidemap[sidetag] = [sideobj]
952-
} else {
953-
sidemap[sidetag].push(sideobj)
954-
}
955-
}
956-
vertex = nextvertex
957-
vertextag = nextvertextag
958-
}
959-
}
960-
}
961-
// now sidemap contains 'unmatched' sides
962-
// i.e. side AB in one polygon does not have a matching side BA in another polygon
963-
let vertextag2sidestart = {}
964-
let vertextag2sideend = {}
965-
let sidestocheck = {}
966-
let sidemapisempty = true
967-
for (let sidetag in sidemap) {
968-
sidemapisempty = false
969-
sidestocheck[sidetag] = true
970-
sidemap[sidetag].map(function (sideobj) {
971-
let starttag = sideobj.vertex0.getTag()
972-
let endtag = sideobj.vertex1.getTag()
973-
if (starttag in vertextag2sidestart) {
974-
vertextag2sidestart[starttag].push(sidetag)
975-
} else {
976-
vertextag2sidestart[starttag] = [sidetag]
977-
}
978-
if (endtag in vertextag2sideend) {
979-
vertextag2sideend[endtag].push(sidetag)
980-
} else {
981-
vertextag2sideend[endtag] = [sidetag]
982-
}
983-
})
984-
}
985-
986-
if (!sidemapisempty) {
987-
// make a copy of the polygons array, since we are going to modify it:
988-
let polygons = csg.polygons.slice(0)
989-
990-
function addSide (vertex0, vertex1, polygonindex) {
991-
let starttag = vertex0.getTag()
992-
let endtag = vertex1.getTag()
993-
if (starttag === endtag) throw new Error('Assertion failed')
994-
let newsidetag = starttag + '/' + endtag
995-
let reversesidetag = endtag + '/' + starttag
996-
if (reversesidetag in sidemap) {
997-
// we have a matching reverse oriented side.
998-
// Instead of adding the new side, cancel out the reverse side:
999-
// console.log("addSide("+newsidetag+") has reverse side:");
1000-
deleteSide(vertex1, vertex0, null)
1001-
return null
1002-
}
1003-
// console.log("addSide("+newsidetag+")");
1004-
let newsideobj = {
1005-
vertex0: vertex0,
1006-
vertex1: vertex1,
1007-
polygonindex: polygonindex
1008-
}
1009-
if (!(newsidetag in sidemap)) {
1010-
sidemap[newsidetag] = [newsideobj]
1011-
} else {
1012-
sidemap[newsidetag].push(newsideobj)
1013-
}
1014-
if (starttag in vertextag2sidestart) {
1015-
vertextag2sidestart[starttag].push(newsidetag)
1016-
} else {
1017-
vertextag2sidestart[starttag] = [newsidetag]
1018-
}
1019-
if (endtag in vertextag2sideend) {
1020-
vertextag2sideend[endtag].push(newsidetag)
1021-
} else {
1022-
vertextag2sideend[endtag] = [newsidetag]
1023-
}
1024-
return newsidetag
1025-
}
1026-
1027-
function deleteSide (vertex0, vertex1, polygonindex) {
1028-
let starttag = vertex0.getTag()
1029-
let endtag = vertex1.getTag()
1030-
let sidetag = starttag + '/' + endtag
1031-
// console.log("deleteSide("+sidetag+")");
1032-
if (!(sidetag in sidemap)) throw new Error('Assertion failed')
1033-
let idx = -1
1034-
let sideobjs = sidemap[sidetag]
1035-
for (let i = 0; i < sideobjs.length; i++) {
1036-
let sideobj = sideobjs[i]
1037-
if (sideobj.vertex0 !== vertex0) continue
1038-
if (sideobj.vertex1 !== vertex1) continue
1039-
if (polygonindex !== null) {
1040-
if (sideobj.polygonindex !== polygonindex) continue
1041-
}
1042-
idx = i
1043-
break
1044-
}
1045-
if (idx < 0) throw new Error('Assertion failed')
1046-
sideobjs.splice(idx, 1)
1047-
if (sideobjs.length === 0) {
1048-
delete sidemap[sidetag]
1049-
}
1050-
idx = vertextag2sidestart[starttag].indexOf(sidetag)
1051-
if (idx < 0) throw new Error('Assertion failed')
1052-
vertextag2sidestart[starttag].splice(idx, 1)
1053-
if (vertextag2sidestart[starttag].length === 0) {
1054-
delete vertextag2sidestart[starttag]
1055-
}
1056-
1057-
idx = vertextag2sideend[endtag].indexOf(sidetag)
1058-
if (idx < 0) throw new Error('Assertion failed')
1059-
vertextag2sideend[endtag].splice(idx, 1)
1060-
if (vertextag2sideend[endtag].length === 0) {
1061-
delete vertextag2sideend[endtag]
1062-
}
1063-
}
1064-
1065-
while (true) {
1066-
let sidemapisempty = true
1067-
for (let sidetag in sidemap) {
1068-
sidemapisempty = false
1069-
sidestocheck[sidetag] = true
1070-
}
1071-
if (sidemapisempty) break
1072-
let donesomething = false
1073-
while (true) {
1074-
let sidetagtocheck = null
1075-
for (let sidetag in sidestocheck) {
1076-
sidetagtocheck = sidetag
1077-
break
1078-
}
1079-
if (sidetagtocheck === null) break // sidestocheck is empty, we're done!
1080-
let donewithside = true
1081-
if (sidetagtocheck in sidemap) {
1082-
let sideobjs = sidemap[sidetagtocheck]
1083-
if (sideobjs.length === 0) throw new Error('Assertion failed')
1084-
let sideobj = sideobjs[0]
1085-
for (let directionindex = 0; directionindex < 2; directionindex++) {
1086-
let startvertex = (directionindex === 0) ? sideobj.vertex0 : sideobj.vertex1
1087-
let endvertex = (directionindex === 0) ? sideobj.vertex1 : sideobj.vertex0
1088-
let startvertextag = startvertex.getTag()
1089-
let endvertextag = endvertex.getTag()
1090-
let matchingsides = []
1091-
if (directionindex === 0) {
1092-
if (startvertextag in vertextag2sideend) {
1093-
matchingsides = vertextag2sideend[startvertextag]
1094-
}
1095-
} else {
1096-
if (startvertextag in vertextag2sidestart) {
1097-
matchingsides = vertextag2sidestart[startvertextag]
1098-
}
1099-
}
1100-
for (let matchingsideindex = 0; matchingsideindex < matchingsides.length; matchingsideindex++) {
1101-
let matchingsidetag = matchingsides[matchingsideindex]
1102-
let matchingside = sidemap[matchingsidetag][0]
1103-
let matchingsidestartvertex = (directionindex === 0) ? matchingside.vertex0 : matchingside.vertex1
1104-
let matchingsideendvertex = (directionindex === 0) ? matchingside.vertex1 : matchingside.vertex0
1105-
let matchingsidestartvertextag = matchingsidestartvertex.getTag()
1106-
let matchingsideendvertextag = matchingsideendvertex.getTag()
1107-
if (matchingsideendvertextag !== startvertextag) throw new Error('Assertion failed')
1108-
if (matchingsidestartvertextag === endvertextag) {
1109-
// matchingside cancels sidetagtocheck
1110-
deleteSide(startvertex, endvertex, null)
1111-
deleteSide(endvertex, startvertex, null)
1112-
donewithside = false
1113-
directionindex = 2 // skip reverse direction check
1114-
donesomething = true
1115-
break
1116-
} else {
1117-
let startpos = startvertex.pos
1118-
let endpos = endvertex.pos
1119-
let checkpos = matchingsidestartvertex.pos
1120-
let direction = checkpos.minus(startpos)
1121-
// Now we need to check if endpos is on the line startpos-checkpos:
1122-
let t = endpos.minus(startpos).dot(direction) / direction.dot(direction)
1123-
if ((t > 0) && (t < 1)) {
1124-
let closestpoint = startpos.plus(direction.times(t))
1125-
let distancesquared = closestpoint.distanceToSquared(endpos)
1126-
if (distancesquared < (EPS * EPS)) {
1127-
// Yes it's a t-junction! We need to split matchingside in two:
1128-
let polygonindex = matchingside.polygonindex
1129-
let polygon = polygons[polygonindex]
1130-
// find the index of startvertextag in polygon:
1131-
let insertionvertextag = matchingside.vertex1.getTag()
1132-
let insertionvertextagindex = -1
1133-
for (let i = 0; i < polygon.vertices.length; i++) {
1134-
if (polygon.vertices[i].getTag() === insertionvertextag) {
1135-
insertionvertextagindex = i
1136-
break
1137-
}
1138-
}
1139-
if (insertionvertextagindex < 0) throw new Error('Assertion failed')
1140-
// split the side by inserting the vertex:
1141-
let newvertices = polygon.vertices.slice(0)
1142-
newvertices.splice(insertionvertextagindex, 0, endvertex)
1143-
let newpolygon = new Polygon(newvertices, polygon.shared /* polygon.plane */)
1144-
1145-
// FIX
1146-
// calculate plane with differents point
1147-
if (isNaN(newpolygon.plane.w)) {
1148-
let found = false,
1149-
loop = function (callback) {
1150-
newpolygon.vertices.forEach(function (item) {
1151-
if (found) return
1152-
callback(item)
1153-
})
1154-
}
1155-
1156-
loop(function (a) {
1157-
loop(function (b) {
1158-
loop(function (c) {
1159-
newpolygon.plane = Plane.fromPoints(a.pos, b.pos, c.pos)
1160-
if (!isNaN(newpolygon.plane.w)) {
1161-
found = true
1162-
}
1163-
})
1164-
})
1165-
})
1166-
}
1167-
// FIX
1168-
1169-
polygons[polygonindex] = newpolygon
1170-
1171-
// remove the original sides from our maps:
1172-
// deleteSide(sideobj.vertex0, sideobj.vertex1, null);
1173-
deleteSide(matchingside.vertex0, matchingside.vertex1, polygonindex)
1174-
let newsidetag1 = addSide(matchingside.vertex0, endvertex, polygonindex)
1175-
let newsidetag2 = addSide(endvertex, matchingside.vertex1, polygonindex)
1176-
if (newsidetag1 !== null) sidestocheck[newsidetag1] = true
1177-
if (newsidetag2 !== null) sidestocheck[newsidetag2] = true
1178-
donewithside = false
1179-
directionindex = 2 // skip reverse direction check
1180-
donesomething = true
1181-
break
1182-
} // if(distancesquared < 1e-10)
1183-
} // if( (t > 0) && (t < 1) )
1184-
} // if(endingstidestartvertextag === endvertextag)
1185-
} // for matchingsideindex
1186-
} // for directionindex
1187-
} // if(sidetagtocheck in sidemap)
1188-
if (donewithside) {
1189-
delete sidestocheck[sidetag]
1190-
}
1191-
}
1192-
if (!donesomething) break
1193-
}
1194-
let newcsg = CSG.fromPolygons(polygons)
1195-
newcsg.properties = csg.properties
1196-
newcsg.isCanonicalized = true
1197-
newcsg.isRetesselated = true
1198-
csg = newcsg
1199-
} // if(!sidemapisempty)
1200-
sidemapisempty = true
1201-
for (let sidetag in sidemap) {
1202-
sidemapisempty = false
1203-
break
1204-
}
1205-
if (!sidemapisempty) {
1206-
// throw new Error("!sidemapisempty");
1207-
console.log('!sidemapisempty')
1208-
}
1209-
return csg
902+
return fixTJunctions(CSG.fromPolygons, this)
1210903
},
1211904

1212905
toTriangles: function () {
@@ -1248,7 +941,6 @@ CSG.prototype = {
1248941
}
1249942
}
1250943

1251-
1252944
/** Construct a CSG solid from a list of `Polygon` instances.
1253945
* @param {Polygon[]} polygons - list of polygons
1254946
* @returns {CSG} new CSG object

0 commit comments

Comments
 (0)