-
Notifications
You must be signed in to change notification settings - Fork 208
/
TorusPipe.ts
352 lines (340 loc) · 15.8 KB
/
TorusPipe.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Solid
*/
import { Arc3d } from "../curve/Arc3d";
import { CurveCollection } from "../curve/CurveCollection";
import { GeometryQuery } from "../curve/GeometryQuery";
import { Loop } from "../curve/Loop";
import { Path } from "../curve/Path";
import { Geometry } from "../Geometry";
import { Angle } from "../geometry3d/Angle";
import { AngleSweep } from "../geometry3d/AngleSweep";
import { GeometryHandler, UVSurface, UVSurfaceIsoParametricDistance } from "../geometry3d/GeometryHandler";
import { Plane3dByOriginAndVectors } from "../geometry3d/Plane3dByOriginAndVectors";
import { Vector2d } from "../geometry3d/Point2dVector2d";
import { Point3d, Vector3d } from "../geometry3d/Point3dVector3d";
import { Range3d } from "../geometry3d/Range";
import { Transform } from "../geometry3d/Transform";
import { SolidPrimitive } from "./SolidPrimitive";
/**
* A torus pipe is a partial torus (donut). In a local coordinate system
* * The z axis passes through the hole.
* * The "major hoop" arc has
* * vectorTheta0 = (radiusA, 0, 0)
* * vectorTheta90 = (0, radiusA, 0)
* * The major arc point at angle theta is `C(theta) = vectorTheta0 * cos(theta) + vectorTheta90 * sin(theta)
* * The minor hoop at theta various with phi "around the minor hoop"
* * (x,y,z) = C(theta) + (radiusB * cos(theta), radiusB * sin(theta), 0) * cos(phi) + (0, 0, radiusB) * sin(phi)
* * The stored form of the torus pipe is oriented for positive volume:
* * Both radii are positive, with radiusA >= radiusB > 0
* * The sweep is positive
* * The coordinate system has positive determinant.
* * For uv parameterization,
* * u is around the minor hoop, with (0..1) mapping to phi of (0 degrees ..360 degrees)
* * v is along the major hoop with (0..1) mapping to theta of (0 .. sweep)
* * a constant v section is a full circle
* * a constant u section is an arc with sweep angle matching the torusPipe sweep angle.
* @public
*/
export class TorusPipe extends SolidPrimitive implements UVSurface, UVSurfaceIsoParametricDistance {
/** String name for schema properties */
public readonly solidPrimitiveType = "torusPipe";
private _localToWorld: Transform;
private _radiusA: number; // radius of (large) circle in xy plane
private _radiusB: number; // radius of (small) circle in xz plane.
private _sweep: Angle;
private _isReversed: boolean;
// constructor captures the pointers!
protected constructor(map: Transform, radiusA: number, radiusB: number, sweep: Angle, capped: boolean) {
super(capped);
this._localToWorld = map;
this._radiusA = radiusA;
this._radiusB = radiusB;
this._sweep = sweep;
this._isReversed = false;
}
/** return a copy of the TorusPipe */
public clone(): TorusPipe {
const result = new TorusPipe(this._localToWorld.clone(), this._radiusA, this._radiusB, this._sweep.clone(), this.capped);
result._isReversed = this._isReversed;
return result;
}
/** Apply `transform` to the local coordinate system. */
public tryTransformInPlace(transform: Transform): boolean {
if (transform.matrix.isSingular())
return false;
transform.multiplyTransformTransform(this._localToWorld, this._localToWorld);
return true;
}
/** Clone this TorusPipe and transform the clone */
public cloneTransformed(transform: Transform): TorusPipe | undefined {
const result = this.clone();
if (!result.tryTransformInPlace(transform))
return undefined;
return result;
}
/** Create a new `TorusPipe`
* @param frame local to world transformation. For best results, the matrix part should be a pure rotation.
* @param majorRadius major hoop radius
* @param minorRadius minor hoop radius
* @param sweep sweep angle for major circle, with positive sweep from x axis towards y axis.
* @param capped true for circular caps
*/
public static createInFrame(frame: Transform, majorRadius: number, minorRadius: number, sweep: Angle, capped: boolean): TorusPipe | undefined {
// force near-zero radii to true zero
majorRadius = Math.abs(Geometry.correctSmallMetricDistance(majorRadius));
minorRadius = Math.abs(Geometry.correctSmallMetricDistance(minorRadius));
if (majorRadius < minorRadius)
return undefined;
if (majorRadius === 0.0)
return undefined;
if (minorRadius === 0.0)
return undefined;
if (sweep.isAlmostZero)
return undefined;
// remove mirror and negative sweep
let yScale = 1.0;
let zScale = 1.0;
let isReversed = false;
if (frame.matrix.determinant() < 0.0)
zScale *= -1.0;
const sweep1 = sweep.clone();
if (sweep.radians < 0.0) {
sweep1.setRadians(-sweep.radians);
zScale *= -1.0;
yScale *= -1.0;
isReversed = true;
}
const frame1 = frame.clone();
frame1.matrix.scaleColumnsInPlace(1, yScale, zScale);
const result = new TorusPipe(frame1, majorRadius, minorRadius, sweep1, capped);
result._isReversed = isReversed;
return result;
}
/** Create a TorusPipe from the typical parameters of the Dgn file */
public static createDgnTorusPipe(center: Point3d, vectorX: Vector3d, vectorY: Vector3d, majorRadius: number, minorRadius: number, sweep: Angle, capped: boolean) {
const vectorZ = vectorX.unitCrossProductWithDefault(vectorY, 0, 0, 1);
const frame = Transform.createOriginAndMatrixColumns(center, vectorX, vectorY, vectorZ);
return TorusPipe.createInFrame(frame, majorRadius, minorRadius, sweep, capped);
}
/** Create a TorusPipe from its primary arc and minor radius */
public static createAlongArc(arc: Arc3d, minorRadius: number, capped: boolean) {
if (!Angle.isAlmostEqualRadiansAllowPeriodShift(0.0, arc.sweep.startRadians))
arc = arc.cloneInRotatedBasis(arc.sweep.startAngle);
const sweepRadians = arc.sweep.sweepRadians;
const data = arc.toScaledMatrix3d();
const frame = Transform.createOriginAndMatrix(data.center, data.axes);
return TorusPipe.createInFrame(frame, data.r0, minorRadius, Angle.createRadians(sweepRadians), capped);
}
/** Return a coordinate frame (right handed, unit axes)
* * origin at center of major circle
* * major circle in plane of first two columns
* * last column perpendicular to first two
*/
public getConstructiveFrame(): Transform | undefined {
return this._localToWorld.cloneRigid();
}
/** Return the center of the torus pipe (inside the donut hole) */
public cloneCenter(): Point3d { return this._localToWorld.getOrigin(); }
/** return unit vector along the x axis (in the major hoop plane) */
public cloneVectorX(): Vector3d {
const xAxis = this._localToWorld.matrix.columnX();
return xAxis.normalizeWithDefault(1, 0, 0, xAxis);
}
/** return unit vector along the y axis (in the major hoop plane) */
public cloneVectorY(): Vector3d {
const yAxis = this._localToWorld.matrix.columnY();
return yAxis.normalizeWithDefault(0, 1, 0, yAxis);
}
/** return unit vector along the z axis */
public cloneVectorZ(): Vector3d {
const zAxis = this._localToWorld.matrix.columnZ();
return zAxis.normalizeWithDefault(0, 0, 1, zAxis);
}
/** get the major hoop radius (`radiusA`) in world coordinates */
public getMajorRadius(): number { return this._radiusA * this._localToWorld.matrix.columnXMagnitude(); }
/** get the minor hoop radius (`radiusB`) in world coordinates */
public getMinorRadius(): number { return this._radiusB * this._localToWorld.matrix.columnZMagnitude(); }
/** get the sweep angle along the major circle. */
public getSweepAngle(): Angle { return this._sweep.clone(); }
/** Ask if this TorusPipe is labeled as reversed */
public getIsReversed(): boolean { return this._isReversed; }
/** Return the sweep angle as a fraction of full 360 degrees (2PI radians) */
public getThetaFraction(): number { return this._sweep.radians / (Math.PI * 2.0); }
/** Return a (clone of) the TorusPipe's local to world transformation. */
public cloneLocalToWorld(): Transform { return this._localToWorld.clone(); }
/** ask if `other` is an instance of `TorusPipe` */
public isSameGeometryClass(other: any): boolean { return other instanceof TorusPipe; }
/** test if `this` and `other` have nearly equal geometry */
public override isAlmostEqual(other: GeometryQuery): boolean {
if (other instanceof TorusPipe) {
if ((!this._sweep.isFullCircle) && this.capped !== other.capped)
return false;
// Compare getter output so that we can equate TorusPipes created/transformed in equivalent ways.
// In particular, the column vectors contribute their scale to the radii, so we ignore their length.
if (!this.cloneCenter().isAlmostEqual(other.cloneCenter()))
return false;
if (!this.cloneVectorX().isAlmostEqual(other.cloneVectorX()))
return false;
if (!this.cloneVectorY().isAlmostEqual(other.cloneVectorY()))
return false;
if (!this.cloneVectorZ().isAlmostEqual(other.cloneVectorZ()))
return false;
if (!Geometry.isSameCoordinate(this.getMinorRadius(), other.getMinorRadius()))
return false;
if (!Geometry.isSameCoordinate(this.getMajorRadius(), other.getMajorRadius()))
return false;
if (!this.getSweepAngle().isAlmostEqualNoPeriodShift(other.getSweepAngle()))
return false;
// ignore _isReversed; it doesn't affect geometry
return true;
}
return false;
}
/** Return the angle (in radians) for given fractional position around the major hoop.
*/
public vFractionToRadians(v: number): number { return this._sweep.radians * v; }
/** Second step of double dispatch: call `handler.handleTorusPipe(this)` */
public dispatchToGeometryHandler(handler: GeometryHandler): any {
return handler.handleTorusPipe(this);
}
/**
* Return the Arc3d section at vFraction. For the TorusPipe, this is a minor circle.
* @param vFraction fractional position along the sweep direction
*/
public constantVSection(v: number): CurveCollection | undefined {
const thetaRadians = this.vFractionToRadians(v);
const c0 = Math.cos(thetaRadians);
const s0 = Math.sin(thetaRadians);
const majorRadius = this._radiusA;
const minorRadius = this._radiusB;
const center = this._localToWorld.multiplyXYZ(majorRadius * c0, majorRadius * s0, 0);
const vector0 = this._localToWorld.multiplyVectorXYZ(minorRadius * c0, minorRadius * s0, 0);
const vector90 = this._localToWorld.multiplyVectorXYZ(0, 0, minorRadius);
return Loop.create(Arc3d.create(center, vector0, vector90));
}
/** Return an arc at constant u, and arc sweep matching this TorusPipe sweep. */
public constantUSection(uFraction: number): CurveCollection | undefined {
const theta1Radians = this._sweep.radians;
const phiRadians = uFraction * 2 * Math.PI;
const majorRadius = this._radiusA;
const minorRadius = this._radiusB;
const transform = this._localToWorld;
const axes = transform.matrix;
const center = this._localToWorld.multiplyXYZ(0, 0, minorRadius * Math.sin(phiRadians));
const rxy = majorRadius + minorRadius * Math.cos(phiRadians);
const vector0 = axes.multiplyXYZ(rxy, 0, 0);
const vector90 = axes.multiplyXYZ(0, rxy, 0);
return Path.create(Arc3d.create(center, vector0, vector90, AngleSweep.createStartEndRadians(0.0, theta1Radians)));
}
/** extend `rangeToExtend` to include this `TorusPipe` */
public extendRange(rangeToExtend: Range3d, transform?: Transform) {
const theta1Radians = this._sweep.radians;
const majorRadius = this._radiusA;
const minorRadius = this._radiusB;
const transform0 = this._localToWorld;
const numThetaSample = Math.ceil(theta1Radians / (Math.PI / 16.0));
const numHalfPhiSample = 16;
let phi0 = 0;
let dPhi = 0;
let numPhiSample = 0;
let theta = 0;
let cosTheta = 0;
let sinTheta = 0;
let rxy = 0;
let phi = 0;
let j = 0;
const dTheta = theta1Radians / numThetaSample;
for (let i = 0; i <= numThetaSample; i++) {
theta = i * dTheta;
cosTheta = Math.cos(theta);
sinTheta = Math.sin(theta);
// At the ends, do the entire phi circle.
// Otherwise only do the outer half
if (i === 0 || i === numThetaSample) {
phi0 = -Math.PI;
dPhi = 2.0 * Math.PI / numHalfPhiSample;
numPhiSample = numHalfPhiSample;
} else {
phi0 = -0.5 * Math.PI;
dPhi = Math.PI / numHalfPhiSample;
numPhiSample = 2 * numHalfPhiSample - 1;
}
if (transform) {
for (j = 0; j <= numPhiSample; j++) {
phi = phi0 + j * dPhi;
rxy = majorRadius + minorRadius * Math.cos(phi);
rangeToExtend.extendTransformTransformedXYZ(transform, transform0,
cosTheta * rxy, sinTheta * rxy,
Math.sin(phi) * minorRadius);
}
} else {
for (j = 0; j <= numPhiSample; j++) {
phi = phi0 + j * dPhi;
rxy = majorRadius + minorRadius * Math.cos(phi);
rangeToExtend.extendTransformedXYZ(transform0,
cosTheta * rxy, sinTheta * rxy,
Math.sin(phi) * minorRadius);
}
}
}
}
/** Evaluate as a uv surface
* @param u fractional position in minor (phi)
* @param v fractional position on major (theta) arc
*/
public uvFractionToPoint(u: number, v: number, result?: Point3d): Point3d {
const thetaRadians = v * this._sweep.radians;
const phiRadians = u * Math.PI * 2.0;
const cosTheta = Math.cos(thetaRadians);
const sinTheta = Math.sin(thetaRadians);
const majorRadius = this._radiusA;
const minorRadius = this._radiusB;
const rxy = majorRadius + Math.cos(phiRadians) * minorRadius;
return this._localToWorld.multiplyXYZ(rxy * cosTheta, rxy * sinTheta, minorRadius * Math.sin(phiRadians), result);
}
/** Evaluate as a uv surface, returning point and two vectors.
* @param u fractional position in minor (phi)
* @param v fractional position on major (theta) arc
*/
public uvFractionToPointAndTangents(u: number, v: number, result?: Plane3dByOriginAndVectors): Plane3dByOriginAndVectors {
const thetaRadians = v * this._sweep.radians;
const phiRadians = u * Math.PI * 2.0;
const fTheta = this._sweep.radians;
const fPhi = Math.PI * 2.0;
const cosTheta = Math.cos(thetaRadians);
const sinTheta = Math.sin(thetaRadians);
const sinPhi = Math.sin(phiRadians);
const cosPhi = Math.cos(phiRadians);
const majorRadius = this._radiusA;
const minorRadius = this._radiusB;
const rxy = majorRadius + Math.cos(phiRadians) * minorRadius;
const rSinPhi = minorRadius * sinPhi;
const rCosPhi = minorRadius * cosPhi; // appears only as derivative of rSinPhi.
return Plane3dByOriginAndVectors.createOriginAndVectors(
this._localToWorld.multiplyXYZ(cosTheta * rxy, sinTheta * rxy, rSinPhi),
this._localToWorld.multiplyVectorXYZ(-cosTheta * rSinPhi * fPhi, -sinTheta * rSinPhi * fPhi, rCosPhi * fPhi),
this._localToWorld.multiplyVectorXYZ(-rxy * sinTheta * fTheta, rxy * cosTheta * fTheta, 0),
result);
}
/**
* Directional distance query
* * u direction is around the (full) minor hoop
* * v direction is around the outer radius, sum of (absolute values of) major and minor radii.
*/
public maxIsoParametricDistance(): Vector2d {
const a = Math.abs(this.getMajorRadius());
const b = Math.abs(this.getMinorRadius());
return Vector2d.create(b * Math.PI * 2.0, (a + b) * this._sweep.radians);
}
/**
* @return true if this is a closed volume.
*/
public get isClosedVolume(): boolean {
return this.capped || this._sweep.isFullCircle;
}
}