@itwin/core-geometry 4.0.0-dev.6 → 4.0.0-dev.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/lib/cjs/Geometry.d.ts +3 -3
  2. package/lib/cjs/Geometry.d.ts.map +1 -1
  3. package/lib/cjs/Geometry.js +27 -11
  4. package/lib/cjs/Geometry.js.map +1 -1
  5. package/lib/cjs/curve/CurveCurve.d.ts +11 -8
  6. package/lib/cjs/curve/CurveCurve.d.ts.map +1 -1
  7. package/lib/cjs/curve/CurveCurve.js +16 -12
  8. package/lib/cjs/curve/CurveCurve.js.map +1 -1
  9. package/lib/cjs/curve/CurveCurveIntersectXY.d.ts +5 -1
  10. package/lib/cjs/curve/CurveCurveIntersectXY.d.ts.map +1 -1
  11. package/lib/cjs/curve/CurveCurveIntersectXY.js +11 -10
  12. package/lib/cjs/curve/CurveCurveIntersectXY.js.map +1 -1
  13. package/lib/cjs/geometry3d/Angle.d.ts +18 -0
  14. package/lib/cjs/geometry3d/Angle.d.ts.map +1 -1
  15. package/lib/cjs/geometry3d/Angle.js +38 -0
  16. package/lib/cjs/geometry3d/Angle.js.map +1 -1
  17. package/lib/cjs/geometry3d/CoincidentGeometryOps.d.ts +1 -0
  18. package/lib/cjs/geometry3d/CoincidentGeometryOps.d.ts.map +1 -1
  19. package/lib/cjs/geometry3d/CoincidentGeometryOps.js +3 -0
  20. package/lib/cjs/geometry3d/CoincidentGeometryOps.js.map +1 -1
  21. package/lib/cjs/geometry3d/Matrix3d.d.ts +171 -118
  22. package/lib/cjs/geometry3d/Matrix3d.d.ts.map +1 -1
  23. package/lib/cjs/geometry3d/Matrix3d.js +448 -417
  24. package/lib/cjs/geometry3d/Matrix3d.js.map +1 -1
  25. package/lib/cjs/geometry3d/Point3dVector3d.d.ts +12 -13
  26. package/lib/cjs/geometry3d/Point3dVector3d.d.ts.map +1 -1
  27. package/lib/cjs/geometry3d/Point3dVector3d.js +15 -13
  28. package/lib/cjs/geometry3d/Point3dVector3d.js.map +1 -1
  29. package/lib/cjs/geometry3d/Segment1d.d.ts +1 -1
  30. package/lib/cjs/geometry3d/Segment1d.js +1 -1
  31. package/lib/cjs/geometry3d/Segment1d.js.map +1 -1
  32. package/lib/cjs/numerics/Polynomials.d.ts +12 -0
  33. package/lib/cjs/numerics/Polynomials.d.ts.map +1 -1
  34. package/lib/cjs/numerics/Polynomials.js +14 -0
  35. package/lib/cjs/numerics/Polynomials.js.map +1 -1
  36. package/lib/cjs/polyface/PolyfaceBuilder.d.ts +1 -0
  37. package/lib/cjs/polyface/PolyfaceBuilder.d.ts.map +1 -1
  38. package/lib/cjs/polyface/PolyfaceBuilder.js +46 -6
  39. package/lib/cjs/polyface/PolyfaceBuilder.js.map +1 -1
  40. package/lib/cjs/polyface/PolyfaceQuery.d.ts +54 -0
  41. package/lib/cjs/polyface/PolyfaceQuery.d.ts.map +1 -1
  42. package/lib/cjs/polyface/PolyfaceQuery.js +71 -1
  43. package/lib/cjs/polyface/PolyfaceQuery.js.map +1 -1
  44. package/lib/cjs/polyface/multiclip/OffsetMeshContext.d.ts +202 -0
  45. package/lib/cjs/polyface/multiclip/OffsetMeshContext.d.ts.map +1 -0
  46. package/lib/cjs/polyface/multiclip/OffsetMeshContext.js +1038 -0
  47. package/lib/cjs/polyface/multiclip/OffsetMeshContext.js.map +1 -0
  48. package/lib/cjs/serialization/GeometrySamples.d.ts +4 -1
  49. package/lib/cjs/serialization/GeometrySamples.d.ts.map +1 -1
  50. package/lib/cjs/serialization/GeometrySamples.js +14 -6
  51. package/lib/cjs/serialization/GeometrySamples.js.map +1 -1
  52. package/lib/cjs/topology/Graph.d.ts +113 -7
  53. package/lib/cjs/topology/Graph.d.ts.map +1 -1
  54. package/lib/cjs/topology/Graph.js +185 -7
  55. package/lib/cjs/topology/Graph.js.map +1 -1
  56. package/lib/cjs/topology/HalfEdgeGraphFromIndexedLoopsContext.d.ts +38 -0
  57. package/lib/cjs/topology/HalfEdgeGraphFromIndexedLoopsContext.d.ts.map +1 -0
  58. package/lib/cjs/topology/HalfEdgeGraphFromIndexedLoopsContext.js +82 -0
  59. package/lib/cjs/topology/HalfEdgeGraphFromIndexedLoopsContext.js.map +1 -0
  60. package/lib/esm/Geometry.d.ts +3 -3
  61. package/lib/esm/Geometry.d.ts.map +1 -1
  62. package/lib/esm/Geometry.js +27 -11
  63. package/lib/esm/Geometry.js.map +1 -1
  64. package/lib/esm/curve/CurveCurve.d.ts +11 -8
  65. package/lib/esm/curve/CurveCurve.d.ts.map +1 -1
  66. package/lib/esm/curve/CurveCurve.js +16 -12
  67. package/lib/esm/curve/CurveCurve.js.map +1 -1
  68. package/lib/esm/curve/CurveCurveIntersectXY.d.ts +5 -1
  69. package/lib/esm/curve/CurveCurveIntersectXY.d.ts.map +1 -1
  70. package/lib/esm/curve/CurveCurveIntersectXY.js +11 -10
  71. package/lib/esm/curve/CurveCurveIntersectXY.js.map +1 -1
  72. package/lib/esm/geometry3d/Angle.d.ts +18 -0
  73. package/lib/esm/geometry3d/Angle.d.ts.map +1 -1
  74. package/lib/esm/geometry3d/Angle.js +38 -0
  75. package/lib/esm/geometry3d/Angle.js.map +1 -1
  76. package/lib/esm/geometry3d/CoincidentGeometryOps.d.ts +1 -0
  77. package/lib/esm/geometry3d/CoincidentGeometryOps.d.ts.map +1 -1
  78. package/lib/esm/geometry3d/CoincidentGeometryOps.js +3 -0
  79. package/lib/esm/geometry3d/CoincidentGeometryOps.js.map +1 -1
  80. package/lib/esm/geometry3d/Matrix3d.d.ts +171 -118
  81. package/lib/esm/geometry3d/Matrix3d.d.ts.map +1 -1
  82. package/lib/esm/geometry3d/Matrix3d.js +448 -417
  83. package/lib/esm/geometry3d/Matrix3d.js.map +1 -1
  84. package/lib/esm/geometry3d/Point3dVector3d.d.ts +12 -13
  85. package/lib/esm/geometry3d/Point3dVector3d.d.ts.map +1 -1
  86. package/lib/esm/geometry3d/Point3dVector3d.js +15 -13
  87. package/lib/esm/geometry3d/Point3dVector3d.js.map +1 -1
  88. package/lib/esm/geometry3d/Segment1d.d.ts +1 -1
  89. package/lib/esm/geometry3d/Segment1d.js +1 -1
  90. package/lib/esm/geometry3d/Segment1d.js.map +1 -1
  91. package/lib/esm/numerics/Polynomials.d.ts +12 -0
  92. package/lib/esm/numerics/Polynomials.d.ts.map +1 -1
  93. package/lib/esm/numerics/Polynomials.js +14 -0
  94. package/lib/esm/numerics/Polynomials.js.map +1 -1
  95. package/lib/esm/polyface/PolyfaceBuilder.d.ts +1 -0
  96. package/lib/esm/polyface/PolyfaceBuilder.d.ts.map +1 -1
  97. package/lib/esm/polyface/PolyfaceBuilder.js +46 -6
  98. package/lib/esm/polyface/PolyfaceBuilder.js.map +1 -1
  99. package/lib/esm/polyface/PolyfaceQuery.d.ts +54 -0
  100. package/lib/esm/polyface/PolyfaceQuery.d.ts.map +1 -1
  101. package/lib/esm/polyface/PolyfaceQuery.js +69 -0
  102. package/lib/esm/polyface/PolyfaceQuery.js.map +1 -1
  103. package/lib/esm/polyface/multiclip/OffsetMeshContext.d.ts +202 -0
  104. package/lib/esm/polyface/multiclip/OffsetMeshContext.d.ts.map +1 -0
  105. package/lib/esm/polyface/multiclip/OffsetMeshContext.js +1032 -0
  106. package/lib/esm/polyface/multiclip/OffsetMeshContext.js.map +1 -0
  107. package/lib/esm/serialization/GeometrySamples.d.ts +4 -1
  108. package/lib/esm/serialization/GeometrySamples.d.ts.map +1 -1
  109. package/lib/esm/serialization/GeometrySamples.js +14 -6
  110. package/lib/esm/serialization/GeometrySamples.js.map +1 -1
  111. package/lib/esm/topology/Graph.d.ts +113 -7
  112. package/lib/esm/topology/Graph.d.ts.map +1 -1
  113. package/lib/esm/topology/Graph.js +185 -7
  114. package/lib/esm/topology/Graph.js.map +1 -1
  115. package/lib/esm/topology/HalfEdgeGraphFromIndexedLoopsContext.d.ts +38 -0
  116. package/lib/esm/topology/HalfEdgeGraphFromIndexedLoopsContext.d.ts.map +1 -0
  117. package/lib/esm/topology/HalfEdgeGraphFromIndexedLoopsContext.js +78 -0
  118. package/lib/esm/topology/HalfEdgeGraphFromIndexedLoopsContext.js.map +1 -0
  119. package/package.json +4 -4
@@ -0,0 +1,1038 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
4
+ * See LICENSE.md in the project root for license terms and full copyright notice.
5
+ *--------------------------------------------------------------------------------------------*/
6
+ /** @packageDocumentation
7
+ * @module Polyface
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.OffsetMeshContext = exports.SectorOffsetProperties = exports.FacetOffsetProperties = void 0;
11
+ const Polynomials_1 = require("../../numerics/Polynomials");
12
+ const GrowableXYZArray_1 = require("../../geometry3d/GrowableXYZArray");
13
+ const Point3dVector3d_1 = require("../../geometry3d/Point3dVector3d");
14
+ const PolygonOps_1 = require("../../geometry3d/PolygonOps");
15
+ const Ray3d_1 = require("../../geometry3d/Ray3d");
16
+ const Graph_1 = require("../../topology/Graph");
17
+ const HalfEdgeGraphFromIndexedLoopsContext_1 = require("../../topology/HalfEdgeGraphFromIndexedLoopsContext");
18
+ const Geometry_1 = require("../../Geometry");
19
+ const PolylineCompressionByEdgeOffset_1 = require("../../geometry3d/PolylineCompressionByEdgeOffset");
20
+ const Angle_1 = require("../../geometry3d/Angle");
21
+ function isDefinedAndTrue(value) {
22
+ if (value === undefined)
23
+ return false;
24
+ return value;
25
+ }
26
+ class AverageNormalData {
27
+ constructor() {
28
+ this.numActiveSectors = 0;
29
+ this.numInactiveSectors = 0; // exterior and sling.
30
+ this.averageNormal = Point3dVector3d_1.Vector3d.create();
31
+ this.radiansSum = 0.0;
32
+ this.maxDeviationRadiansFromAverage = 0.0;
33
+ }
34
+ clear() {
35
+ this.numActiveSectors = 0;
36
+ this.numInactiveSectors = 0; // exterior and sling.
37
+ this.averageNormal.setZero();
38
+ this.radiansSum = 0.0;
39
+ this.maxDeviationRadiansFromAverage = 0.0;
40
+ }
41
+ /** Add a normal to the evolving sum, scaled by radians in the corner */
42
+ accumulateNormal(node, normal, inactiveMask) {
43
+ if (node.isMaskSet(inactiveMask)) {
44
+ this.numInactiveSectors++;
45
+ }
46
+ else {
47
+ const sectorSweepRadians = Graph_1.HalfEdge.sectorSweepRadiansXYZ(node, normal);
48
+ this.averageNormal.addScaledInPlace(normal, sectorSweepRadians);
49
+ this.radiansSum += sectorSweepRadians;
50
+ this.numActiveSectors++;
51
+ }
52
+ }
53
+ /** normalize the accumulated normals. */
54
+ finishNormalAveraging() {
55
+ if (this.numActiveSectors > 0 && this.averageNormal.normalizeInPlace()) {
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+ /** Compute the deviation from average. update max deviation member */
61
+ recordDeviation(normal, isActive) {
62
+ if (isActive) {
63
+ const radians = this.averageNormal.radiansTo(normal);
64
+ this.maxDeviationRadiansFromAverage = Math.max(Math.abs(this.maxDeviationRadiansFromAverage), radians);
65
+ }
66
+ else {
67
+ }
68
+ }
69
+ /** Return the max deviation as computed on prior calls to recordDeviation */
70
+ get maxDeviationRadians() { return this.maxDeviationRadiansFromAverage; }
71
+ }
72
+ function emitSector(sector) {
73
+ if (OffsetMeshContext.stringDebugFunction !== undefined) {
74
+ OffsetMeshContext.stringDebugFunction(` Sector xyz ${sector.xyz.x},${sector.xyz.y},${sector.xyz.z} `);
75
+ OffsetMeshContext.stringDebugFunction(` normal ${sector.normal.x},${sector.normal.y},${sector.normal.z} `);
76
+ }
77
+ }
78
+ // facet properties used during offset.
79
+ //
80
+ class FacetOffsetProperties {
81
+ constructor(facetIndex, normal) {
82
+ this.facetIndex = facetIndex;
83
+ this.facetNormal = normal;
84
+ }
85
+ }
86
+ exports.FacetOffsetProperties = FacetOffsetProperties;
87
+ /**
88
+ * Sector properties during offset.
89
+ * * this.normal may be initially assigned as the facet normal but can mutate by
90
+ * averaging with neighbors.
91
+ * * this.xyz is initially the base mesh xyz but is expected to move along the normal.
92
+ * * this.count is used locally in computations.
93
+ */
94
+ class SectorOffsetProperties {
95
+ constructor(normal, xyz) {
96
+ this.xyz = xyz;
97
+ this.normal = normal;
98
+ this.count = 0;
99
+ }
100
+ /**
101
+ * Compute the angle between plane normals on opposite sides of the edge.
102
+ * * parallel normals have zero angle.
103
+ * * if the edge cuts inward to the volume behind the faces, the angle is negative.
104
+ * * if the edge is outward (a convex edge) the the volume, the angle is positive.
105
+ * @param edgeNodeA node on one side of the edge
106
+ * @param edgeVector pre-allocated vector to receive vector along edge.
107
+ * @param averageNormal pre-allocated vector to receive the average normal for a chamfer of the offset edge.
108
+ * @param offsetDistance distance of offset being constructed. The sign of this resolves angle ambiguity.
109
+ * @param radiansTolerance tolerance for large angle between normals.
110
+ * @returns true if this edge has SectorOffsetProperties on both sides and the angle between normals angle exceeds radiansTolerance.
111
+ */
112
+ static edgeHasLargeExteriorAngleBetweenNormals(edgeNodeA, edgeVector, averageNormal, offsetDistance, radiansTolerance = Math.PI * 0.5) {
113
+ const propsA = edgeNodeA.edgeTag;
114
+ const edgeNodeB = edgeNodeA.edgeMate;
115
+ const propsB = edgeNodeB.edgeTag;
116
+ if (propsA !== undefined && propsB !== undefined) {
117
+ edgeNodeA.vectorToFaceSuccessor(edgeVector);
118
+ const radians = propsA.normal.signedRadiansTo(propsB.normal, edgeVector);
119
+ if (Geometry_1.Geometry.split3WaySign(offsetDistance, -1, 1, 1) * radians >= radiansTolerance) {
120
+ Point3dVector3d_1.Vector3d.createAdd2Scaled(propsA.normal, 1.0, propsB.normal, 1.0, averageNormal);
121
+ if (averageNormal.normalizeInPlace())
122
+ return true;
123
+ }
124
+ }
125
+ return false;
126
+ }
127
+ static almostEqualNormals(sectorA, sectorB, radiansTolerance = Geometry_1.Geometry.smallAngleRadians) {
128
+ return sectorA.normal.radiansTo(sectorB.normal) <= radiansTolerance;
129
+ }
130
+ static radiansBetweenNormals(sectorA, sectorB) {
131
+ return sectorA.normal.radiansTo(sectorB.normal);
132
+ }
133
+ // Set the offset point this.xyz as sum of the nodeXyz + distance * this.normal
134
+ setOffsetPointAtDistanceAtHalfEdge(halfEdge, distance) {
135
+ halfEdge.getPoint3d(this.xyz);
136
+ this.xyz.addScaledInPlace(this.normal, distance);
137
+ }
138
+ // Copy xyz from parameter into (preexisting object) xyz
139
+ static setXYZAtHalfEdge(halfEdge, xyz) {
140
+ const props = halfEdge.edgeTag;
141
+ if (props !== undefined && xyz !== undefined)
142
+ props.xyz.set(xyz.x, xyz.y, xyz.z);
143
+ }
144
+ // Set the offset point this.xyz directly
145
+ setXYAndZ(xyz) {
146
+ this.xyz.set(xyz.x, xyz.y, xyz.z);
147
+ }
148
+ // Look through the half edge to its properties. Set the normal there. Optionally set xyz from node xyz and offset distance
149
+ static setNormalAtHalfEdge(halfEdge, uvw, distance) {
150
+ const props = halfEdge.edgeTag;
151
+ if (props !== undefined) {
152
+ props.normal.set(uvw.x, uvw.y, uvw.z);
153
+ if (distance !== undefined)
154
+ props.setOffsetPointAtDistanceAtHalfEdge(halfEdge, distance);
155
+ }
156
+ }
157
+ // Look through the half edge and its vertex successor to properties. Get the two normals. Return the angle sweeping from one to the next
158
+ static sweepRadiansAroundNormal(nodeA, upVector) {
159
+ const propsA = nodeA.edgeTag;
160
+ const propsB = nodeA.vertexSuccessor.edgeTag;
161
+ if (propsA !== undefined && propsB !== undefined) {
162
+ return propsA.normal.planarRadiansTo(propsB.normal, upVector);
163
+ }
164
+ return undefined;
165
+ }
166
+ // Look through the half edge to its properties. return (if possible) the coordinates
167
+ static getSectorPointAtHalfEdge(halfEdge, xyz, xyzArray) {
168
+ const props = halfEdge.edgeTag;
169
+ if (props !== undefined) {
170
+ if (xyz !== undefined)
171
+ xyz.setFromPoint3d(props.xyz);
172
+ if (xyzArray !== undefined)
173
+ xyzArray.push(props.xyz);
174
+ return true;
175
+ }
176
+ return false;
177
+ }
178
+ // access the XYZ and push to the array (which makes copies, not reference)
179
+ // return pointer to the SectorOffsetProperties
180
+ static pushXYZ(xyzArray, halfEdge) {
181
+ const sector = halfEdge.edgeTag;
182
+ if (sector !== undefined)
183
+ xyzArray.push(sector.xyz);
184
+ return sector;
185
+ }
186
+ // Dereference to execute: accumulatingVector += halfEdge.edgeTag.normal * scale
187
+ static accumulateScaledNormalAtHalfEdge(halfEdge, scale, accumulatingVector) {
188
+ const sector = halfEdge.edgeTag;
189
+ if (sector !== undefined)
190
+ accumulatingVector.addScaledInPlace(sector.normal, scale);
191
+ }
192
+ }
193
+ exports.SectorOffsetProperties = SectorOffsetProperties;
194
+ /*
195
+ About Chamfer Edges ..... as constructed in addChamferTopologyToAllEdges
196
+
197
+ When edge vertex X to vertex Y has a sharp angle between normals, a "chamfer face" must be created to "fatten" it.
198
+
199
+ The original half edges (nodes) for the edge are AX and AY. These are "mates" in the halfEdge mental model. As always,
200
+ AX is (as needed)
201
+ (i) the preferred half edge for the left side of the edge moving from X to Y. (i.e. above the edge)
202
+ (ii) a part of the face loop for the face to the left when proceeding CCW around the face to the above the drawn edge
203
+ (iii) a part of the vertex loop around X
204
+ Likewise, AY is (as needed)
205
+ (i) the preferred half edge for the left side of the edge moving from Y to X (i.e. below the edge)
206
+ (ii) a part of the face loop for the face to the left of the edge when proceeding CCW around the face below the edge.
207
+ (iii) a part of the vertex loop around Y
208
+
209
+ AX------>
210
+ X______________________________________________________________________Y
211
+ <---AY
212
+
213
+ When the chamfer face is created, it needs to have a sliver face "inside the edge" -- something in the space here
214
+
215
+ AX------>
216
+ _____________________________________________________________________
217
+ / \
218
+ X Y
219
+ \_____________________________________________________________________/
220
+ <---AY
221
+
222
+ The chamfer face will have a plane normal is the average of the two faces' plane normals.
223
+
224
+ The creation sequence for the chamfer face puts a slit "inside the edge" as above HalfEdges AX and AY remain as parts
225
+ of their respective face loops. In addition, at each end a singleton edge "sling" face is inserted at each
226
+ end of the sliver face.
227
+
228
+ The sequence is:
229
+
230
+ STEP 1: splitEdgeCreateSliver creates the sliver face with 2 half edges DX and DY
231
+ STEP 2: splitEdge (with undefined as the "prior" edge) creates a sling with HalfEdge CX "inside" and BX "outside".
232
+ (The sling face is not yet attached to X -- briefly floating in space)
233
+ STEP 3: pinch of HalfEdges BX and DX inserts the sling face "inside" the slit face at the X end.
234
+
235
+ Steps 2 and 3 are executed from each end. Due to the symmetric structure, a 2-pass loop can apply the logic at each end without distinct names in code.
236
+
237
+ AX------>
238
+ _______________________________________________________________
239
+ / <---DY \
240
+ / \
241
+ / BX---> \
242
+ / _______________ _______________ \
243
+ | / \ / <----CY \ |
244
+ |/ \ / \|
245
+ X | | Y
246
+ |\ CX---> / \ /|
247
+ | \_______________/ \_______________/ |
248
+ \ <---BY /
249
+ \ /
250
+ \ DX---> /
251
+ \ ______________________________________________________________/
252
+ <---AY
253
+
254
+ During the construction, the letters ABCD are used as above, but with prefixes emphasizing their role
255
+ outsideAX, outsideAY
256
+ slingB, slingC, sliverD
257
+
258
+ The "inside" sling faces (CX and CY) each have their own FacetOffsetProperties and SectorOffsetProperties.
259
+ The sliver face has its own FacetOffsetProperties which are referenced by DX, BY, DY, BX.
260
+ Each of those 4 has its own SectorOffSetProperties.
261
+
262
+ Important properties during offset construction:
263
+ 1) the original graph always has original topology and coordinates
264
+ 2) Each face of the original graph has a FacetOffsetProperties with a representative point and a normal. These are unchanged during the computation.
265
+ 3) Each node has its own SectorOffsetProperties with a coordinate and normal independent of the parent node.
266
+ 3.1 The first offset coordinates in each node are directly offset by face normal.
267
+ 3.2 This creates mismatch across edges and around vertices.
268
+ 3.3 Various sweeps "around each vertex" try to do intersections among appropriate offset planes to find
269
+ common coordinates in place of the initial mismatches.
270
+ 4) The independence of all the sectors allows the offset construction to fix things up in any order it chooses.
271
+ 5) During the construction, the xyz in SectorOffsetProperties around a single vertex do NOT have to match.
272
+ 6) At output time, there are three sweeps:
273
+ 6.1: By face: Go around the face and output a facet with the coordinates in the various sectors.
274
+ 6.2: By edge: For each edge, if the sector xyz match across both ends output nothing. If not, output a triangle or quad
275
+ 6.3: By vertex: At each vertex, if all vertex coordinates match output nothing. Otherwise output a facet with all the coordinates.
276
+ */
277
+ class OffsetMeshContext {
278
+ constructor(basePolyface, baseGraph, options) {
279
+ this._basePolyface = basePolyface;
280
+ this._baseGraph = baseGraph;
281
+ this._breakMaskA = baseGraph.grabMask();
282
+ this._breakMaskB = baseGraph.grabMask();
283
+ this._insideOfChamferFace = baseGraph.grabMask();
284
+ this._outsideOfChamferFace = baseGraph.grabMask();
285
+ this._insideChamferSling = baseGraph.grabMask();
286
+ this._outsideEndOfChamferFace = baseGraph.grabMask();
287
+ this._exteriorMask = Graph_1.HalfEdgeMask.EXTERIOR;
288
+ this._offsetCoordinatesReassigned = baseGraph.grabMask();
289
+ this._smoothRadiansBetweenNormals = options.smoothSingleAngleBetweenNormals.radians;
290
+ this._chamferTurnRadians = options.chamferAngleBetweenNormals.radians;
291
+ this._smoothAccumulatedRadiansBetweenNormals = options.smoothAccumulatedAngleBetweenNormals.radians;
292
+ }
293
+ /** "Exterior" side of a bare edge of the mesh */
294
+ get exteriorMask() { return this._exteriorMask; }
295
+ /** "First" sector of a smooth sequence. */
296
+ get breakMaskA() { return this._breakMaskA; }
297
+ /** "Last" sector of a smooth sequence. */
298
+ get breakMaskB() { return this._breakMaskB; }
299
+ /** This edge is on a chamfered face, and along the original edge */
300
+ get insideOfChamferFace() { return this._insideOfChamferFace; }
301
+ /** This is the original edge of a chamfer face */
302
+ get outsideOfChamferFace() { return this._outsideOfChamferFace; }
303
+ /** This edge is on a chamfered face, and at the end -- other side may be a sling */
304
+ get insideChamferSling() { return this._insideChamferSling; }
305
+ /** This is the outside of the end of a chamfer face -- i.e. the inside of a new face-at-vertex */
306
+ get outsideEndOfChamferFace() { return this._outsideEndOfChamferFace; }
307
+ // At each node . .
308
+ // * Find the sector data
309
+ // * recompute the sector point using node XYZ and sectorData normal.
310
+ applyFaceNormalOffsetsToSectorData(distance) {
311
+ this._baseGraph.announceNodes((_graph, node) => {
312
+ const sectorData = node.edgeTag;
313
+ if (sectorData !== undefined) {
314
+ sectorData.setOffsetPointAtDistanceAtHalfEdge(node, distance);
315
+ }
316
+ return true;
317
+ });
318
+ }
319
+ /**
320
+ * * build a mesh offset by given distance.
321
+ * * output the mesh to the given builder.
322
+ * @param basePolyface original mesh
323
+ * @param builder polyface builder to receive the new mesh.
324
+ * @param distance signed offset distance.
325
+ */
326
+ static buildOffsetMeshWithEdgeChamfers(basePolyface, builder, distance, options) {
327
+ const baseGraph = this.buildBaseGraph(basePolyface);
328
+ if (baseGraph !== undefined) {
329
+ const offsetBuilder = new OffsetMeshContext(basePolyface, baseGraph, options);
330
+ offsetBuilder.applyFaceNormalOffsetsToSectorData(distance);
331
+ if (OffsetMeshContext.graphDebugFunction !== undefined)
332
+ OffsetMeshContext.graphDebugFunction("BaseGraph", baseGraph, offsetBuilder._breakMaskA, offsetBuilder._breakMaskB);
333
+ const outputSelector = options.outputSelector ? options.outputSelector : {
334
+ outputOffsetsFromFaces: true,
335
+ outputOffsetsFromEdges: true,
336
+ outputOffsetsFromVertices: true,
337
+ };
338
+ if (isDefinedAndTrue(outputSelector.outputOffsetsFromFacesBeforeChamfers))
339
+ offsetBuilder.announceFacetsWithSectorCoordinatesAroundFaces(builder);
340
+ offsetBuilder.addChamferTopologyToAllEdges(options, distance);
341
+ offsetBuilder.computeOffsetFacetIntersections(distance);
342
+ if (OffsetMeshContext.graphDebugFunction !== undefined)
343
+ OffsetMeshContext.graphDebugFunction("after computeEdgeChamfers", baseGraph, offsetBuilder._breakMaskA, offsetBuilder._breakMaskB);
344
+ if (isDefinedAndTrue(outputSelector.outputOffsetsFromFaces))
345
+ offsetBuilder.announceFacetsWithSectorCoordinatesAroundFaces(builder);
346
+ if (isDefinedAndTrue(outputSelector.outputOffsetsFromEdges))
347
+ offsetBuilder.announceFacetsWithSectorCoordinatesAroundEdges(builder);
348
+ if (isDefinedAndTrue(outputSelector.outputOffsetsFromVertices))
349
+ offsetBuilder.announceFacetsWithSectorCoordinatesAroundVertices(builder);
350
+ }
351
+ }
352
+ /**
353
+ * For each face of the graph, shift vertices by offsetDistance and emit to the builder as a facet
354
+ * @param polyfaceBuilder
355
+ */
356
+ announceSimpleOffsetFromFaces(polyfaceBuilder, offsetDistance) {
357
+ const xyzLoop = new GrowableXYZArray_1.GrowableXYZArray();
358
+ const xyz = Point3dVector3d_1.Point3d.create(); // reused at each point around each facet.
359
+ const uvw = Point3dVector3d_1.Vector3d.create(); // reused once per facet
360
+ const announceNodeAroundFace = (node) => {
361
+ node.getPoint3d(xyz);
362
+ xyz.addInPlace(uvw);
363
+ xyzLoop.push(xyz);
364
+ return 0;
365
+ };
366
+ this._baseGraph.announceFaceLoops((_graph, seed) => {
367
+ if (!seed.isMaskSet(Graph_1.HalfEdgeMask.EXTERIOR)) {
368
+ const facetProperties = seed.faceTag;
369
+ uvw.setFromVector3d(facetProperties.facetNormal.direction);
370
+ uvw.scaleInPlace(offsetDistance);
371
+ xyzLoop.length = 0;
372
+ seed.sumAroundFace(announceNodeAroundFace);
373
+ polyfaceBuilder.addPolygonGrowableXYZArray(xyzLoop);
374
+ }
375
+ return true;
376
+ });
377
+ }
378
+ /**
379
+ * For each face of the graph, output the xyz of the sector data
380
+ * @param polyfaceBuilder
381
+ */
382
+ announceFacetsWithSectorCoordinatesAroundFaces(polyfaceBuilder) {
383
+ const xyzLoop = new GrowableXYZArray_1.GrowableXYZArray();
384
+ // For face loop visits .. get the point from the sector data.
385
+ const announceNodeAroundFace = (node) => {
386
+ const sectorData = node.edgeTag;
387
+ if (sectorData !== undefined) {
388
+ xyzLoop.push(sectorData.xyz);
389
+ }
390
+ return 0;
391
+ };
392
+ this._baseGraph.announceFaceLoops((_graph, seed) => {
393
+ if (!seed.isMaskSet(Graph_1.HalfEdgeMask.EXTERIOR)) {
394
+ xyzLoop.length = 0;
395
+ seed.sumAroundFace(announceNodeAroundFace);
396
+ if (xyzLoop.length > 2)
397
+ polyfaceBuilder.addPolygonGrowableXYZArray(xyzLoop);
398
+ }
399
+ return true;
400
+ });
401
+ }
402
+ countBits(mask) {
403
+ let n = 0;
404
+ let mask1 = mask;
405
+ while (mask1 !== 0) {
406
+ if (mask1 & 0x01)
407
+ n++;
408
+ mask1 = mask1 >> 1;
409
+ }
410
+ return n;
411
+ }
412
+ /**
413
+ * For each edge of the graph . .
414
+ * * Collect coordinates in 4 sectors going around the edge
415
+ * * Compress with tight tolerance so adjacent sectors with clean point match reduce to a single point.
416
+ * * Emit as a facet.
417
+ * @param polyfaceBuilder
418
+ */
419
+ announceFacetsWithSectorCoordinatesAroundEdges(polyfaceBuilder) {
420
+ const xyzLoop = new GrowableXYZArray_1.GrowableXYZArray();
421
+ const primaryCompressionTolerance = Geometry_1.Geometry.smallMetricDistance;
422
+ const allMasksForEdgesToIgnore = this._exteriorMask
423
+ | this._outsideEndOfChamferFace
424
+ | this._outsideOfChamferFace
425
+ | this._insideOfChamferFace
426
+ | this._insideChamferSling;
427
+ this._baseGraph.announceEdges((_graph, nodeA) => {
428
+ // This starts by looking for EXTERIOR on both sides ...
429
+ if (nodeA.findMaskAroundEdge(this._exteriorMask) !== undefined) {
430
+ return true;
431
+ }
432
+ else if (!nodeA.isMaskSet(allMasksForEdgesToIgnore)) { // By design, we believe that these two test for allMasksForEdgesToIgnore condition would catch the EXTERIOR case above
433
+ const nodeB = nodeA.faceSuccessor;
434
+ const nodeC = nodeA.edgeMate;
435
+ if (!nodeC.isMaskSet(allMasksForEdgesToIgnore)) {
436
+ const nodeD = nodeC.faceSuccessor;
437
+ xyzLoop.clear();
438
+ SectorOffsetProperties.getSectorPointAtHalfEdge(nodeA, undefined, xyzLoop);
439
+ SectorOffsetProperties.getSectorPointAtHalfEdge(nodeB, undefined, xyzLoop);
440
+ SectorOffsetProperties.getSectorPointAtHalfEdge(nodeC, undefined, xyzLoop);
441
+ SectorOffsetProperties.getSectorPointAtHalfEdge(nodeD, undefined, xyzLoop);
442
+ PolylineCompressionByEdgeOffset_1.PolylineCompressionContext.compressInPlaceByShortEdgeLength(xyzLoop, primaryCompressionTolerance);
443
+ if (xyzLoop.length > 2) {
444
+ polyfaceBuilder.addPolygonGrowableXYZArray(xyzLoop);
445
+ }
446
+ }
447
+ }
448
+ else {
449
+ return true;
450
+ }
451
+ return true;
452
+ });
453
+ }
454
+ getCoordinateString(node, showXYZ = true, showFaceSuccessorXYZ = false) {
455
+ if (showXYZ) {
456
+ if (showFaceSuccessorXYZ) {
457
+ return `${Graph_1.HalfEdge.nodeToIdXYZString(node)} ==> ${Graph_1.HalfEdge.nodeToIdXYZString(node.faceSuccessor)}`;
458
+ }
459
+ else {
460
+ return `${Graph_1.HalfEdge.nodeToIdXYZString(node)}`;
461
+ }
462
+ }
463
+ else {
464
+ if (showFaceSuccessorXYZ) {
465
+ return `==> ${Graph_1.HalfEdge.nodeToIdXYZString(node.faceSuccessor)}`;
466
+ }
467
+ else {
468
+ return "";
469
+ }
470
+ }
471
+ }
472
+ inspectMasks(node, showXYZ = true, showFaceSuccessorXYZ = false) {
473
+ const s = "[";
474
+ const v = s.concat(node.id.toString(), node.isMaskSet(this._exteriorMask) ? "X" : "", node.isMaskSet(this.breakMaskA) ? "A" : "", node.isMaskSet(this.breakMaskB) ? "B" : "", node.isMaskSet(this.insideChamferSling) ? "(sling)" : "", node.isMaskSet(this.insideOfChamferFace) ? "(in chamfer)" : "", node.isMaskSet(this.outsideEndOfChamferFace) ? "(@sling)" : "", node.isMaskSet(this.outsideOfChamferFace) ? "(@chamfer)" : "", this.getCoordinateString(node, showXYZ, showFaceSuccessorXYZ), "]");
475
+ return v;
476
+ }
477
+ /**
478
+ * For each face of the graph, output the xyz of the sector data
479
+ * @param polyfaceBuilder
480
+ */
481
+ announceFacetsWithSectorCoordinatesAroundVertices(polyfaceBuilder) {
482
+ const xyzLoop = new GrowableXYZArray_1.GrowableXYZArray();
483
+ const primaryCompressionTolerance = Geometry_1.Geometry.smallMetricDistance;
484
+ this._baseGraph.announceVertexLoops((_graph, seed) => {
485
+ if (!seed.findMaskAroundVertex(this._exteriorMask)) {
486
+ xyzLoop.length = 0;
487
+ seed.sumAroundVertex((node) => {
488
+ if (!node.isMaskSet(this._insideChamferSling))
489
+ SectorOffsetProperties.getSectorPointAtHalfEdge(node, undefined, xyzLoop);
490
+ return 0.0;
491
+ });
492
+ PolylineCompressionByEdgeOffset_1.PolylineCompressionContext.compressInPlaceByShortEdgeLength(xyzLoop, primaryCompressionTolerance);
493
+ if (xyzLoop.length > 2) {
494
+ polyfaceBuilder.addPolygonGrowableXYZArray(xyzLoop);
495
+ }
496
+ }
497
+ return true;
498
+ });
499
+ }
500
+ /**
501
+ * * Exterior half edges have HalfEdgeMask.EXTERIOR
502
+ * * All interior half edge around a facet have facetTag pointing to a facetProperties object for that facet.
503
+ * * the facetOffsetProperties object has the simple facet normal.
504
+ * * Each half edge has edgeTag pointing to to a sectorOffsetProperties object
505
+ * * the sectorOffsetProperties has a copy of the facet normal.
506
+ * @param polyface
507
+ * @returns graph
508
+ */
509
+ static buildBaseGraph(polyface) {
510
+ const graphBuilder = new HalfEdgeGraphFromIndexedLoopsContext_1.HalfEdgeGraphFromIndexedLoopsContext();
511
+ const visitor = polyface.createVisitor();
512
+ const xyzA = Point3dVector3d_1.Point3d.create();
513
+ const xyzB = Point3dVector3d_1.Point3d.create();
514
+ for (visitor.reset(); visitor.moveToNextFacet();) {
515
+ const normal = PolygonOps_1.PolygonOps.centroidAreaNormal(visitor.point);
516
+ if (normal !== undefined) {
517
+ const edgeA = graphBuilder.insertLoop(visitor.pointIndex, (insideHalfEdge) => {
518
+ const mate = insideHalfEdge.edgeMate;
519
+ polyface.data.getPoint(insideHalfEdge.i, xyzA);
520
+ insideHalfEdge.setXYZ(xyzA);
521
+ polyface.data.getPoint(mate.i, xyzB);
522
+ mate.setXYZ(xyzB);
523
+ });
524
+ const facetProperties = new FacetOffsetProperties(visitor.currentReadIndex(), normal);
525
+ if (edgeA !== undefined) {
526
+ edgeA.sumAroundFace((edgeB) => {
527
+ edgeB.faceTag = facetProperties;
528
+ edgeB.edgeTag = new SectorOffsetProperties(normal.direction.clone(), edgeB.getPoint3d());
529
+ return 0;
530
+ });
531
+ }
532
+ }
533
+ }
534
+ return graphBuilder.graph;
535
+ }
536
+ setOffsetAtDistanceAroundVertex(vertexSeed, distance, ignoreChamfers = false) {
537
+ vertexSeed.sumAroundVertex((nodeAroundVertex) => {
538
+ const props = nodeAroundVertex.edgeTag;
539
+ if (props !== undefined) {
540
+ if (ignoreChamfers && this.isInsideChamferOrSling(vertexSeed)) {
541
+ // SKIP !!
542
+ }
543
+ else {
544
+ props.setOffsetPointAtDistanceAtHalfEdge(nodeAroundVertex, distance);
545
+ }
546
+ }
547
+ return 0.0;
548
+ });
549
+ }
550
+ setOffsetXYAndZAroundVertex(vertexSeed, xyz) {
551
+ vertexSeed.sumAroundVertex((nodeAroundVertex) => {
552
+ const props = nodeAroundVertex.edgeTag;
553
+ if (props !== undefined) {
554
+ props.setXYAndZ(xyz);
555
+ nodeAroundVertex.setMask(this._offsetCoordinatesReassigned);
556
+ }
557
+ return 0.0;
558
+ });
559
+ }
560
+ /**
561
+ * * start at vertexSeed.
562
+ * * set the offset point at up to (and including) one with (a) this._breakMaskB or (b) this._exteriorMask
563
+ * *
564
+ * @param vertexSeed first node to mark.
565
+ * @param f function to call to announce each node and its sector properties.
566
+ * @returns number of nodes marked.
567
+ */
568
+ announceNodeAndSectorPropertiesInSmoothSector(vertexSeed, f) {
569
+ let n = 0;
570
+ for (let currentNode = vertexSeed;; currentNode = currentNode.vertexSuccessor) {
571
+ const props = currentNode.edgeTag;
572
+ if (props !== undefined) {
573
+ f(currentNode, props);
574
+ n++;
575
+ }
576
+ if (currentNode.isMaskSet(this._breakMaskB))
577
+ return n;
578
+ // REMARK: these additional exit conditions should not happen if (a) the graph is properly marked and (b) the start node is not exterior.
579
+ if (currentNode.isMaskSet(this._exteriorMask))
580
+ return n;
581
+ if (currentNode === vertexSeed && n === 0)
582
+ return n;
583
+ }
584
+ }
585
+ computeAverageNormalAndMaxDeviationAroundVertex(vertexSeed, data) {
586
+ data.clear();
587
+ const inactiveNodeMask = this._exteriorMask | this._insideChamferSling;
588
+ vertexSeed.sumAroundVertex((node) => {
589
+ const sectorData = node.edgeTag;
590
+ if (sectorData)
591
+ data.accumulateNormal(node, sectorData.normal, inactiveNodeMask);
592
+ return 0.0;
593
+ });
594
+ if (!data.finishNormalAveraging()) {
595
+ return undefined;
596
+ }
597
+ vertexSeed.sumAroundVertex((node) => {
598
+ const sectorData = node.edgeTag;
599
+ if (sectorData)
600
+ data.recordDeviation(sectorData.normal, !node.isMaskSet(inactiveNodeMask));
601
+ return 0.0;
602
+ });
603
+ return data.maxDeviationRadians;
604
+ }
605
+ assignOffsetByAverageNormalAroundVertex(vertexSeed, maxAllowedDeviationRadians, data, distance) {
606
+ const maxDeviationRadians = this.computeAverageNormalAndMaxDeviationAroundVertex(vertexSeed, data);
607
+ if (OffsetMeshContext.stringDebugFunction) {
608
+ OffsetMeshContext.stringDebugFunction(`XYZ ${Graph_1.HalfEdge.nodeToIdXYZString(vertexSeed)} Average Normal ${data.averageNormal.toJSON()}`);
609
+ OffsetMeshContext.stringDebugFunction(` angle ratio ${data.radiansSum / (2 * Math.PI)} maxDeviation ${data.maxDeviationRadiansFromAverage}`);
610
+ }
611
+ if (maxDeviationRadians !== undefined && maxDeviationRadians <= maxAllowedDeviationRadians) {
612
+ vertexSeed.sumAroundVertex((node) => {
613
+ SectorOffsetProperties.setNormalAtHalfEdge(node, data.averageNormal, distance);
614
+ return 0;
615
+ });
616
+ return true;
617
+ }
618
+ return false;
619
+ }
620
+ /** Search around a vertex for a sector which has a different normal from its vertexPredecessor.
621
+ * * The seed will be the first candidate considered
622
+ */
623
+ markBreakEdgesAndSaveAverageNormalsAroundVertex(vertexSeed) {
624
+ vertexSeed.clearMaskAroundVertex(this._breakMaskA);
625
+ vertexSeed.clearMaskAroundVertex(this._breakMaskB);
626
+ const smoothSingleSmoothRadiansBetweenNormals = this._smoothRadiansBetweenNormals;
627
+ const accumulatedRadiansBetweenNormals = this._smoothAccumulatedRadiansBetweenNormals;
628
+ // Step 1: Examine the edge between nodeA and the sector on its vertex predecessor side. This (alone) determines single angle breaks.
629
+ let numBreaks = 0;
630
+ let nodeP = vertexSeed;
631
+ let _numSmooth = 0;
632
+ do {
633
+ const nodeQ = nodeP.edgeMate;
634
+ const nodeR = nodeQ.faceSuccessor; // same as nodeA.vertexPredecessor
635
+ if (nodeP.isMaskSet(this._exteriorMask)) {
636
+ if (!nodeQ.isMaskSet(this._exteriorMask)) {
637
+ nodeR.setMask(this._breakMaskB);
638
+ numBreaks++;
639
+ }
640
+ }
641
+ else {
642
+ if (nodeP.isMaskSet(this._outsideOfChamferFace)) {
643
+ nodeP.setMask(this._breakMaskA);
644
+ }
645
+ else if (nodeP.isMaskSet(this._outsideEndOfChamferFace)) {
646
+ nodeP.setMask(this._breakMaskA);
647
+ nodeP.setMask(this._breakMaskB);
648
+ }
649
+ else if (nodeP.isMaskSet(this._insideChamferSling)) {
650
+ // This is the sling. It's normal is along edge -- not really a break.
651
+ }
652
+ else if (nodeP.isMaskSet(this._insideOfChamferFace)) {
653
+ nodeP.setMask(this._breakMaskA);
654
+ nodeP.setMask(this._breakMaskB);
655
+ nodeR.setMask(this._breakMaskB);
656
+ }
657
+ else if (nodeQ.isMaskSet(this._exteriorMask)) {
658
+ numBreaks++;
659
+ nodeP.setMask(this._breakMaskA);
660
+ }
661
+ else if (!SectorOffsetProperties.almostEqualNormals(nodeP.edgeTag, nodeR.edgeTag, smoothSingleSmoothRadiansBetweenNormals)) {
662
+ nodeP.setMask(this._breakMaskA);
663
+ numBreaks++;
664
+ nodeR.setMask(this._breakMaskB);
665
+ }
666
+ else {
667
+ _numSmooth++;
668
+ }
669
+ }
670
+ nodeP = nodeP.vertexSuccessor;
671
+ } while (nodeP !== vertexSeed);
672
+ if (OffsetMeshContext.stringDebugFunction !== undefined)
673
+ OffsetMeshContext.stringDebugFunction(` numSkip ${_numSmooth} `);
674
+ if (numBreaks === 0) {
675
+ // make the first vertex a break so subsequent searches have a place to start
676
+ vertexSeed.setMask(this._breakMaskA);
677
+ vertexSeed.vertexPredecessor.setMask(this._breakMaskB);
678
+ numBreaks = 1;
679
+ }
680
+ // Step 2: At each single break, sweep forward to its closing breakB. Insert breaks at accumulated angles.
681
+ // (minor TODO: for the insertion case, try to split more equally.)
682
+ const nodeAStart = nodeP.findMaskAroundVertex(this._breakMaskA);
683
+ if (nodeAStart !== undefined) {
684
+ nodeP = nodeAStart;
685
+ do {
686
+ if (nodeP.isMaskSet(this._breakMaskA) && !nodeP.isMaskSet(this._breakMaskB)) {
687
+ let accumulatedRadians = 0.0;
688
+ do {
689
+ const nodeB = nodeP.vertexSuccessor;
690
+ accumulatedRadians += SectorOffsetProperties.radiansBetweenNormals(nodeP.edgeTag, nodeB.edgeTag);
691
+ if (accumulatedRadians > accumulatedRadiansBetweenNormals) {
692
+ nodeP.setMask(this._breakMaskB);
693
+ nodeB.setMask(this._breakMaskA);
694
+ numBreaks++;
695
+ accumulatedRadians = 0.0;
696
+ }
697
+ nodeP = nodeB;
698
+ } while (!nodeP.isMaskSet(this._breakMaskB));
699
+ }
700
+ else {
701
+ nodeP = nodeP.vertexSuccessor;
702
+ }
703
+ } while (nodeP !== nodeAStart);
704
+ }
705
+ if (numBreaks > 0 && nodeAStart !== undefined) {
706
+ // In each compound sector, accumulate and install average normal.
707
+ nodeP = nodeAStart;
708
+ const averageNormal = Point3dVector3d_1.Vector3d.create();
709
+ const edgeVectorU = Point3dVector3d_1.Vector3d.create();
710
+ const edgeVectorV = Point3dVector3d_1.Vector3d.create();
711
+ averageNormal.setZero();
712
+ do {
713
+ if (nodeP.isMaskSet(this._breakMaskA) && !nodeP.isMaskSet(this._breakMaskB)) {
714
+ let nodeQ = nodeP;
715
+ averageNormal.setZero();
716
+ for (;;) {
717
+ nodeQ.vectorToFaceSuccessor(edgeVectorU);
718
+ nodeQ.vectorToFacePredecessor(edgeVectorV);
719
+ let singleSectorRadians = edgeVectorU.signedRadiansTo(edgeVectorV, nodeQ.faceTag.facetNormal.direction);
720
+ if (singleSectorRadians < 0.0)
721
+ singleSectorRadians += Math.PI * 2;
722
+ SectorOffsetProperties.accumulateScaledNormalAtHalfEdge(nodeQ, singleSectorRadians, averageNormal);
723
+ if (nodeQ.isMaskSet(this._breakMaskB))
724
+ break;
725
+ nodeQ = nodeQ.vertexSuccessor;
726
+ }
727
+ if (averageNormal.normalizeInPlace()) {
728
+ nodeQ = nodeP;
729
+ for (;;) {
730
+ SectorOffsetProperties.setNormalAtHalfEdge(nodeQ, averageNormal);
731
+ if (nodeQ.isMaskSet(this._breakMaskB))
732
+ break;
733
+ nodeQ = nodeQ.vertexSuccessor;
734
+ }
735
+ }
736
+ }
737
+ nodeP = nodeP.vertexSuccessor;
738
+ } while (nodeP !== nodeAStart);
739
+ }
740
+ }
741
+ /** Compute the point of intersection of the planes in the sectors of 3 half edges */
742
+ compute3SectorIntersection(nodeA, nodeB, nodeC, result) {
743
+ const sectorA = nodeA.edgeTag;
744
+ const sectorB = nodeB.edgeTag;
745
+ const sectorC = nodeC.edgeTag;
746
+ const vector = Polynomials_1.SmallSystem.intersect3Planes(sectorA.xyz, sectorA.normal, sectorB.xyz, sectorB.normal, sectorC.xyz, sectorC.normal, result);
747
+ return vector;
748
+ }
749
+ /** Compute the point of intersection of the planes in the sectors of 3 half edges */
750
+ compute3SectorIntersectionDebug(nodeA, nodeB, nodeC, result) {
751
+ const sectorA = nodeA.edgeTag;
752
+ const sectorB = nodeB.edgeTag;
753
+ const sectorC = nodeC.edgeTag;
754
+ if (OffsetMeshContext.stringDebugFunction !== undefined) {
755
+ OffsetMeshContext.stringDebugFunction(`compute3${this.inspectMasks(nodeA)}${this.inspectMasks(nodeB)}${this.inspectMasks(nodeC)} `);
756
+ for (const sector of [sectorA, sectorB, sectorC])
757
+ emitSector(sector);
758
+ }
759
+ const vector = Polynomials_1.SmallSystem.intersect3Planes(sectorA.xyz, sectorA.normal, sectorB.xyz, sectorB.normal, sectorC.xyz, sectorC.normal, result);
760
+ if (OffsetMeshContext.stringDebugFunction !== undefined) {
761
+ if (vector === undefined)
762
+ OffsetMeshContext.stringDebugFunction(" NO INTERSECTION");
763
+ else
764
+ OffsetMeshContext.stringDebugFunction(` ComputedVector ${vector.x},${vector.y},${vector.z} `);
765
+ }
766
+ return vector;
767
+ }
768
+ /** Compute the point of intersection of the planes in the sectors of 2 half edges, using cross product of their normals to resolve */
769
+ compute2SectorIntersection(nodeA, nodeB, result) {
770
+ const sectorA = nodeA.edgeTag;
771
+ const sectorB = nodeB.edgeTag;
772
+ const normalC = sectorA.normal.crossProduct(sectorB.normal);
773
+ return Polynomials_1.SmallSystem.intersect3Planes(sectorA.xyz, sectorA.normal, sectorB.xyz, sectorB.normal, sectorB.xyz, normalC, result);
774
+ }
775
+ /**
776
+ * * at input, graph has all original faces and edges
777
+ * * each sector points to a faceProperties with original facet normal
778
+ * * at exit:
779
+ * * new "chamfer faces" are added outside of edges with angle between normal sin excess of options.chamferTurnAngleBetweenNormals
780
+ * * the original edge is split along its length to create space
781
+ * * one edge "along" each direction inside the slit.
782
+ * * a sling edge at each end of the slit.
783
+ * * outside of the sling is part of the slit face loop.
784
+ * * inside is a single-node face
785
+ * * thus the slit itself has 4 nodes.
786
+ * * the two nodes at each end can thus contain the two distinct points at that end of the chamfer.
787
+ * * all 4 nodes of the slit face point to a new FacetOffsetProperties with the average normal.
788
+ * * the inside of each sling face has
789
+ * * original vertex coordinates in the node
790
+ * * face properties with a normal pointing outward from that end of the original edge -- hence define a plane that can clip the chamfer
791
+ * * the two points at each end of the chamfer are computed as the intersection of
792
+ * * chamfer plane
793
+ * * sling plane
794
+ * * adjacent plane of the face on the other side of the edge being chamfered.
795
+ * @param distance distance to offset. The sign of this is important in the chamfer construction.
796
+ */
797
+ addChamferTopologyToAllEdges(options, distance) {
798
+ const edgesToChamfer = [];
799
+ const chamferRadians = options.chamferAngleBetweenNormals.radians;
800
+ const vertexXYZ = Point3dVector3d_1.Point3d.create(); // reuse
801
+ const edgeVector = Point3dVector3d_1.Vector3d.create(); // reuse
802
+ const outwardEdgeVector = Point3dVector3d_1.Vector3d.create(); // reuse
803
+ const averageNormal = Point3dVector3d_1.Vector3d.create(); // reuse
804
+ // collect all the edges with sharp turn angle.
805
+ this._baseGraph.announceEdges((_graph, edgeNode) => {
806
+ if (SectorOffsetProperties.edgeHasLargeExteriorAngleBetweenNormals(edgeNode, edgeVector, averageNormal, distance, chamferRadians)) {
807
+ edgesToChamfer.push(edgeNode);
808
+ return true;
809
+ }
810
+ return true;
811
+ });
812
+ // Create sliver faces.
813
+ // Sliver face gets an average normal from its neighbors.
814
+ // outsideA is the HalfEdge labeled A in the diagram.
815
+ // sliverDX and sliverDY are the edges "inside the sliver" at the respective X and Y ends.
816
+ for (const outsideA of edgesToChamfer) {
817
+ // remark: this recomputes as in collection round.
818
+ if (SectorOffsetProperties.edgeHasLargeExteriorAngleBetweenNormals(outsideA, edgeVector, averageNormal, chamferRadians)) {
819
+ // This copies coordinates and vertex id .... sectorOffsetProperties are delayed until late in the 2-pass loop below.
820
+ // The returned HalfEdge is labeled D in the diagram
821
+ const sliverDX = this._baseGraph.splitEdgeCreateSliverFace(outsideA);
822
+ const sliverDY = sliverDX.facePredecessor;
823
+ const offsetPoint = sliverDX.getPoint3d();
824
+ offsetPoint.addScaledInPlace(averageNormal, distance);
825
+ const ray = Ray3d_1.Ray3d.createCapture(offsetPoint, averageNormal.clone());
826
+ const facetProperties = new FacetOffsetProperties(-1, ray);
827
+ // for each side (hence end) of the sliver face, set mask and install a sling loop for the anticipated end of the chamfer face
828
+ // new node names in the loop omit X or Y suffix because that is implied by which pass is running.
829
+ let s = -1.0;
830
+ for (const sliverD of [sliverDX, sliverDY]) {
831
+ edgeVector.scale(s, outwardEdgeVector);
832
+ sliverD.getPoint3d(vertexXYZ);
833
+ sliverD.setMask(this._insideOfChamferFace);
834
+ sliverD.edgeMate.setMask(this._outsideOfChamferFace);
835
+ // mark and reference the chamfer face.
836
+ sliverD.faceTag = facetProperties;
837
+ // sling at this end
838
+ const slingB = this._baseGraph.splitEdge(undefined, vertexXYZ.x, vertexXYZ.y, vertexXYZ.z, sliverD.i);
839
+ const slingC = slingB.edgeMate;
840
+ slingB.setMask(this._outsideEndOfChamferFace);
841
+ slingB.faceTag = facetProperties;
842
+ slingC.setMask(this._insideChamferSling);
843
+ Graph_1.HalfEdge.pinch(sliverD, slingB);
844
+ const endNormal = Ray3d_1.Ray3d.create(vertexXYZ, outwardEdgeVector); // clones the inputs
845
+ const slingFaceProperties = new FacetOffsetProperties(-1, endNormal);
846
+ slingC.faceTag = slingFaceProperties;
847
+ // initialize sectors with existing vertex point.
848
+ sliverD.edgeTag = new SectorOffsetProperties(averageNormal.clone(), offsetPoint.clone());
849
+ slingB.edgeTag = new SectorOffsetProperties(averageNormal.clone(), offsetPoint.clone());
850
+ slingC.edgeTag = new SectorOffsetProperties(outwardEdgeVector.clone(), vertexXYZ.clone());
851
+ // OffsetMeshContext.stringDebugFunction("Chamfer Setup");
852
+ const chamferPointE = this.compute3SectorIntersection(sliverD, sliverD.edgeMate, slingC);
853
+ const chamferPointF = this.compute3SectorIntersection(slingB, slingB.vertexSuccessor, slingC);
854
+ // sliverD.edgeTag = new SectorOffsetProperties(averageNormal.clone(), vertexXYZ.clone());
855
+ SectorOffsetProperties.setXYZAtHalfEdge(sliverD, chamferPointE);
856
+ SectorOffsetProperties.setXYZAtHalfEdge(slingB, chamferPointF);
857
+ s *= -1.0;
858
+ }
859
+ }
860
+ }
861
+ }
862
+ /**
863
+ * * at input:
864
+ * * Each node points to sectorOffsetProperties with previously computed XYZ (presumably mismatched)
865
+ * * at exit:
866
+ * * Each sectorOffsetProperties has an offset point computed with consideration of offset planes in the neighborhood.
867
+ * @param distance distance to offset.
868
+ */
869
+ computeOffsetFacetIntersections(distance) {
870
+ if (OffsetMeshContext.stringDebugFunction !== undefined)
871
+ OffsetMeshContext.stringDebugFunction("***** recompute intersections");
872
+ const breakEdges = [];
873
+ const vertexXYZ = Point3dVector3d_1.Point3d.create();
874
+ const chamferXYZ = Point3dVector3d_1.Point3d.create();
875
+ const maxVertexMove = 2.0 * distance;
876
+ const averageNormalData = new AverageNormalData();
877
+ const maxAllowedNormalDeviationRadians = Angle_1.Angle.degreesToRadians(25.0);
878
+ //
879
+ // FOR EACH VERTEX
880
+ //
881
+ this._baseGraph.announceVertexLoops((_graph, vertexSeedA) => {
882
+ // reposition to an important vertex.
883
+ // first choice: a chamfer face.
884
+ let vertexSeed = vertexSeedA.findMaskAroundVertex(this._outsideEndOfChamferFace);
885
+ if (vertexSeed === undefined)
886
+ vertexSeed = vertexSeedA.findMaskAroundVertex(this._breakMaskA);
887
+ if (vertexSeed === undefined)
888
+ vertexSeed = vertexSeedA;
889
+ if (OffsetMeshContext.stringDebugFunction !== undefined) {
890
+ OffsetMeshContext.stringDebugFunction("");
891
+ OffsetMeshContext.stringDebugFunction(` VERTEX LOOP ${vertexSeed.getPoint3d().toJSON()} `);
892
+ vertexSeed.sumAroundVertex((node) => { OffsetMeshContext.stringDebugFunction(this.inspectMasks(node, false, true)); return 0; });
893
+ }
894
+ // Take care of the easiest vertices directly . . . note that this returns from the lambda, not computeOffsetFacetIntersections
895
+ if (this.assignOffsetByAverageNormalAroundVertex(vertexSeed, maxAllowedNormalDeviationRadians, averageNormalData, distance))
896
+ return true;
897
+ this.markBreakEdgesAndSaveAverageNormalsAroundVertex(vertexSeed);
898
+ this.setOffsetAtDistanceAroundVertex(vertexSeed, distance, true);
899
+ vertexSeed.collectMaskedEdgesAroundVertex(this._breakMaskA, true, breakEdges);
900
+ if (OffsetMeshContext.stringDebugFunction !== undefined) {
901
+ OffsetMeshContext.stringDebugFunction(` BREAK EDGES from ${this.inspectMasks(vertexSeed, true, false)}`);
902
+ for (const node of breakEdges) {
903
+ OffsetMeshContext.stringDebugFunction(this.inspectMasks(node, false, true));
904
+ }
905
+ }
906
+ if (breakEdges.length <= 1) {
907
+ // just one smooth sequence.
908
+ // everything is set already.
909
+ }
910
+ else if (breakEdges.length === 2) {
911
+ // exterior vertex with two incident smooth
912
+ const vectorFromOrigin = this.compute2SectorIntersection(breakEdges[0], breakEdges[1]);
913
+ if (vectorFromOrigin !== undefined) {
914
+ this.setOffsetXYAndZAroundVertex(vertexSeed, vectorFromOrigin);
915
+ }
916
+ }
917
+ else if (breakEdges.length === 3) {
918
+ if (OffsetMeshContext.stringDebugFunction !== undefined)
919
+ OffsetMeshContext.stringDebugFunction(` Vertex Update just ${breakEdges.length} `);
920
+ const vectorFromOrigin = this.compute3SectorIntersection(breakEdges[0], breakEdges[1], breakEdges[2]);
921
+ if (vectorFromOrigin !== undefined) {
922
+ this.setOffsetXYAndZAroundVertex(vertexSeed, vectorFromOrigin);
923
+ }
924
+ // simple 3-face corner . . .
925
+ }
926
+ else {
927
+ // Lots and Lots of edges
928
+ // each set of 3 sectors independently generates an offset for its central sector.
929
+ if (OffsetMeshContext.stringDebugFunction !== undefined)
930
+ OffsetMeshContext.stringDebugFunction(` Vertex Update breakEdges ${breakEdges.length} `);
931
+ vertexSeed.getPoint3d(vertexXYZ);
932
+ // Pass 1 -- look for intersection among multiple chamfers
933
+ for (let i = 0; i < breakEdges.length; i++) {
934
+ const i0 = i;
935
+ const i1 = (i0 + 1) % breakEdges.length;
936
+ const i2 = (i1 + 1) % breakEdges.length;
937
+ if (breakEdges[i0].isMaskSet(this._outsideEndOfChamferFace)
938
+ && breakEdges[i1].isMaskSet(this._outsideOfChamferFace)
939
+ && breakEdges[i2].isMaskSet(this._insideOfChamferFace)) {
940
+ if (OffsetMeshContext.stringDebugFunction !== undefined)
941
+ OffsetMeshContext.stringDebugFunction(` ChamferChamfer Fixup ${this.inspectMasks(breakEdges[i0])} ${this.inspectMasks(breakEdges[i1])} ${this.inspectMasks(breakEdges[i2])} `);
942
+ const vectorFromOrigin = this.compute3SectorIntersection(breakEdges[i0], breakEdges[i1], breakEdges[i2]);
943
+ if (vectorFromOrigin !== undefined) {
944
+ // Treat all 3 spots as possibly compound sequences
945
+ for (const iOutput of [i0, i1, i2]) {
946
+ this.announceNodeAndSectorPropertiesInSmoothSector(breakEdges[iOutput], (node, properties) => {
947
+ properties.setXYAndZ(vectorFromOrigin);
948
+ node.setMask(this._offsetCoordinatesReassigned);
949
+ });
950
+ }
951
+ // Since all three were reset, skip past. This is done on the acyclic integer that controls the loop.
952
+ i += 2;
953
+ }
954
+ }
955
+ }
956
+ // Pass 2 -- look for unassigned nodes just before or after a chamfer.
957
+ // The chamfer wins
958
+ for (let i = 0; i < breakEdges.length; i++) {
959
+ const i0 = i;
960
+ const i1 = (i0 + 1) % breakEdges.length;
961
+ if (this.isInsideSling(breakEdges[i0], breakEdges[i1]))
962
+ continue;
963
+ if (!this.isOffsetAssigned(breakEdges[i0])
964
+ && breakEdges[i1].isMaskSet(this.insideOfChamferFace)) {
965
+ this.transferXYZFromNodeToSmoothSector(breakEdges[i1], breakEdges[i0], "push left from chamfer", chamferXYZ);
966
+ }
967
+ else if (!this.isOffsetAssigned(breakEdges[i1])
968
+ && breakEdges[i0].isMaskSet(this.outsideEndOfChamferFace)) {
969
+ this.transferXYZFromNodeToSmoothSector(breakEdges[i0], breakEdges[i1], "push right from chamfer", chamferXYZ);
970
+ }
971
+ }
972
+ // Pass 3 -- look for unassigned nodes as middle of 3-face intersections
973
+ for (let i = 0; i < breakEdges.length; i++) {
974
+ const i0 = i;
975
+ const i1 = (i0 + 1) % breakEdges.length;
976
+ const i2 = (i1 + 1) % breakEdges.length;
977
+ if (this.isInsideSling(breakEdges[i0], breakEdges[i1], breakEdges[i2]))
978
+ continue;
979
+ if (this.isOffsetAssigned(breakEdges[i1]))
980
+ continue;
981
+ if (OffsetMeshContext.stringDebugFunction !== undefined)
982
+ OffsetMeshContext.stringDebugFunction(` Intersection Fixup ${this.inspectMasks(breakEdges[i0])} ${this.inspectMasks(breakEdges[i1])} ${this.inspectMasks(breakEdges[i2])} `);
983
+ const vectorFromOrigin = this.compute3SectorIntersection(breakEdges[i0], breakEdges[i1], breakEdges[i2]);
984
+ if (vectorFromOrigin !== undefined) {
985
+ if (vertexXYZ.distance(vectorFromOrigin) < maxVertexMove) {
986
+ this.announceNodeAndSectorPropertiesInSmoothSector(breakEdges[i1], (node, properties) => {
987
+ properties.setXYAndZ(vectorFromOrigin);
988
+ node.setMask(this._offsetCoordinatesReassigned);
989
+ });
990
+ }
991
+ }
992
+ }
993
+ }
994
+ if (OffsetMeshContext.stringDebugFunction !== undefined) {
995
+ const n0 = vertexSeed.countMaskAroundVertex(this._offsetCoordinatesReassigned, false);
996
+ const n1 = vertexSeed.countMaskAroundVertex(this._offsetCoordinatesReassigned, true);
997
+ const message = ` **** Vertex offset mask counts(TRUE ${n1})(FALSE ${n0})`;
998
+ OffsetMeshContext.stringDebugFunction(message);
999
+ }
1000
+ return true;
1001
+ });
1002
+ }
1003
+ // return true if any of these nodes is "inside" the sling at the end of a chamfer.
1004
+ isInsideSling(node0, node1, node2) {
1005
+ return node0.isMaskSet(this._insideChamferSling)
1006
+ || (node1 !== undefined && node1.isMaskSet(this._insideChamferSling))
1007
+ || (node2 !== undefined && node2.isMaskSet(this._insideChamferSling));
1008
+ }
1009
+ // return true if any of these nodes is "inside" the sling at the end of a chamfer.
1010
+ isInsideChamferOrSling(node0) {
1011
+ return node0.isMaskSet(this._insideChamferSling)
1012
+ || node0.isMaskSet(this._insideOfChamferFace)
1013
+ || node0.isMaskSet(this._outsideEndOfChamferFace);
1014
+ }
1015
+ isOffsetAssigned(node0, node1, node2) {
1016
+ return node0.isMaskSet(this._offsetCoordinatesReassigned)
1017
+ || (node1 !== undefined && node1.isMaskSet(this._offsetCoordinatesReassigned))
1018
+ || (node2 !== undefined && node2.isMaskSet(this._offsetCoordinatesReassigned));
1019
+ }
1020
+ /**
1021
+ *
1022
+ * @param sourceNode node with good xyz
1023
+ * @param destinationStartNode first of a sequence of nodes to set (delimited by masks)
1024
+ * @param description string for debug
1025
+ * @param workPoint point to use for coordinate transfer.
1026
+ */
1027
+ transferXYZFromNodeToSmoothSector(sourceNode, destinationStartNode, description, workPoint) {
1028
+ if (OffsetMeshContext.stringDebugFunction !== undefined)
1029
+ OffsetMeshContext.stringDebugFunction(` ${description} ${this.inspectMasks(sourceNode)} to ${this.inspectMasks(destinationStartNode)}} `);
1030
+ SectorOffsetProperties.getSectorPointAtHalfEdge(sourceNode, workPoint, undefined);
1031
+ this.announceNodeAndSectorPropertiesInSmoothSector(destinationStartNode, (node, properties) => {
1032
+ properties.setXYAndZ(workPoint);
1033
+ node.setMask(this._offsetCoordinatesReassigned);
1034
+ });
1035
+ }
1036
+ }
1037
+ exports.OffsetMeshContext = OffsetMeshContext;
1038
+ //# sourceMappingURL=OffsetMeshContext.js.map