-
Notifications
You must be signed in to change notification settings - Fork 8
/
SpaceTransform.js
283 lines (246 loc) · 8.79 KB
/
SpaceTransform.js
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
/*
Similarly as a point can be represented in multiple coordinate systems,
so can a transformation. To prevent users from figuring out how transformations
are converted to other representations, we have SpaceTransform.
The API is similar to SpacePoint. However, instead of offset methods, we have
a set of similarity transformation methods.
*/
var Transform = require('./Transform')
var nudged = require('nudged')
var SpaceTransform = function (reference, T) {
// Immutable i.e. new instances are returned.
//
// Example
// var t = taaspace.SpaceTransform(taa, taaspace.Transform.IDENTITY);
//
// Parameter
// reference
// a SpacePlane, SpacePoint, or SpaceTransform
// an item in space, enabling coord projections.
// T
// Optional. A taaspace.Transform. Default to identity transform.
// DEBUG
if (!('_T' in reference)) {
throw new Error('invalid reference')
}
if (T && !T.hasOwnProperty('tx')) {
throw new Error('invalid transform')
}
// T is the transformation on the plane.
if (typeof T === 'undefined') { T = Transform.IDENTITY }
this.T = T
// _T is the global coordinate transformation of the plane.
if (reference.hasOwnProperty('getGlobalTransform')) {
// Is a SpacePlane
this._T = reference.getGlobalTransform().T
} else {
// Is a SpacePoint or SpaceTransform
this._T = reference._T
}
}
var proto = SpaceTransform.prototype
proto.switchTo = function (newReference) {
// Combine the transformation to a new coordinate system.
// Return new SpaceTransform.
return new SpaceTransform(newReference, this.T)
}
proto.equals = function (st) {
// Test if given SpaceTransform represents equivalent transformation
// regardless of reference.
// Parameters:
// st: a SpaceTransform
// Implementation before 2017-12-03. "regarding the reference"
// return (this.T.equals(st.T) && this._T.equals(st._T))
// Implementation after 2017-12-03. "regardless of reference"
return this.toSpace().T.equals(st.toSpace().T)
}
proto.inverse = function () {
// Return inversed transformation on the same plane.
return new SpaceTransform(this, this.T.inverse())
}
proto.to = function (target) {
// Convert the transform onto the target coordinate plane.
//
// Parameters:
// target: a SpacePlane, SpacePoint, or SpaceTransform
//
// Return:
// new SpaceTransform
var targetGT, this2target, tOnTarget
if (target === null) {
// target is the root node (space)
return this.toSpace()
}
// Target's global transformation. This._T is already global.
if (target.hasOwnProperty('_T')) {
targetGT = target._T
} else if ('getGlobalTransform' in target) {
targetGT = target.getGlobalTransform().T
} else {
throw new Error('Cannot convert SpaceTransform to: ' + target)
}
// Avoid unnecessary, probably rounding errors inducing computation.
if (targetGT.equals(this._T)) {
return this
} // else
// The transformation on the target plane equals to:
// 1) convert from target to current
// 2) execute the transformation
// 3) convert back to target.
// Fortunately we can combine these steps into a one transformation matrix.
// Let us first compute conversion from this to target and remember that:
// x_space = T_plane * x_plane
this2target = targetGT.inverse().multiplyBy(this._T)
tOnTarget = this2target.multiplyBy(this.T.multiplyBy(this2target.inverse()))
return new SpaceTransform(target, tOnTarget)
}
proto.toSpace = function () {
// Convert the transform onto the space coordinate plane.
// Return a new SpaceTransform.
//
// Implementation note:
// We already have coord. transf. from the current plane to the space:
// this._T
// To simulate the transformation on space:
// 1) convert from space to the plane: this._T.inverse()
// 2) apply the transformation
// 3) convert from plane back to the space: this._T
var tOnSpace = this._T.multiplyBy(this.T.multiplyBy(this._T.inverse()))
var spaceMock = { '_T': Transform.IDENTITY }
return new SpaceTransform(spaceMock, tOnSpace)
}
proto.transformBy = function (spaceTransform) {
// Transform the image of this by given SpaceTransform.
var t = spaceTransform.to(this).T
var nt = t.multiplyBy(this.T)
return new SpaceTransform(this, nt)
}
proto.translate = function (domain, range) {
// Move transform image horizontally and vertically by example.
//
// Translate the plane so that after the translation, the domain points
// would be as close to given range points as possible.
//
// Parameters:
// domain: array of SpacePoints
// range: array of SpacePoints
var st = SpaceTransform.estimate(this, 'T', domain, range)
return new SpaceTransform(this, st.T.multiplyBy(this.T))
}
proto.scale = function (pivot, multiplierOrDomain, range) {
// Parameters:
// pivot: a SpacePoint
// multiplier: the scale factor, > 0
// OR
// pivot: a SpacePoint
// domain: array of SpacePoints
// range: array of SpacePoints
var useMultiplier, normPivot, domain, t, st
useMultiplier = (typeof range === 'undefined')
if (useMultiplier) {
normPivot = pivot.toPointOn(this)
// Multiplier does not depend on plane.
t = this.T.scaleBy(multiplierOrDomain, normPivot)
return new SpaceTransform(this, t)
} else {
domain = multiplierOrDomain
st = SpaceTransform.estimate(this, 'S', domain, range, pivot)
// Combine it with the current transformation
t = st.T.multiplyBy(this.T)
return new SpaceTransform(this, t)
}
}
proto.rotate = function (pivot, radiansOrDomain, range) {
// Parameters:
// pivot: a SpacePoint
// radians: rotation angle
// OR
// pivot: a SpacePoint
// domain: array of SpacePoints
// range: array of SpacePoints
var useRadians, normPivot, domain, t, st
useRadians = (typeof range === 'undefined')
if (useRadians) {
normPivot = pivot.to(this)
// Radians do not depend on plane.
t = this.T.rotateBy(radiansOrDomain, normPivot.xy)
return new SpaceTransform(this, t)
} else {
domain = radiansOrDomain
st = SpaceTransform.estimate(this, 'R', domain, range, pivot)
// Combine it with the current transformation
t = st.T.multiplyBy(this.T)
return new SpaceTransform(this, t)
}
}
proto.translateScale = function (domain, range) {
// Parameters:
// domain: array of SpacePoints
// range: array of SpacePoints
var st = SpaceTransform.estimate(this, 'TS', domain, range)
return new SpaceTransform(this, st.T.multiplyBy(this.T))
}
proto.translateRotate = function (domain, range) {
// Parameters:
// domain: array of SpacePoints
// range: array of SpacePoints
var st = SpaceTransform.estimate(this, 'TR', domain, range)
return new SpaceTransform(this, st.T.multiplyBy(this.T))
}
proto.scaleRotate = function (pivot, domain, range) {
// Parameters:
// pivot: SpacePoint
// domain: array of SpacePoints
// range: array of SpacePoints
var st = SpaceTransform.estimate(this, 'SR', domain, range, pivot)
return new SpaceTransform(this, st.T.multiplyBy(this.T))
}
proto.translateScaleRotate = function (domain, range) {
// Parameters:
// domain: array of SpacePoints
// range: array of SpacePoints
var st = SpaceTransform.estimate(this, 'TSR', domain, range)
return new SpaceTransform(this, st.T.multiplyBy(this.T))
}
SpaceTransform.estimate = function (plane, type, domain, range, pivot) {
// Estimate SpaceTransform.
//
// Parameters:
// plane: SpacePlane, the plane of the returned SpaceTransform
// types: transformation type.
// Available types: T,S,R,TS,TR,SR,TSR (see nudged for further details)
// domain: array of SpacePoints
// range: array of SpacePoints
// pivot: SpacePoint, optional pivot, used with types S,R,SR
var normPivot
if (typeof pivot !== 'undefined') {
normPivot = pivot.toPointOn(plane)
}
// Allow single points
if (domain.hasOwnProperty('_T')) { domain = [domain] }
if (range.hasOwnProperty('_T')) { range = [range] }
// Convert all SpacePoints onto the plane and to arrays
var normDomain = SpaceTransform.normalizeToPoints(domain, plane)
var normRange = SpaceTransform.normalizeToPoints(range, plane)
// Then compute optimal transformation on the plane
var T = nudged.estimate(type, normDomain, normRange, normPivot)
return new SpaceTransform(plane, T)
}
SpaceTransform.normalizeToPoints = function (spacePoints, plane) {
// Convert all the space points onto same plane and
// represent the as xy list: [[x1,y1], [x2,y2], ...].
//
// Arguments:
// spacePoints, array of SpacePoints
// plane, a SpacePlane onto normalize. null equals space
// Return:
// array of [x,y] points
var i, p, normalized
normalized = []
for (i = 0; i < spacePoints.length; i += 1) {
p = spacePoints[i]
normalized.push(p.toPointOn(plane))
}
return normalized
}
module.exports = SpaceTransform