bspy 4.4__py3-none-any.whl → 5.0.0__py3-none-any.whl

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.
bspy/_spline_milling.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import numpy as np
2
2
  import bspy.spline
3
3
  import bspy.spline_block
4
+ from collections import namedtuple
4
5
 
5
6
  def line_of_curvature(self, uvStart, is_max, tolerance = 1.0e-3):
6
7
  if self.nInd != 2: raise ValueError("Surface must have two independent variables")
@@ -115,6 +116,60 @@ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtr
115
116
  if path is not None and (path.nInd != 1 or path.nDep != 2 or self.nInd != 2):
116
117
  raise ValueError("path must be a 2D curve and self must be a 3D surface")
117
118
 
119
+ # Compute new order, knots, and fillets for offset (ensure order is at least 4).
120
+ Fillet = namedtuple('Fillet', ('adjustment', 'isFillet'))
121
+ newOrder = []
122
+ newKnotList = []
123
+ newUniqueList = []
124
+ filletList = []
125
+ for order, knots in zip(self.order, self.knots):
126
+ min4Order = max(order, 4)
127
+ unique, counts = np.unique(knots, return_counts=True)
128
+ counts += min4Order - order # Ensure order is at least 4
129
+ newOrder.append(min4Order)
130
+ adjustment = 0
131
+ epsilon = np.finfo(unique.dtype).eps
132
+
133
+ # Add first knot.
134
+ newKnots = [unique[0]] * counts[0]
135
+ newUnique = [unique[0]]
136
+ fillets = [Fillet(adjustment, False)]
137
+
138
+ # Add internal knots, checking for C1 discontinuities needing fillets.
139
+ for knot, count in zip(unique[1:-1], counts[1:-1]):
140
+ knot += adjustment
141
+ newKnots += [knot] * count
142
+ newUnique.append(knot)
143
+ # Check for lack of C1 continuity (need for a fillet)
144
+ if count >= min4Order - 1:
145
+ fillets.append(Fillet(adjustment, True))
146
+ # Create parametric space for fillet.
147
+ adjustment += 1
148
+ knot += 1 + epsilon # Add additional adjustment and step slightly past discontinuity
149
+ newKnots += [knot] * (min4Order - 1)
150
+ newUnique.append(knot)
151
+ fillets.append(Fillet(adjustment, False))
152
+
153
+ # Add last knot.
154
+ newKnots += [unique[-1] + adjustment] * counts[-1]
155
+ newUnique.append(unique[-1] + adjustment)
156
+ fillets.append(Fillet(adjustment, False))
157
+
158
+ # Build fillet and knot lists.
159
+ newKnotList.append(np.array(newKnots, knots.dtype))
160
+ newUniqueList.append(np.array(newUnique, knots.dtype))
161
+ filletList.append(fillets)
162
+
163
+ if path is not None:
164
+ min4Order = max(path.order[0], 4)
165
+ newOrder = [min4Order]
166
+ unique, counts = np.unique(path.knots[0], return_counts=True)
167
+ counts += min4Order - path.order[0] # Ensure order is at least 4
168
+ newKnotList = [np.repeat(unique, counts)]
169
+ domain = path.domain()
170
+ else:
171
+ domain = [(unique[0], unique[-1]) for unique in newUniqueList]
172
+
118
173
  # Determine geometry of drill bit.
119
174
  if subtract:
120
175
  edgeRadius *= -1
@@ -126,83 +181,83 @@ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtr
126
181
 
127
182
  # Define drill bit function.
128
183
  if abs(w) < tolerance and path is None: # Simple offset curve or surface
129
- def drillBit(uv):
130
- return self(uv) + edgeRadius * self.normal(uv)
184
+ def drillBit(normal):
185
+ return edgeRadius * normal
131
186
  elif self.nDep == 2: # General offset curve
132
- def drillBit(u):
133
- xy = self(u)
134
- normal = self.normal(u)
187
+ def drillBit(normal):
135
188
  upward = np.sign(normal[1])
136
189
  if upward * normal[1] <= bottom:
137
- xy[0] += edgeRadius * normal[0] + w * np.sign(normal[0])
138
- xy[1] += edgeRadius * normal[1]
190
+ return np.array((edgeRadius * normal[0] + w * np.sign(normal[0]), edgeRadius * normal[1]))
139
191
  else:
140
- xy[0] += bottomRadius * normal[0]
141
- xy[1] += bottomRadius * normal[1] - upward * h
142
- return xy
143
- elif self.nDep == 3 and path is None: # General offset surface
144
- def drillBit(uv):
145
- xyz = self(uv)
146
- normal = self.normal(uv)
192
+ return np.array((bottomRadius * normal[0], bottomRadius * normal[1] - upward * h))
193
+ elif self.nDep == 3: # General offset surface
194
+ def drillBit(normal):
147
195
  upward = np.sign(normal[1])
148
196
  if upward * normal[1] <= bottom:
149
197
  norm = np.sqrt(normal[0] * normal[0] + normal[2] * normal[2])
150
- xyz[0] += edgeRadius * normal[0] + w * normal[0] / norm
151
- xyz[1] += edgeRadius * normal[1]
152
- xyz[2] += edgeRadius * normal[2] + w * normal[2] / norm
198
+ return np.array((edgeRadius * normal[0] + w * normal[0] / norm, edgeRadius * normal[1], edgeRadius * normal[2] + w * normal[2] / norm))
153
199
  else:
154
- xyz[0] += bottomRadius * normal[0]
155
- xyz[1] += bottomRadius * normal[1] - upward * h
156
- xyz[2] += bottomRadius * normal[2]
157
- return xyz
158
- elif self.nDep == 3: # General offset of a given path along a surface
159
- surface = self
160
- self = path # Redefine self to be the path (used below for fitting)
161
- def drillBit(u):
162
- uv = self(u)
163
- xyz = surface(uv)
164
- normal = surface.normal(uv)
165
- upward = np.sign(normal[1])
166
- if upward * normal[1] <= bottom:
167
- norm = np.sqrt(normal[0] * normal[0] + normal[2] * normal[2])
168
- xyz[0] += edgeRadius * normal[0] + w * normal[0] / norm
169
- xyz[1] += edgeRadius * normal[1]
170
- xyz[2] += edgeRadius * normal[2] + w * normal[2] / norm
171
- else:
172
- xyz[0] += bottomRadius * normal[0]
173
- xyz[1] += bottomRadius * normal[1] - upward * h
174
- xyz[2] += bottomRadius * normal[2]
175
- return xyz
200
+ return np.array((bottomRadius * normal[0], bottomRadius * normal[1] - upward * h, bottomRadius * normal[2]))
176
201
  else: # Should never get here (exception raised earlier)
177
202
  raise ValueError("The offset is only defined for 2D curves and 3D surfaces with well-defined normals.")
178
203
 
179
- # Compute new order and knots for offset (ensure order is at least 4).
180
- newOrder = []
181
- newKnots = []
182
- for order, knots in zip(self.order, self.knots):
183
- min4Order = max(order, 4)
184
- unique, count = np.unique(knots, return_counts=True)
185
- count += min4Order - order
186
- newOrder.append(min4Order)
187
- newKnots.append(np.repeat(unique, count))
204
+ # Define function to pass to fit.
205
+ def fitFunction(uv):
206
+ if path is not None:
207
+ uv = path(uv)
208
+
209
+ # Compute adjusted spline uv values, accounting for fillets.
210
+ hasFillet = False
211
+ adjustedUV = uv.copy()
212
+ for (i, u), unique, fillets in zip(enumerate(uv), newUniqueList, filletList):
213
+ ix = np.searchsorted(unique, u, 'right') - 1
214
+ fillet = fillets[ix]
215
+ if fillet.isFillet:
216
+ hasFillet = True
217
+ adjustedUV[i] = unique[ix] - fillet.adjustment
218
+ else:
219
+ adjustedUV[i] -= fillet.adjustment
220
+
221
+ # If we have fillets, compute the normal from their normal fan.
222
+ if hasFillet:
223
+ normal = np.zeros(self.nDep, self.coefs.dtype)
224
+ nudged = adjustedUV.copy()
225
+ for (i, u), unique, fillets in zip(enumerate(uv), newUniqueList, filletList):
226
+ ix = np.searchsorted(unique, u, 'right') - 1
227
+ fillet = fillets[ix]
228
+ if fillet.isFillet:
229
+ epsilon = np.finfo(unique.dtype).eps
230
+ alpha = u - unique[ix]
231
+ np.copyto(nudged, adjustedUV)
232
+ nudged[i] -= epsilon
233
+ normal += (1 - alpha) * self.normal(nudged)
234
+ nudged[i] += 2 * epsilon
235
+ normal += alpha * self.normal(nudged)
236
+ normal = normal / np.linalg.norm(normal)
237
+ else:
238
+ normal = self.normal(adjustedUV)
239
+
240
+ # Return the offset based on the normal.
241
+ return self(adjustedUV) + drillBit(normal)
188
242
 
189
243
  # Fit new spline to offset by drill bit.
190
- offset = bspy.spline.Spline.fit(self.domain(), drillBit, newOrder, newKnots, tolerance)
244
+ offset = bspy.spline.Spline.fit(domain, fitFunction, newOrder, newKnotList, tolerance)
191
245
 
192
246
  # Remove cusps as required (only applies to offset curves).
193
- if removeCusps and self.nInd == 1:
247
+ if removeCusps and (self.nInd == 1 or path is not None):
194
248
  # Find the cusps by checking for tangent direction reversal between the spline and offset.
195
249
  cusps = []
196
250
  previousKnot = None
197
251
  start = None
198
252
  for knot in np.unique(offset.knots[0][offset.order[0]:offset.nCoef[0]]):
199
- tangent = self.derivative((1,), knot)
200
253
  if path is not None:
201
- tangent = surface.jacobian(path(knot)) @ tangent
202
- flipped = np.dot(tangent, offset.derivative((1,), knot)) < 0
203
- if flipped and start is None:
254
+ tangent = self.jacobian(path(knot)) @ path.derivative((1,), knot)
255
+ else:
256
+ tangent = self.derivative((1,), knot)
257
+ negated = np.dot(tangent, offset.derivative((1,), knot)) < 0
258
+ if negated and start is None:
204
259
  start = knot
205
- if not flipped and start is not None:
260
+ if not negated and start is not None:
206
261
  cusps.append((start, previousKnot))
207
262
  start = None
208
263
  previousKnot = knot
@@ -219,7 +274,7 @@ def offset(self, edgeRadius, bitRadius=None, angle=np.pi / 2.2, path=None, subtr
219
274
  # This is necessary to find the intersection point (2 equations, 2 unknowns).
220
275
  tangent = offset.derivative((1,), cusp[0])
221
276
  projection = np.concatenate((tangent / np.linalg.norm(tangent),
222
- surface.normal(path(cusp[0])))).reshape((2,3))
277
+ self.normal(path(cusp[0])))).reshape((2,3))
223
278
  before = before.transform(projection)
224
279
  after = after.transform(projection)
225
280
  block = bspy.spline_block.SplineBlock([[before, after]])
bspy/hyperplane.py CHANGED
@@ -41,19 +41,19 @@ class Hyperplane(Manifold):
41
41
  def __repr__(self):
42
42
  return "Hyperplane({0}, {1}, {2})".format(self._normal, self._point, self._tangentSpace)
43
43
 
44
- def complete_slice(self, slice, solid):
44
+ def complete_cutout(self, cutout, solid):
45
45
  """
46
- Add any missing inherent (implicit) boundaries of this manifold's domain to the given slice of the
47
- given solid that are needed to make the slice valid and complete.
46
+ Add any missing inherent (implicit) boundaries of this manifold's domain to the given cutout of the
47
+ given solid that are needed to make the cutout valid and complete.
48
48
 
49
49
  Parameters
50
50
  ----------
51
- slice : `Solid`
52
- The slice of the given solid formed by the manifold. The slice may be incomplete, missing some of the
51
+ cutout : `Solid`
52
+ The cutout of the given solid formed by the manifold. The cutout may be incomplete, missing some of the
53
53
  manifold's inherent domain boundaries. Its dimension must match `self.domain_dimension()`.
54
54
 
55
55
  solid : `Solid`
56
- The solid being sliced by the manifold. Its dimension must match `self.range_dimension()`.
56
+ The solid determining the cutout of the manifold. Its dimension must match `self.range_dimension()`.
57
57
 
58
58
  Parameters
59
59
  ----------
@@ -63,17 +63,17 @@ class Hyperplane(Manifold):
63
63
 
64
64
  See Also
65
65
  --------
66
- `Solid.slice` : Slice the solid by a manifold.
66
+ `Solid.compute_cutout` : Compute the cutout portion of the manifold within the solid.
67
67
 
68
68
  Notes
69
69
  -----
70
70
  Since hyperplanes have no inherent domain boundaries, this operation only tests for
71
71
  point containment for zero-dimension hyperplanes (points).
72
72
  """
73
- assert self.domain_dimension() == slice.dimension
73
+ assert self.domain_dimension() == cutout.dimension
74
74
  assert self.range_dimension() == solid.dimension
75
- if slice.dimension == 0:
76
- slice.containsInfinity = solid.contains_point(self._point)
75
+ if cutout.dimension == 0:
76
+ cutout.containsInfinity = solid.contains_point(self._point)
77
77
 
78
78
  def copy(self):
79
79
  """
@@ -86,7 +86,7 @@ class Hyperplane(Manifold):
86
86
  return Hyperplane(self._normal, self._point, self._tangentSpace)
87
87
 
88
88
  @staticmethod
89
- def create_axis_aligned(dimension, axis, offset, flipNormal=False):
89
+ def create_axis_aligned(dimension, axis, offset, negateNormal=False):
90
90
  """
91
91
  Create an axis-aligned hyperplane.
92
92
 
@@ -101,7 +101,7 @@ class Hyperplane(Manifold):
101
101
  offset : `float`
102
102
  The offset from zero along the axis of a point on the hyperplane.
103
103
 
104
- flipNormal : `bool`, optional
104
+ negateNormal : `bool`, optional
105
105
  A Boolean indicating that the normal should point toward in the negative direction along the axis.
106
106
  Default is False, meaning the normal points in the positive direction along the axis.
107
107
 
@@ -112,7 +112,7 @@ class Hyperplane(Manifold):
112
112
  """
113
113
  assert dimension > 0
114
114
  diagonal = np.identity(dimension)
115
- sign = -1.0 if flipNormal else 1.0
115
+ sign = -1.0 if negateNormal else 1.0
116
116
  normal = sign * diagonal[:,axis]
117
117
  point = offset * normal
118
118
  if dimension > 1:
@@ -178,21 +178,6 @@ class Hyperplane(Manifold):
178
178
  """
179
179
  return np.dot(self._tangentSpace, np.atleast_1d(domainPoint)) + self._point
180
180
 
181
- def flip_normal(self):
182
- """
183
- Flip the direction of the normal.
184
-
185
- Returns
186
- -------
187
- hyperplane : `Hyperplane`
188
- The hyperplane with flipped normal. The hyperplane retains the same tangent space.
189
-
190
- See Also
191
- --------
192
- `Solid.complement` : Return the complement of the solid: whatever was inside is outside and vice-versa.
193
- """
194
- return Hyperplane(-self._normal, self._point, self._tangentSpace)
195
-
196
181
  @staticmethod
197
182
  def from_dict(dictionary):
198
183
  """
@@ -244,14 +229,14 @@ class Hyperplane(Manifold):
244
229
  (Hyperplanes will have at most one intersection, but other types of manifolds can have several.)
245
230
  Each intersection records either a crossing or a coincident region.
246
231
 
247
- For a crossing, intersection is a `Manifold.Crossing`: (left, right)
248
- * left : `Manifold` in the manifold's domain where the manifold and the other cross.
249
- * right : `Manifold` in the other's domain where the manifold and the other cross.
232
+ For a crossing, intersection is a `Manifold.Crossing`: (firstPart, secondPart)
233
+ * firstPart : `Manifold` in the manifold's domain where the manifold and the other cross.
234
+ * secondPart : `Manifold` in the other's domain where the manifold and the other cross.
250
235
  * Both intersection manifolds have the same domain and range (the crossing between the manifold and the other).
251
236
 
252
- For a coincident region, intersection is a `Manifold.Coincidence`: (left, right, alignment, transform, inverse, translation)
253
- * left : `Solid` in the manifold's domain within which the manifold and the other are coincident.
254
- * right : `Solid` in the other's domain within which the manifold and the other are coincident.
237
+ For a coincident region, intersection is a `Manifold.Coincidence`: (firstPart, secondPart, alignment, transform, inverse, translation)
238
+ * firstPart : `Solid` in the manifold's domain within which the manifold and the other are coincident.
239
+ * secondPart : `Solid` in the other's domain within which the manifold and the other are coincident.
255
240
  * alignment : scalar value holding the normal alignment between the manifold and the other (the dot product of their unit normals).
256
241
  * transform : `numpy.array` holding the transform matrix from the manifold's domain to the other's domain.
257
242
  * inverse : `numpy.array` holding the inverse transform matrix from the other's domain to the boundary's domain.
@@ -260,7 +245,7 @@ class Hyperplane(Manifold):
260
245
 
261
246
  See Also
262
247
  --------
263
- `Solid.slice` : slice the solid by a manifold.
248
+ `Solid.compute_cutout` : Compute the cutout portion of the manifold within the solid.
264
249
  `numpy.linalg.svd` : Compute the singular value decomposition of a matrix array.
265
250
 
266
251
  Notes
@@ -361,6 +346,21 @@ class Hyperplane(Manifold):
361
346
 
362
347
  return intersections
363
348
 
349
+ def negate_normal(self):
350
+ """
351
+ Negate the direction of the normal.
352
+
353
+ Returns
354
+ -------
355
+ hyperplane : `Hyperplane`
356
+ The hyperplane with negated normal. The hyperplane retains the same tangent space.
357
+
358
+ See Also
359
+ --------
360
+ `Solid.complement` : Return the complement of the solid: whatever was inside is outside and vice-versa.
361
+ """
362
+ return Hyperplane(-self._normal, self._point, self._tangentSpace)
363
+
364
364
  def normal(self, domainPoint, normalize=True, indices=None):
365
365
  """
366
366
  Return the normal.
bspy/manifold.py CHANGED
@@ -15,8 +15,8 @@ class Manifold:
15
15
  minSeparation = 0.0001
16
16
  """If two points are within minSeparation of each each other, they are coincident."""
17
17
 
18
- Crossing = namedtuple('Crossing', ('left','right'))
19
- Coincidence = namedtuple('Coincidence', ('left', 'right', 'alignment', 'transform', 'inverse', 'translation'))
18
+ Crossing = namedtuple('Crossing', ('firstPart','secondPart'))
19
+ Coincidence = namedtuple('Coincidence', ('firstPart', 'secondPart', 'alignment', 'transform', 'inverse', 'translation'))
20
20
  """Return type for intersect."""
21
21
 
22
22
  factory = {}
@@ -43,14 +43,14 @@ class Manifold:
43
43
  A list of intersections between the two manifolds.
44
44
  Each intersection records either a crossing or a coincident region.
45
45
 
46
- For a crossing, intersection is a Manifold.Crossing: (left, right)
47
- * left : `Manifold` in the manifold's domain where the manifold and the other cross.
48
- * right : `Manifold` in the other's domain where the manifold and the other cross.
46
+ For a crossing, intersection is a Manifold.Crossing: (firstPart, secondPart)
47
+ * firstPart : `Manifold` in the manifold's domain where the manifold and the other cross.
48
+ * secondPart : `Manifold` in the other's domain where the manifold and the other cross.
49
49
  * Both intersection manifolds have the same domain and range (the crossing between the manifold and the other).
50
50
 
51
- For a coincident region, intersection is Manifold.Coincidence: (left, right, alignment, transform, inverse, translation)
52
- * left : `Solid` in the manifold's domain within which the manifold and the other are coincident.
53
- * right : `Solid` in the other's domain within which the manifold and the other are coincident.
51
+ For a coincident region, intersection is Manifold.Coincidence: (firstPart, secondPart, alignment, transform, inverse, translation)
52
+ * firstPart : `Solid` in the manifold's domain within which the manifold and the other are coincident.
53
+ * secondPart : `Solid` in the other's domain within which the manifold and the other are coincident.
54
54
  * alignment : scalar value holding the normal alignment between the manifold and the other (the dot product of their unit normals).
55
55
  * transform : `numpy.array` holding the matrix transform from the boundary's domain to the other's domain.
56
56
  * inverse : `numpy.array` holding the matrix inverse transform from the other's domain to the boundary's domain.
@@ -63,7 +63,7 @@ class Manifold:
63
63
  See Also
64
64
  --------
65
65
  `intersect` : Intersect two manifolds.
66
- `Solid.slice` : slice the solid by a manifold.
66
+ `Solid.compute_cutout` : Compute the cutout portion of the manifold within the solid.
67
67
 
68
68
  Notes
69
69
  -----
@@ -97,31 +97,31 @@ class Manifold:
97
97
 
98
98
  return intersections, isTwin
99
99
 
100
- def complete_slice(self, slice, solid):
100
+ def complete_cutout(self, cutout, solid):
101
101
  """
102
- Add any missing inherent (implicit) boundaries of this manifold's domain to the given slice of the
103
- given solid that are needed to make the slice valid and complete.
102
+ Add any missing inherent (implicit) boundaries of this manifold's domain to the given cutout of the
103
+ given solid that are needed to make the cutout valid and complete.
104
104
 
105
105
  Parameters
106
106
  ----------
107
- slice : `Solid`
108
- The slice of the given solid formed by the manifold. The slice may be incomplete, missing some of the
107
+ cutout : `Solid`
108
+ The cutout of the given solid formed by the manifold. The cutout may be incomplete, missing some of the
109
109
  manifold's inherent domain boundaries. Its dimension must match `self.domain_dimension()`.
110
110
 
111
111
  solid : `Solid`
112
- The solid being sliced by the manifold. Its dimension must match `self.range_dimension()`.
112
+ The solid determining the cutout of the manifold. Its dimension must match `self.range_dimension()`.
113
113
 
114
114
  See Also
115
115
  --------
116
- `Solid.slice` : Slice the solid by a manifold.
116
+ `Solid.compute_cutout` : Compute the cutout portion of the manifold within the solid.
117
117
 
118
118
  Notes
119
119
  -----
120
120
  For manifolds without inherent domain boundaries (like hyperplanes), the operation does nothing.
121
121
  """
122
- assert self.domain_dimension() == slice.dimension
122
+ assert self.domain_dimension() == cutout.dimension
123
123
  assert self.range_dimension() == solid.dimension
124
-
124
+
125
125
  def copy(self):
126
126
  """
127
127
  Copy the manifold.
@@ -157,21 +157,6 @@ class Manifold:
157
157
  """
158
158
  return None
159
159
 
160
- def flip_normal(self):
161
- """
162
- Flip the direction of the normal.
163
-
164
- Returns
165
- -------
166
- manifold : `Manifold`
167
- The manifold with flipped normal. The manifold retains the same tangent space.
168
-
169
- See Also
170
- --------
171
- `Solid.complement` : Return the complement of the solid: whatever was inside is outside and vice-versa.
172
- """
173
- return None
174
-
175
160
  @staticmethod
176
161
  def from_dict(dictionary):
177
162
  """
@@ -222,14 +207,14 @@ class Manifold:
222
207
  A list of intersections between the two manifolds.
223
208
  Each intersection records either a crossing or a coincident region.
224
209
 
225
- For a crossing, intersection is a `Manifold.Crossing`: (left, right)
226
- * left : `Manifold` in the manifold's domain where the manifold and the other cross.
227
- * right : `Manifold` in the other's domain where the manifold and the other cross.
210
+ For a crossing, intersection is a `Manifold.Crossing`: (firstPart, secondPart)
211
+ * firstPart : `Manifold` in the manifold's domain where the manifold and the other cross.
212
+ * secondPart : `Manifold` in the other's domain where the manifold and the other cross.
228
213
  * Both intersection manifolds have the same domain and range (the crossing between the manifold and the other).
229
214
 
230
- For a coincident region, intersection is a `Manifold.Coincidence`: (left, right, alignment, transform, inverse, translation)
231
- * left : `Solid` in the manifold's domain within which the manifold and the other are coincident.
232
- * right : `Solid` in the other's domain within which the manifold and the other are coincident.
215
+ For a coincident region, intersection is a `Manifold.Coincidence`: (firstPart, secondPart, alignment, transform, inverse, translation)
216
+ * firstPart : `Solid` in the manifold's domain within which the manifold and the other are coincident.
217
+ * secondPart : `Solid` in the other's domain within which the manifold and the other are coincident.
233
218
  * alignment : scalar value holding the normal alignment between the manifold and the other (the dot product of their unit normals).
234
219
  * transform : `numpy.array` holding the transform matrix from the manifold's domain to the other's domain.
235
220
  * inverse : `numpy.array` holding the inverse transform matrix from the other's domain to the boundary's domain.
@@ -239,7 +224,7 @@ class Manifold:
239
224
  See Also
240
225
  --------
241
226
  `cached_intersect` : Intersect two manifolds, caching the result for twins (same intersection but swapping self and other).
242
- `Solid.slice` : slice the solid by a manifold.
227
+ `Solid.compute_cutout` : Compute the cutout portion of the manifold within the solid.
243
228
 
244
229
  Notes
245
230
  -----
@@ -247,6 +232,21 @@ class Manifold:
247
232
  """
248
233
  return NotImplemented
249
234
 
235
+ def negate_normal(self):
236
+ """
237
+ Negate the direction of the normal.
238
+
239
+ Returns
240
+ -------
241
+ manifold : `Manifold`
242
+ The manifold with negated normal. The manifold retains the same tangent space.
243
+
244
+ See Also
245
+ --------
246
+ `Solid.complement` : Return the complement of the solid: whatever was inside is outside and vice-versa.
247
+ """
248
+ return None
249
+
250
250
  def normal(self, domainPoint, normalize=True, indices=None):
251
251
  """
252
252
  Return the normal.