wt-huatu 1.3.4

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 (110) hide show
  1. package/Readme.md +223 -0
  2. package/component/HuatuComponentCache.js +89 -0
  3. package/component/HuatuComponentDef.js +249 -0
  4. package/component/HuatuComponentPlacement.js +172 -0
  5. package/component/HuatuComponentPort.js +58 -0
  6. package/component/IHuatuComponent.js +2 -0
  7. package/for-node/node-cli-funcs.js +36 -0
  8. package/geometry/AngDeg.js +68 -0
  9. package/geometry/Angle.js +77 -0
  10. package/geometry/Bezier.js +623 -0
  11. package/geometry/Circle.js +143 -0
  12. package/geometry/Ellipse.js +546 -0
  13. package/geometry/EllipticalArc.js +337 -0
  14. package/geometry/IGeometry.js +1 -0
  15. package/geometry/Intersection.js +601 -0
  16. package/geometry/Line.js +136 -0
  17. package/geometry/LineSeg.js +179 -0
  18. package/geometry/Point.js +88 -0
  19. package/geometry/PolyLine.js +97 -0
  20. package/geometry/Polygon.js +122 -0
  21. package/geometry/Rect.js +149 -0
  22. package/geometry/Ring.js +91 -0
  23. package/geometry/SegmentModel.js +206 -0
  24. package/geometry/Triangle.js +168 -0
  25. package/geometry/Vector.js +92 -0
  26. package/huatu/Huatu.js +4279 -0
  27. package/huatu/HuatuAngleMark.js +400 -0
  28. package/huatu/HuatuBlock.js +26 -0
  29. package/huatu/HuatuClipPath.js +29 -0
  30. package/huatu/HuatuCompDrawing.js +77 -0
  31. package/huatu/HuatuDimMark.js +579 -0
  32. package/huatu/HuatuDrawing.js +185 -0
  33. package/huatu/HuatuDrawingGroup.js +52 -0
  34. package/huatu/HuatuJob.js +347 -0
  35. package/huatu/HuatuLabel.js +311 -0
  36. package/huatu/HuatuLabelStyle.js +190 -0
  37. package/huatu/HuatuLayer.js +10 -0
  38. package/huatu/HuatuMarker.js +209 -0
  39. package/huatu/HuatuParser.js +435 -0
  40. package/huatu/HuatuPath.js +131 -0
  41. package/huatu/HuatuPathSeries.js +51 -0
  42. package/huatu/HuatuPattern.js +54 -0
  43. package/huatu/HuatuPoint.js +20 -0
  44. package/huatu/HuatuPointSeries.js +67 -0
  45. package/huatu/HuatuStyle.js +394 -0
  46. package/huatu/HuatuSymbol.js +81 -0
  47. package/huatu/HuatuSymbolArray.js +235 -0
  48. package/huatu/HuatuTransform.js +113 -0
  49. package/huatu/HuatuVar.js +17 -0
  50. package/huatu/HuatuYml.js +271 -0
  51. package/huatu/IHuatu.js +2 -0
  52. package/huatu/IHuatuYml.js +1 -0
  53. package/huatu/IndentParsingStack.js +82 -0
  54. package/index.d.ts +1747 -0
  55. package/index.js +247 -0
  56. package/math/Complex.js +72 -0
  57. package/math/Matrix.js +31 -0
  58. package/math/NumberRange.js +38 -0
  59. package/math/VarFuncs.js +24 -0
  60. package/math/WtMath.js +217 -0
  61. package/package.json +34 -0
  62. package/path/PathBezier2.js +75 -0
  63. package/path/PathBezier3.js +89 -0
  64. package/path/PathCircle.js +82 -0
  65. package/path/PathConfig.js +81 -0
  66. package/path/PathContinuousSegmentedPath.js +390 -0
  67. package/path/PathEllipse.js +99 -0
  68. package/path/PathEllipticalArc.js +111 -0
  69. package/path/PathGeneric.js +59 -0
  70. package/path/PathLine.js +75 -0
  71. package/path/PathLines.js +46 -0
  72. package/path/PathPolygon.js +142 -0
  73. package/path/PathPolyline.js +266 -0
  74. package/path/PathRect.js +125 -0
  75. package/path/PathRing.js +51 -0
  76. package/path/PathSegmentedPath.js +199 -0
  77. package/path/PathTriangle.js +90 -0
  78. package/presets/marker.js +37 -0
  79. package/presets/pattern.js +85 -0
  80. package/shape/SvgShapeArrowHead.js +92 -0
  81. package/shape/SvgShapeCircle.js +26 -0
  82. package/shape/SvgShapeCross.js +43 -0
  83. package/shape/SvgShapeEllipse.js +28 -0
  84. package/shape/SvgShapeGeneric.js +50 -0
  85. package/shape/SvgShapeHeart.js +40 -0
  86. package/shape/SvgShapeIsoscelesTriangle.js +52 -0
  87. package/shape/SvgShapePolygon.js +39 -0
  88. package/shape/SvgShapeRect.js +26 -0
  89. package/shape/SvgShapeRhombus.js +27 -0
  90. package/shape/SvgShapeStar.js +103 -0
  91. package/svg/ISvg.js +1 -0
  92. package/svg/SvgBBox.js +149 -0
  93. package/svg/SvgConstants.js +14 -0
  94. package/svg/SvgGradient.js +97 -0
  95. package/svg/SvgMarker.js +221 -0
  96. package/svg/SvgPattern.js +228 -0
  97. package/svg/SvgPoint.js +33 -0
  98. package/svg/SvgPointSet.js +41 -0
  99. package/templates/lines.js +35 -0
  100. package/tools/gen-id.js +2 -0
  101. package/tools/html.js +2 -0
  102. package/tools/katex.js +75 -0
  103. package/tools/rand-str.js +122 -0
  104. package/tools/regex.js +15 -0
  105. package/tools/utils.js +438 -0
  106. package/tools/webColor.js +700 -0
  107. package/wire/HuatuNet.js +22 -0
  108. package/wire/HuatuWire.js +415 -0
  109. package/wire/HuatuWireDrawing.js +17 -0
  110. package/wire/IHuatuWire.js +1 -0
package/huatu/Huatu.js ADDED
@@ -0,0 +1,4279 @@
1
+ import { WT_REGEX } from "../tools/regex.js";
2
+ import { booleanCheck, evalExpr2Number, parseStrIntoKeyValuePair, replaceMathPiAndFuncs, resolvePointLike, assignOnlyDefined, fitNumInRange } from "../tools/utils.js";
3
+ import { HuatuPoint } from "./HuatuPoint.js";
4
+ import { HuatuVar } from "./HuatuVar.js";
5
+ import { SvgPoint } from "../svg/SvgPoint.js";
6
+ import { SvgCsts } from "../svg/SvgConstants.js";
7
+ import { WtMath } from "../math/WtMath.js";
8
+ import { NumberRange } from "../math/NumberRange.js";
9
+ import { AngDeg } from "../geometry/AngDeg.js";
10
+ import Point from "../geometry/Point.js";
11
+ import Circle from "../geometry/Circle.js";
12
+ import Rect from "../geometry/Rect.js";
13
+ import Triangle from "../geometry/Triangle.js";
14
+ import Line from "../geometry/Line.js";
15
+ import LineSeg from "../geometry/LineSeg.js";
16
+ import Polygon from "../geometry/Polygon.js";
17
+ import Ellipse from "../geometry/Ellipse.js";
18
+ import { Intersection } from "../geometry/Intersection.js";
19
+ import { Bezier2, Bezier3 } from "../geometry/Bezier.js";
20
+ import { EllipticalArc } from "../geometry/EllipticalArc.js";
21
+ import { HuatuPath } from "./HuatuPath.js";
22
+ import { PathSegmentedPath } from "../path/PathSegmentedPath.js";
23
+ import PolyLine from "../geometry/PolyLine.js";
24
+ import { isValidColor } from "../tools/webColor.js";
25
+ import { HuatuPathSeries } from "./HuatuPathSeries.js";
26
+ import { HuatuSymbol } from "./HuatuSymbol.js";
27
+ import { HuatuStyle } from "./HuatuStyle.js";
28
+ import { HuatuSymbolArray } from "./HuatuSymbolArray.js";
29
+ import { SvgBBox } from "../svg/SvgBBox.js";
30
+ import { HuatuLabel } from "./HuatuLabel.js";
31
+ import { HuatuDrawing } from "./HuatuDrawing.js";
32
+ import { HuatuBlock } from "./HuatuBlock.js";
33
+ import { HuatuLayer } from "./HuatuLayer.js";
34
+ import { HuatuTransform } from "./HuatuTransform.js";
35
+ import { predefinedPatterns } from "../presets/pattern.js";
36
+ import { predefinedMarkers } from "../presets/marker.js";
37
+ import { HuatuPointSeries } from "./HuatuPointSeries.js";
38
+ import { HuatuClipPath } from "./HuatuClipPath.js";
39
+ import { HuatuPattern } from "./HuatuPattern.js";
40
+ import { HuatuMarker } from "./HuatuMarker.js";
41
+ import { HuatuJob } from "./HuatuJob.js";
42
+ import { HuatuLabelStyle } from "./HuatuLabelStyle.js";
43
+ import Ring from "../geometry/Ring.js";
44
+ import { PathPolyline } from "../path/PathPolyline.js";
45
+ import { PathPolygon } from "../path/PathPolygon.js";
46
+ import { loadVarFuncsToGlobalThis } from "../math/VarFuncs.js";
47
+ import { PathRect } from "../path/PathRect.js";
48
+ import { PathTriangle } from "../path/PathTriangle.js";
49
+ import { PathCircle } from "../path/PathCircle.js";
50
+ import { PathEllipse } from "../path/PathEllipse.js";
51
+ import { HuatuComponentDef } from "../component/HuatuComponentDef.js";
52
+ import { HuatuComponentPlacement } from "../component/HuatuComponentPlacement.js";
53
+ import { HuatuComponentPort } from "../component/HuatuComponentPort.js";
54
+ import { HuatuCompDrawing } from "./HuatuCompDrawing.js";
55
+ import { HuatuWire } from "../wire/HuatuWire.js";
56
+ import { HuatuWireDrawing } from "../wire/HuatuWireDrawing.js";
57
+ import { HuatuAngleMark } from "./HuatuAngleMark.js";
58
+ import { HuatuDimMark } from "./HuatuDimMark.js";
59
+ import { genHex } from "../tools/gen-id.js";
60
+ export class WtHuatu {
61
+ title;
62
+ desc;
63
+ config;
64
+ width;
65
+ height;
66
+ viewBox;
67
+ vars = [];
68
+ points = [];
69
+ pointSeries = [];
70
+ paths = [];
71
+ pathSeries = [];
72
+ clips = [];
73
+ patterns = [];
74
+ markers = [];
75
+ styles = [];
76
+ labelStyles = [];
77
+ arrays = [];
78
+ defines = [];
79
+ symbols = [];
80
+ blocks = [];
81
+ labels = [];
82
+ layers = [];
83
+ compDefs = [];
84
+ compPlacements = [];
85
+ // This is for Component placement's use
86
+ compPorts = [];
87
+ // Reserve Solved X-Points to avoid re-computing
88
+ solvedXPoints = [];
89
+ // This is for Resolving wires
90
+ nets = [];
91
+ // Jobs
92
+ draw;
93
+ jobs = [];
94
+ infos = [];
95
+ warnings = [];
96
+ resolvingErrors = [];
97
+ varErrors = [];
98
+ pointErrors = [];
99
+ constructor() {
100
+ /* Load Var Funcs to Global if not */
101
+ if (!globalThis.wtVarFuncs) {
102
+ loadVarFuncsToGlobalThis();
103
+ }
104
+ // TODO: If something else need to be added, please append here
105
+ }
106
+ static REGEX_LABEL_DEF = /^label\s*[,;\s](.*)\|\|\|(.*)$/i;
107
+ static REGEX_WIRE_DEF = /^wire\s*[,;\s](.*)$/i;
108
+ static REGEX_ANGLE_MARK_DEF = /^angleMark\s*[,;\s](.*)$/i;
109
+ static REGEX_DIM_MARK_DEF = /^dimMark\s*[,;\s](.*)$/i;
110
+ static REGEX_SHAPE_AND_DRAWING_DEF = /^([^\|]*)\|\|\|(.*)$/i;
111
+ /**
112
+ * The shortcut function to replace all the vars in an expression with actual values.
113
+ * @param expr The expression to be replaced
114
+ * @returns The result after replaced all the vars
115
+ */
116
+ replaceVars(expr) {
117
+ // If already a number, then skip the replacing
118
+ if (WT_REGEX.FLOAT_NUMBER.test(expr))
119
+ return expr;
120
+ const sortedVars = this.vars
121
+ .map(v => ({ name: v.name, value: v.value }))
122
+ .sort((a, b) => b.name.length - a.name.length); // Make the longest var be replaced firstly
123
+ for (let i = 0; i < sortedVars.length; i++) {
124
+ const varRegex = new RegExp(`(^|[^.])\\b${sortedVars[i].name}\\b`, "g");
125
+ expr = expr.replace(varRegex, (m, g1) => `${g1}(${sortedVars[i].value})`);
126
+ }
127
+ const unReplacedNames = [...expr.matchAll(/(^|[^.])\b([a-z][a-z0-9_]*)\b([^.]|$)/ig)]
128
+ .map(x => x[2]);
129
+ if (unReplacedNames.length > 0) {
130
+ unReplacedNames.forEach(x => {
131
+ this.varErrors.push(`Var [${x}] has not been defined!`);
132
+ });
133
+ return "";
134
+ }
135
+ else {
136
+ return expr;
137
+ }
138
+ }
139
+ /**
140
+ * A shortcut function to replace all the occurred point coordinates in a point composing expression
141
+ * @param expr The point coordinate consisting of something like `pA.x`
142
+ * @returns The expression with `pA.x` replaced with 3 and `pA.y` with 4,
143
+ * if `pA = (3, 4)`
144
+ */
145
+ replaceCoors(expr) {
146
+ let exprCopy = expr;
147
+ const reGetCoors = /\b([a-z][a-z0-9_]*)[.](x|y)\b/ig;
148
+ const notFoundPoints = [];
149
+ let reExec = reGetCoors.exec(expr);
150
+ while (reExec) {
151
+ let pointName = reExec[1];
152
+ let point = this.points.find(x => x.name === pointName);
153
+ if (point) {
154
+ exprCopy = exprCopy.replace(new RegExp(`\\b${point.name}[.]x\\b`), `(${point.x})`)
155
+ .replace(new RegExp(`\\b${point.name}[.]y\\b`), `(${point.y})`);
156
+ }
157
+ else {
158
+ if (!notFoundPoints.includes(pointName))
159
+ notFoundPoints.push(pointName);
160
+ }
161
+ // Search For Next
162
+ reExec = exprCopy ? reGetCoors.exec(expr) : null;
163
+ }
164
+ if (notFoundPoints.length > 0) {
165
+ notFoundPoints.forEach(x => this.pointErrors.push(`Point "${x}" has not been defined!`));
166
+ exprCopy = "";
167
+ }
168
+ return exprCopy;
169
+ }
170
+ replaceModelParams(expr) {
171
+ let exprCopy = expr;
172
+ const reGetParam = /\b([a-z][a-z0-9_]*)[.](r|rx|ry|rin|rout|rotdeg|rotang|angs|ange|angrange|w|h|len|deg|degr|degs|degsr|dege|deger|clens|clene)\b/ig;
173
+ const notFoundModels = [];
174
+ const notFoundScalars = [];
175
+ let reExec = reGetParam.exec(expr);
176
+ while (reExec) {
177
+ let modelName = reExec[1];
178
+ let paramName = reExec[2];
179
+ let path = this.paths.find(x => x.name === modelName);
180
+ if (path && path.model) {
181
+ let param = path.model.getParam(paramName);
182
+ if (param !== undefined) {
183
+ const re = new RegExp(`\\b${modelName}[.]${paramName}\\b`);
184
+ exprCopy = exprCopy.replace(re, `(${param})`);
185
+ }
186
+ else {
187
+ let nfScalar = `${modelName}.${paramName}`;
188
+ if (!notFoundScalars.includes(nfScalar))
189
+ notFoundScalars.push(nfScalar);
190
+ }
191
+ }
192
+ else {
193
+ this.varErrors.push(`"${modelName}" has not been defined as a shape!`);
194
+ if (!notFoundModels.includes(modelName))
195
+ notFoundModels.push(modelName);
196
+ }
197
+ // Search For Next
198
+ reExec = exprCopy ? reGetParam.exec(expr) : null;
199
+ }
200
+ if (notFoundModels.length > 0 || notFoundScalars.length > 0) {
201
+ notFoundModels.forEach(x => this.varErrors.push(`"${x}" has not been defined as a shape!`));
202
+ notFoundScalars.forEach(x => this.varErrors.push(`"${x}" cannot to be resolved to any scalar!`));
203
+ return "";
204
+ }
205
+ return exprCopy;
206
+ }
207
+ /**
208
+ * The function to resolve a user input VAR
209
+ * @param expr The user input of a VAR
210
+ * @returns the resolved result of the VAR
211
+ */
212
+ resolveVarExpr(expr) {
213
+ // If the expression has some comma or semicolon, then stop resolving
214
+ if (expr.match(/[,;]/)) {
215
+ this.varErrors.push(`Var Definition mustn't have comma(,) or semi-colon(,)!`);
216
+ return undefined;
217
+ }
218
+ // Replace PI and Math functions, like, sin, cos, exp, pi, etc.
219
+ expr = replaceMathPiAndFuncs(expr);
220
+ // Replace GeometryModel parameters
221
+ expr = this.replaceModelParams(expr);
222
+ if (!expr)
223
+ return undefined;
224
+ // Replace Var names to values
225
+ expr = this.replaceVars(expr);
226
+ if (!expr)
227
+ return undefined;
228
+ // Evaluate the final expression
229
+ return evalExpr2Number(expr);
230
+ }
231
+ /**
232
+ * Resolve the var provided, and add it into var list.
233
+ * If some error encountered, then some error message will be returned.
234
+ * @param name Name of the var
235
+ * @param value Presentation of the var
236
+ */
237
+ resolveVar(name, value) {
238
+ if (WtHuatu.RESERVED_NAMES.includes(name.toLowerCase())) {
239
+ return [`Function Names (here you input "${name}") cannot be used as Var Name!`];
240
+ }
241
+ if (this.vars.find(v => v.name === name)) {
242
+ return [`There has already been a var named as ${name}`];
243
+ }
244
+ if (WT_REGEX.FLOAT_NUMBER.test(value)) {
245
+ this.vars.push(new HuatuVar(name, parseFloat(value), "direct"));
246
+ return [];
247
+ }
248
+ this.varErrors = [];
249
+ const varRes = this.resolveVarExpr(value);
250
+ if (varRes !== undefined) {
251
+ this.vars.push(new HuatuVar(name, varRes, "derived"));
252
+ return [];
253
+ }
254
+ else {
255
+ return [...this.varErrors];
256
+ }
257
+ }
258
+ addPoint(point) {
259
+ this.points.push(point);
260
+ // Sort the points by which having longest name be the first
261
+ this.points.sort((p1, p2) => p2.name.length - p1.name.length);
262
+ }
263
+ /**
264
+ * The unified method to resolve/calculate the X/Y coordinate of a Point.
265
+ * It can resolve some thing like `pA.x + sin(b) * (5 + r) + pB.y`
266
+ * It will replace the coors of other point(s) and replace functions and vars
267
+ * Then finally evaluate the result.
268
+ * @param expr The expression of X/Y coordinate of a Point
269
+ * @returns The resolved coordinate, a number
270
+ */
271
+ resolvePointCoorsExpr(expr) {
272
+ if (WT_REGEX.FLOAT_NUMBER.test(expr.trim())) {
273
+ return parseFloat(expr.trim());
274
+ }
275
+ // Replace p.X and p.Y to numbers
276
+ expr = this.replaceCoors(expr);
277
+ if (!expr)
278
+ return undefined;
279
+ // Resolve as Var Expression
280
+ return this.resolveVarExpr(expr);
281
+ }
282
+ /**
283
+ * Resolve a point from some PointSeries(with index) or Some Point (by name), or
284
+ * from the definition of XY coordinates.
285
+ * @param expr The string to query of build a point
286
+ * @returns The identified point or undefined
287
+ */
288
+ resolvePointFromExpr(expr) {
289
+ // Check if it is referring some defined Point
290
+ const definedPoint = this.points.find(p => p.name === expr);
291
+ if (definedPoint) {
292
+ return definedPoint.toPoint();
293
+ }
294
+ // Check if the expr is like pathCirc.cin or rect.ne, etc
295
+ const cpTry = /^([a-z][a-z0-9_]*)[.](n|e|s|w|ne|nw|se|sw|a|b|c|o|m|cin|cout|cg|[0-9]+)$/i.exec(expr);
296
+ if (cpTry) {
297
+ const shapeName = cpTry[1];
298
+ const pathTry = this.paths.find(x => x.name === shapeName);
299
+ if (pathTry && pathTry.model) {
300
+ let pTag = cpTry[2];
301
+ if (/[0-9]+/.exec(pTag)) {
302
+ pTag = parseInt(pTag);
303
+ }
304
+ return pathTry.model.getPoint(pTag);
305
+ }
306
+ else if (pathTry?.path instanceof PathSegmentedPath) {
307
+ return pathTry.path.getPoint(cpTry[2].toLowerCase());
308
+ }
309
+ else {
310
+ return undefined;
311
+ }
312
+ }
313
+ // Check if it is referring some point in a Point Series
314
+ const match = expr.match(WT_REGEX.VAR_WITH_INDEXING);
315
+ if (match) {
316
+ const vName = match[1];
317
+ const vIndex = parseInt(match[3] || match[4]);
318
+ const ps = this.pointSeries.find(s => s.name === vName);
319
+ if (ps) {
320
+ return ps.getPoint(vIndex - 1);
321
+ }
322
+ else {
323
+ let path = this.paths.find(p => p.name === vName);
324
+ if (path && (path.path instanceof PathPolygon ||
325
+ path.path instanceof PathPolyline)) {
326
+ return vIndex >= 1 && vIndex <= path.path.points.length
327
+ ? path.path.points[vIndex - 1]
328
+ : undefined;
329
+ }
330
+ }
331
+ }
332
+ // Check if it defined as form of a point XY pair
333
+ const pXY = resolvePointLike(expr);
334
+ if (pXY) {
335
+ const pX = this.resolvePointCoorsExpr(pXY.x);
336
+ const pY = this.resolvePointCoorsExpr(pXY.y);
337
+ if (pX !== undefined && pY !== undefined) {
338
+ return new SvgPoint(pX, pY);
339
+ }
340
+ }
341
+ return undefined;
342
+ }
343
+ /**
344
+ * Resolve the point directly defined, by supplying name and definition
345
+ * @param name name of the point
346
+ * @param value Presented form to calculate the point
347
+ */
348
+ resolvePoint(name, value) {
349
+ if (this.points.find(p => p.name === name)) {
350
+ return [`There has already been a point named as ${name}`];
351
+ }
352
+ const pXY = resolvePointLike(value);
353
+ if (!pXY) {
354
+ return [`Format [${value}] of the definition is incorrect!`];
355
+ }
356
+ ;
357
+ if (WT_REGEX.FLOAT_NUMBER.test(pXY.x) && WT_REGEX.FLOAT_NUMBER.test(pXY.y)) {
358
+ this.addPoint(new HuatuPoint(parseFloat(pXY.x), parseFloat(pXY.y), name, "direct"));
359
+ return [];
360
+ }
361
+ this.varErrors = [];
362
+ this.pointErrors = [];
363
+ const pX = this.resolvePointCoorsExpr(pXY.x);
364
+ if (pX === undefined) {
365
+ this.pointErrors.push(`X part [${pXY.x}] is incorrect!`);
366
+ this.pointErrors.push(...this.varErrors);
367
+ this.varErrors = [];
368
+ }
369
+ const pY = this.resolvePointCoorsExpr(pXY.y);
370
+ if (pY === undefined) {
371
+ this.pointErrors.push(`Y part [${pXY.y}] is incorrect!`);
372
+ this.pointErrors.push(...this.varErrors);
373
+ this.varErrors = [];
374
+ }
375
+ if (pX !== undefined && pY !== undefined) {
376
+ this.addPoint(new HuatuPoint(pX, pY, name, "derived"));
377
+ }
378
+ return [...this.pointErrors];
379
+ }
380
+ resolveSeparatedNumbers(def) {
381
+ return def.split(/[,]/).map(x => x.trim()).filter(x => !!x)
382
+ .map(x => this.resolveVarExpr(x))
383
+ .filter(x => x !== undefined);
384
+ }
385
+ /**
386
+ * This function is some unified method to resolve the (Geometry) Model of the shape defined by user.
387
+ * This function is normally only be used under this module.
388
+ * @param def The Definition string to build/specify one Shape, e.g. "circle;p0;5" means making some circle
389
+ * centered at point p0 and having radius of 5
390
+ * @returns Error Message if errored or the resolved Model of the shape
391
+ */
392
+ resolveShapeDef(def) {
393
+ const params = def.split(";").map(p => p.trim()).filter(p => !!p);
394
+ if (params.length < 2)
395
+ return `Parameters are too less to build a shape/path!`;
396
+ const shapeType = params[0].toLowerCase();
397
+ let p1, p2, p3, p4, rx, ry;
398
+ let line, deg, l1, l2, ln;
399
+ let x1, y1, width, height;
400
+ let ps, pts;
401
+ let pathReferred, pathDerived;
402
+ let dx, dy, pMoveOrAt, rotDeg;
403
+ switch (shapeType) {
404
+ case "ln": // Line, LineSeg, and PolyLine, Unified definition
405
+ case "line": // Line, LineSeg, and PolyLine, Unified definition
406
+ if (params.length < 3)
407
+ return `Parameters are too less to build a Line/LineSeg!`;
408
+ // Try first param, to see it if is defined Line
409
+ line = this.paths.find(s => s.name === params[1]);
410
+ if (line) { // The def is like: line; ln1; l1; l2;
411
+ if (!(line.model instanceof Line))
412
+ return `"${params[1]}" is not a Line!`;
413
+ const l1 = this.resolveVarExpr(params[2]);
414
+ if (l1 === undefined)
415
+ return `Length from Line (${params[2]}) is invalid`;
416
+ const l2 = params.length > 3 ? this.resolveVarExpr(params[3]) : undefined;
417
+ return line.model.buildSeg(l1, l2);
418
+ }
419
+ // Now the first param should be a point
420
+ p1 = this.resolvePointFromExpr(params[1]);
421
+ if (!p1)
422
+ return `The first parameter should be a Point to define a Line/LineSeg/PolyLine!`;
423
+ p2 = this.resolvePointFromExpr(params[2]);
424
+ if (p2) { // In this case, the def is like: line; p1; p2; p3; ... (only valid points picked)
425
+ if (params.length < 4)
426
+ return new LineSeg(p1, p2);
427
+ const pts = [p1, p2];
428
+ for (let idx = 3; idx < params.length; idx++) {
429
+ const p = this.resolvePointFromExpr(params[idx]);
430
+ if (p)
431
+ pts.push(p);
432
+ }
433
+ return new PolyLine(...pts);
434
+ }
435
+ deg = this.resolveVarExpr(params[2]);
436
+ if (deg === undefined)
437
+ return `Slope Angle (${params[2]}) is incorrect!`;
438
+ // The def is like: line; p; deg; l1 (optional); l2 (optional); ... (the rest discarded)
439
+ ln = new Line(p1, AngDeg.fitDeg(deg));
440
+ if (params.length < 4) { // It is just a line
441
+ return ln;
442
+ }
443
+ l1 = this.resolveVarExpr(params[3]);
444
+ if (l1 === undefined)
445
+ return ln; // If the length definition is invalid, then just return the Line
446
+ l2 = params.length > 4 ? this.resolveVarExpr(params[4]) : undefined;
447
+ return ln.buildSeg(l1, l2);
448
+ case "ls":
449
+ case "lineseg":
450
+ if (params.length < 3)
451
+ return `Parameters are too less to build a Line/LineSeg!`;
452
+ // Format of Line Segment:
453
+ // - lineseg; lineName; l1; l2;
454
+ // - lineseg; (x1, y1); (x2, y2);
455
+ // - lineseg; (x, y); deg; l1; l2;
456
+ line = this.paths.find(s => s.name === params[1]);
457
+ if (line) {
458
+ if (!(line.model instanceof Line))
459
+ return `The Shape you picked (${params[1]}) to derive a Line Seg is not a Line!`;
460
+ const l1 = this.resolveVarExpr(params[2]);
461
+ if (l1 === undefined)
462
+ return `Length from Line (${params[2]}) is invalid`;
463
+ const l2 = params.length > 3 ? this.resolveVarExpr(params[3]) : undefined;
464
+ return line.model.buildSeg(l1, l2);
465
+ }
466
+ const pStart = this.resolvePointFromExpr(params[1]);
467
+ const pEnd = this.resolvePointFromExpr(params[2]);
468
+ if (!pStart)
469
+ return `Start Point (${params[1]}) of the Line Seg is invalid!`;
470
+ if (pEnd)
471
+ return new LineSeg(pStart, pEnd);
472
+ deg = this.resolveVarExpr(params[2]);
473
+ if (deg === undefined)
474
+ return `Line Slope degree (${params[2]}) is incorrect!`;
475
+ ln = new Line(pStart, AngDeg.fitDeg(deg));
476
+ if (params.length < 4) {
477
+ return "Length definition from Line is incorrect, no length specified!";
478
+ }
479
+ l1 = this.resolveVarExpr(params[3]);
480
+ if (l1 === undefined)
481
+ return `Line Length (${params[3]}) is incorrect!`;
482
+ l2 = params.length > 4 ? this.resolveVarExpr(params[4]) : undefined;
483
+ return ln.buildSeg(l1, l2);
484
+ case "pln":
485
+ case "pline":
486
+ case "polyline":
487
+ // Check if the first param is point series
488
+ ps = this.pointSeries.find(x => x.name === params[1]);
489
+ if (ps) {
490
+ if (ps.points.length < 2)
491
+ return `Point Series [${params[1]}] has only one point!`;
492
+ else
493
+ pts = ps.points;
494
+ }
495
+ else {
496
+ pts = params.filter((p, i) => i > 0)
497
+ .map(p => this.resolvePointFromExpr(p))
498
+ .filter(p => !!p);
499
+ }
500
+ if (pts.length < 2)
501
+ return `Points provided are too less, need to be 2 (valid) points at least!`;
502
+ return new PolyLine(...pts);
503
+ case "circ":
504
+ case "circle":
505
+ if (params.length < 3)
506
+ return `Parameters are too less to build a Circle!`;
507
+ // Format of a circle define: circle; (x, y); r
508
+ p1 = this.resolvePointFromExpr(params[1]);
509
+ if (!p1)
510
+ return `Circle center (${params[1]}) is invalid!`;
511
+ let radius = this.resolveVarExpr(params[2]);
512
+ if (radius === undefined)
513
+ return `Circle radius (${params[2]}) is invalid!`;
514
+ if (WtMath.isZero(radius))
515
+ return `Circle radius (${params[2]}) resolved is zero, which is invalid!`;
516
+ if (radius < 0)
517
+ this.warnings.push(`Circle radius used some negative value('${radius}' calculated from '${params[2]}'), its absolute will be used!`);
518
+ return new Circle(p1.x, p1.y, radius);
519
+ case "ring":
520
+ if (params.length < 4)
521
+ return `Parameters are too less to build a Ring!`;
522
+ p1 = this.resolvePointFromExpr(params[1]);
523
+ let r1 = this.resolveVarExpr(params[2]);
524
+ let r2 = this.resolveVarExpr(params[3]);
525
+ if (!p1)
526
+ return `Center (${params[1]}) is invalid to build a Ring!`;
527
+ if (!r1 || !r2)
528
+ return `Inner/Outer radius for a ring must both be set (and positive)!`;
529
+ return new Ring(p1.x, p1.y, r1, r2);
530
+ case "rect":
531
+ case "rectangle":
532
+ if (params.length < 3)
533
+ return `Parameters are too less to build a Rectangle!`;
534
+ // Format of Rectangle:
535
+ // - rect; bycenter; (x, y); w; h
536
+ // - rect; (x1, y1); (x2, y2)
537
+ // - rect; (x1, y1); w; h
538
+ if (params[1].toLowerCase() === "bycenter" && params.length >= 5) {
539
+ const pCenter = this.resolvePointFromExpr(params[2]);
540
+ if (!pCenter)
541
+ return `Center definition (${params[2]}) is invalid.`;
542
+ const w = this.resolveVarExpr(params[3]);
543
+ if (!w || w <= 0)
544
+ return `Width must be greater than Zero, it is now '${w}' (resolved from '${params[3]}')`;
545
+ const h = this.resolveVarExpr(params[4]);
546
+ if (!h || h <= 0)
547
+ return `Height must be greater than Zero, it is now '${h}' (resolved from '${params[4]}')`;
548
+ x1 = pCenter.x - w / 2;
549
+ y1 = pCenter.y - h / 2;
550
+ width = w;
551
+ height = h;
552
+ }
553
+ else {
554
+ p1 = this.resolvePointFromExpr(params[1]);
555
+ if (!p1)
556
+ return `The start point (${params[1]}) of the rectangle is invalid`;
557
+ p2 = this.resolvePointFromExpr(params[2]);
558
+ const w = this.resolveVarExpr(params[2]);
559
+ if (p2) {
560
+ width = Math.abs(p1.x - p2.x);
561
+ height = Math.abs(p1.y - p2.y);
562
+ if (WtMath.isZero(width))
563
+ return "Rectangle Width is zero!";
564
+ if (WtMath.isZero(height))
565
+ return "Rectangle Height is zero!";
566
+ x1 = Math.min(p1.x, p2.x);
567
+ y1 = Math.min(p1.y, p2.y);
568
+ }
569
+ else {
570
+ if (!w || w <= 0)
571
+ return `Width must be greater than Zero, it is now '${w}' (resolved from '${params[3]}')`;
572
+ width = w;
573
+ if (params.length >= 4) {
574
+ const h = this.resolveVarExpr(params[3]);
575
+ if (!h || h <= 0)
576
+ return `Height must be greater than Zero, it is now '${h}' (resolved from '${params[4]}')`;
577
+ height = h;
578
+ }
579
+ else {
580
+ height = w; // If Height is not given, then it is a square, height = width
581
+ return `Rectangle Height might be undefined!`;
582
+ }
583
+ x1 = p1.x;
584
+ y1 = p1.y;
585
+ }
586
+ }
587
+ return new Rect(x1, y1, width, height);
588
+ case "tri":
589
+ case "triangle":
590
+ // Format of Triangle: triangle; (x1, y1); (x2, y2); (x3, y3)
591
+ if (params.length < 4)
592
+ return `Parameters to build a triangle are not sufficient!`;
593
+ p1 = this.resolvePointFromExpr(params[1]);
594
+ p2 = this.resolvePointFromExpr(params[2]);
595
+ p3 = this.resolvePointFromExpr(params[3]);
596
+ if (!p1)
597
+ return `The first point (${params[1]}) is invalid!`;
598
+ if (!p2)
599
+ return `The second point (${params[2]}) is invalid!`;
600
+ if (!p3)
601
+ return `The third point (${params[3]}) is invalid!`;
602
+ return new Triangle(p1, p2, p3);
603
+ case "plg":
604
+ case "polygon":
605
+ // Check if the first param is point series
606
+ ps = this.pointSeries.find(x => x.name === params[1]);
607
+ if (ps) {
608
+ if (ps.points.length < 2)
609
+ return `Point Series [${params[1]}] has only one point!`;
610
+ else
611
+ pts = ps.points;
612
+ }
613
+ else {
614
+ // Format of the polygons: plg; (x1, y1); (x2, y2); (x3, y3); ...;(xn, yn)
615
+ if (params.length < 4)
616
+ return `Parameters for the Polygon is insufficient!`;
617
+ pts = params.filter((p, i) => i > 0)
618
+ .map(p => {
619
+ const tryPoint = this.resolvePointFromExpr(p);
620
+ if (!tryPoint)
621
+ this.warnings.push(`When building a Polygon, '${p}' resolved to no Point!`);
622
+ return tryPoint;
623
+ })
624
+ .filter(p => !!p);
625
+ }
626
+ if (pts.length < 3)
627
+ return `The valid points are less than 3, no polygon is defined!`;
628
+ return new Polygon(...pts);
629
+ case "plgn":
630
+ case "npolygon":
631
+ // Format of the regular polygon N: plgn; N; (cx, cy); radius; start_deg;
632
+ if (params.length < 5)
633
+ return `Parameters for Regular N-Polygon is insufficient`;
634
+ const N = parseInt(params[1]);
635
+ if (!N || N < 0)
636
+ return `Please give a integer for N, [${params[1]}] is invalid!`;
637
+ p1 = this.resolvePointFromExpr(params[2]);
638
+ if (!p1)
639
+ return `Center for the Regular N-Polygon is invalid!`;
640
+ rx = this.resolveVarExpr(params[3]);
641
+ if (rx === undefined)
642
+ return `Radius for the regular N-Polygon is invalid!`;
643
+ deg = this.resolveVarExpr(params[4]) ?? 0;
644
+ return Polygon.buildRegularPolygon(N, p1, rx, deg);
645
+ case "ell":
646
+ case "ellipse":
647
+ // Format of the Ellipse; ellipse; (x, y); rx; ry; deg
648
+ if (params.length < 4)
649
+ return `Parameters for the Ellipse is insufficient!`;
650
+ p1 = this.resolvePointFromExpr(params[1]);
651
+ rx = this.resolveVarExpr(params[2]);
652
+ ry = this.resolveVarExpr(params[3]);
653
+ deg = params.length > 4 ? this.resolveVarExpr(params[4]) : 0;
654
+ if (!p1)
655
+ return `Ellipse Center (${params[1]}) is invalid!`;
656
+ if (!rx || rx <= 0)
657
+ return `Ellipse RX (${params[2]}, resolved as '${rx}') is invalid!`;
658
+ if (!ry || ry <= 0)
659
+ return `Ellipse RY (${params[3]}, resolved as '${ry}') is invalid!`;
660
+ if (deg === undefined)
661
+ return `Ellipse Rotating Degree (${params[4]}) cannot be resolved!`;
662
+ return new Ellipse(p1.x, p1.y, rx, ry, AngDeg.fitDeg(deg));
663
+ case "b2":
664
+ case "bezier2":
665
+ // Format of Bezier2: bezier2; (xStart, yStart); (xCtrl, yCtrl); (xEnd, yEnd)
666
+ if (params.length < 4)
667
+ return `Parameters to build Bezier2 are not sufficient!`;
668
+ p1 = this.resolvePointFromExpr(params[1]);
669
+ p2 = this.resolvePointFromExpr(params[2]);
670
+ p3 = this.resolvePointFromExpr(params[3]);
671
+ if (!p1)
672
+ return `The Start point (${params[1]}) is invalid!`;
673
+ if (!p2)
674
+ return `The Control point (${params[2]}) is invalid!`;
675
+ if (!p3)
676
+ return `The End point (${params[3]}) is invalid!`;
677
+ return new Bezier2(p1, p2, p3);
678
+ case "b3":
679
+ case "bezier3":
680
+ // Format of Bezier3: bezier3; (xStart, yStart); (xCtrl1, yCtrl1); (xCtrl2, yCtrl2); (xEnd, yEnd)
681
+ if (params.length < 5)
682
+ return `Parameters to build Bezier3 are not sufficient!`;
683
+ p1 = this.resolvePointFromExpr(params[1]);
684
+ p2 = this.resolvePointFromExpr(params[2]);
685
+ p3 = this.resolvePointFromExpr(params[3]);
686
+ p4 = this.resolvePointFromExpr(params[4]);
687
+ if (!p1)
688
+ return `The Start point (${params[1]}) is invalid!`;
689
+ if (!p2)
690
+ return `The Control Point 1 (${params[2]}) is invalid!`;
691
+ if (!p3)
692
+ return `The Control Point 2 (${params[3]}) is invalid!`;
693
+ if (!p4)
694
+ return `The End point (${params[4]}) is invalid!`;
695
+ return new Bezier3(p1, p2, p3, p4);
696
+ case "arc":
697
+ // Possible formats:
698
+ // - arc; circ1; start_deg; end_deg; ccw | cw
699
+ // - arc; ell1; start_deg; end_deg; ccw | cw
700
+ // - arc; pStart; rx; ry; rot_deg; ccw | cw; small | large; pEnd (the cw-mark or largeArc-mark could be interchanged in position)
701
+ // - arc; pCenter; rx; ry; rot_deg; start_deg, end_deg, ccw | cw;
702
+ if (params.length < 5)
703
+ return `Parameters to build an Arc are not sufficient!`;
704
+ // Make a function to build arc from Circle/Ellipse
705
+ const buildArcFromEllipse = (model, paramDegStart, paramDegEnd, markCW, ellName) => {
706
+ const degStart = this.resolveVarExpr(paramDegStart);
707
+ if (degStart === undefined)
708
+ return `Start Degree (${paramDegStart}) is invalid!`;
709
+ const degEnd = this.resolveVarExpr(paramDegEnd);
710
+ if (degEnd === undefined)
711
+ return `End Degree (${paramDegEnd}) is invalid!`;
712
+ const dirMark = markCW.toLowerCase();
713
+ if (dirMark !== "cw" && dirMark !== "ccw")
714
+ return `The Direction must be specified, either cw or ccw!`;
715
+ const isCW = dirMark === "cw";
716
+ if (model instanceof Circle) {
717
+ return EllipticalArc.fromCircle(model, degStart, degEnd, isCW);
718
+ }
719
+ if (model instanceof Ellipse) {
720
+ return EllipticalArc.fromEllipse(model, degStart, degEnd, isCW);
721
+ }
722
+ return `The Shape (${ellName}) is not a Circle/Ellipse!`;
723
+ };
724
+ // Check if Arc derived from some Circle/Ellipse
725
+ const shape = this.paths.find(s => s.name === params[1]);
726
+ if (shape) {
727
+ if (shape.model &&
728
+ ((shape.model instanceof Circle) || (shape.model instanceof Ellipse))) {
729
+ return buildArcFromEllipse(shape.model, params[2], params[3], params[4], params[1]);
730
+ }
731
+ else {
732
+ return `Shape [${params[1]}] is not a Circle or an Ellipse!`;
733
+ }
734
+ }
735
+ if (params.length < 8)
736
+ return `Parameters are insufficient to build an Arc!`;
737
+ // Arc built in parametric way
738
+ p1 = this.resolvePointFromExpr(params[1]);
739
+ rx = this.resolveVarExpr(params[2]);
740
+ ry = this.resolveVarExpr(params[3]);
741
+ deg = this.resolveVarExpr(params[4]);
742
+ if (!p1)
743
+ return `The Ellipse/Circle Center or Arc Start point must be valid!`;
744
+ if (!rx || rx <= 0)
745
+ return `RX (${params[2]}) is invalid!`;
746
+ if (!ry || ry <= 0)
747
+ return `RY (${params[3]}) is invalid!`;
748
+ if (deg === undefined)
749
+ return `Ellipse/Circle Rotating Degree (${params[3]}) is invalid!`;
750
+ let cwMark = undefined;
751
+ let largeArcMark = undefined;
752
+ const mark1 = params[5].toLowerCase();
753
+ const mark2 = params[6].toLowerCase();
754
+ if (mark1 === "cw" || mark1 === "ccw")
755
+ cwMark = mark1 === "cw";
756
+ if (mark2 === "cw" || mark2 === "ccw")
757
+ cwMark = mark2 === "cw";
758
+ if (mark1 === "large" || mark1 === "small")
759
+ largeArcMark = mark1 === "large";
760
+ if (mark2 === "large" || mark2 === "small")
761
+ largeArcMark = mark2 === "large";
762
+ if (cwMark !== undefined && largeArcMark !== undefined) { // This the parametric design from start/end points
763
+ p2 = this.resolvePointFromExpr(params[7]);
764
+ if (!p2)
765
+ return `The Arc End point (${params[7]}) is invalid!`;
766
+ if (Point.areIdentical(p1, p2, 4e-3))
767
+ return `Arc Point Start (${params[1]}) and End(${params[7]}) are identical, thus cannot make any arc!`;
768
+ const arc = EllipticalArc.fromStartEndPoints(p1, p2, rx, ry, largeArcMark, cwMark, deg);
769
+ if (!arc.auxEllipse)
770
+ this.warnings.push(`Arc definition '${def}' resolved to no valid Ellipse, thus no arc is resolved!`);
771
+ if (arc.isEnlarged)
772
+ this.warnings.push(`Arc is enlarged to (Rx:${arc.rx.toFixed(3)}, Ry:${arc.ry.toFixed(3)}), because with the given values(Rx:${rx}, Ry:${ry}) no arc could be solved out.`);
773
+ return arc;
774
+ }
775
+ if (cwMark === undefined && largeArcMark === undefined) { // In this case, this two parameters should be start/end degrees
776
+ const ell = new Ellipse(p1.x, p1.y, rx, ry, AngDeg.fitDeg(deg));
777
+ return buildArcFromEllipse(ell, params[5], params[6], params[7], params.filter((p, i) => i > 0 && i < 5).join(", "));
778
+ }
779
+ return `Bad configuration to build an Arc!`;
780
+ case "movex":
781
+ // Format is: moveX; pathDefined; dx
782
+ if (params.length < 3)
783
+ return `Parameters too less to make Path MoveX derivation!`;
784
+ pathReferred = this.paths.find(x => x.name === params[1]);
785
+ if (!pathReferred)
786
+ return `Shape You referred(${params[1]}) doesn't exist!`;
787
+ dx = this.resolveVarExpr(params[2]);
788
+ if (dx === undefined)
789
+ return `DX definition '${params[2]}' cannot resolve to a valid number!`;
790
+ pathDerived = pathReferred.model?.moveBy(dx, 0);
791
+ if (!pathDerived)
792
+ return `'${def}' cannot resolve to valid model/shape/path!`;
793
+ return pathDerived;
794
+ case "movey":
795
+ // Format is: moveY; pathDefined; dy
796
+ if (params.length < 3)
797
+ return `Parameters too less to make Path MoveY derivation!`;
798
+ pathReferred = this.paths.find(x => x.name === params[1]);
799
+ if (!pathReferred)
800
+ return `Shape You referred(${params[1]}) doesn't exist!`;
801
+ dy = this.resolveVarExpr(params[2]);
802
+ if (dy === undefined)
803
+ return `DY definition '${params[2]}' cannot resolve to a valid number!`;
804
+ pathDerived = pathReferred.model?.moveBy(0, dy);
805
+ if (!pathDerived)
806
+ return `'${def}' cannot resolve to valid model/shape/path!`;
807
+ return pathDerived;
808
+ case "move":
809
+ case "moveby":
810
+ // Format is: moveBy; pathDefined; dx,dy
811
+ if (params.length < 3)
812
+ return `Parameters too less to make Path MoveBy derivation!`;
813
+ pathReferred = this.paths.find(x => x.name === params[1]);
814
+ if (!pathReferred)
815
+ return `Shape You referred(${params[1]}) doesn't exist!`;
816
+ pMoveOrAt = this.resolvePointFromExpr(params[2]);
817
+ if (pMoveOrAt === undefined)
818
+ return `MoveBy definition '${params[2]}' cannot resolve to a valid Point/Offset (dx, dy) like!`;
819
+ pathDerived = pathReferred.model?.moveBy(pMoveOrAt.x, pMoveOrAt.y);
820
+ if (!pathDerived)
821
+ return `'${def}' cannot resolve to valid model/shape/path!`;
822
+ return pathDerived;
823
+ case "rotate":
824
+ case "rotateby":
825
+ // Format is: rotateBy; pathDefined; rotDeg; at
826
+ if (params.length < 3)
827
+ return `Parameters too less to make Path Rotate By derivation!`;
828
+ pathReferred = this.paths.find(x => x.name === params[1]);
829
+ if (!pathReferred)
830
+ return `Shape You referred(${params[1]}) doesn't exist!`;
831
+ rotDeg = this.resolveVarExpr(params[2]);
832
+ if (rotDeg === undefined)
833
+ return `Rotation Deg (${params[2]}) cannot resolve to a number to make Path-Rotate-By transform!`;
834
+ pMoveOrAt = params[3] ? this.resolvePointFromExpr(params[3]) : undefined;
835
+ if (pMoveOrAt === undefined)
836
+ this.warnings.push(`When Resolve '${def}': Path Rotate-by will rotate at the center of ORIGIN since no center point specified!`);
837
+ pathDerived = pathReferred.model?.rotateBy(rotDeg, pMoveOrAt);
838
+ if (!pathDerived)
839
+ return `'${def}' cannot resolve to valid model/shape/path!`;
840
+ return pathDerived;
841
+ case "subof":
842
+ // Format is: subof; pathDefined; pointStart; pointEnd
843
+ if (params.length < 4)
844
+ return `Parameters too less to make Sub-Shape/Path!`;
845
+ pathReferred = this.paths.find(x => x.name === params[1]);
846
+ if (!pathReferred)
847
+ return `Shape You referred(${params[1]}) doesn't exist!`;
848
+ if (!pathReferred.path || !PathSegmentedPath.isSegment(pathReferred.path))
849
+ return `Only LineSeg/Bezier2/Bezier3/Arc could make sub-shape, '${params[1]}' is not!`;
850
+ p1 = this.resolvePointFromExpr(params[2]);
851
+ p2 = this.resolvePointFromExpr(params[3]);
852
+ if (!p1)
853
+ return `Start Point (${params[2]}) resolved failed for making Sub-Shape/Path!`;
854
+ if (!p2)
855
+ return `End Point (${params[3]}) resolved failed for making Sub-Shape/Path!`;
856
+ pathDerived = pathReferred.path.makeSubPath(p1, p2);
857
+ if (!pathDerived)
858
+ return `Make sub path/shape of '${params[1]}' from '${params[2]}' to '${params[3]}' failed!`;
859
+ return pathDerived.auxModel;
860
+ default:
861
+ return `Shape type ${params[0]} is invalid!`;
862
+ }
863
+ }
864
+ /**
865
+ * The sub function to resolve the input of duplication-to defines
866
+ * @param def The definition string for the duplication
867
+ * @param withLineJoin A switch to determine whether introducing Line Join features, this is specialized for Segged-Path
868
+ * Because for basic types, there will be no such issue, it will always be smooth and consistent
869
+ * @returns
870
+ */
871
+ tryAsPathDuplication(def, withLineJoin) {
872
+ // Format is: dupTo; pathDefined; in/out/right/left; distance
873
+ const params = def.split(";").map(p => p.trim()).filter(p => !!p);
874
+ // If not dupTo, do nothing
875
+ if (params[0].toLowerCase() !== "dupto")
876
+ return undefined;
877
+ // Continue as dupTo
878
+ if (params.length < 4)
879
+ return `Parameters too less to make Path Rotate By derivation!`;
880
+ let pathReferred = this.paths.find(x => x.name === params[1]);
881
+ if (!pathReferred)
882
+ return `The shape You referred(${params[1]}) doesn't exist (when duplicating a shape)!`;
883
+ let dir = params[2].toLowerCase();
884
+ if (!["in", "out", "right", "left"].includes(dir))
885
+ return `Direction Keyword (${params[2]}) to duplicate a shape is invalid.`;
886
+ let distance = this.resolveVarExpr(params[3]);
887
+ if (!distance)
888
+ return `Distance '${params[3]}' is invalid to duplicate a shape, it should be greater than zero!`;
889
+ let lineJoin = undefined;
890
+ if (withLineJoin) {
891
+ lineJoin = params[4];
892
+ if (lineJoin)
893
+ lineJoin = lineJoin.toLowerCase();
894
+ lineJoin = SvgCsts.LINE_JOINS.includes(lineJoin) ? lineJoin : undefined;
895
+ }
896
+ let pathDuped = pathReferred.path?.duplicateTo(distance, dir, lineJoin);
897
+ if (!pathDuped)
898
+ return `Failed to duplicate '${pathReferred.name}' with parameters '${dir}' and '${params[3]}'`;
899
+ return pathDuped;
900
+ }
901
+ /**
902
+ * Resolve the shape definitions
903
+ * @param name Name of the shape
904
+ * @param value Presentation to define the shape
905
+ * @returns Error message
906
+ */
907
+ resolveShape(name, value) {
908
+ // Check Names
909
+ if (this.paths.find(s => s.name === name))
910
+ return [`There has already been a shape named as ${name}!`];
911
+ if (this.pathSeries.find(s => s.name === name))
912
+ return [`There is some Shape Series which is named as ${name}!`];
913
+ // Try to see if it is duplication of existing shapes
914
+ const dupTo = this.tryAsPathDuplication(value);
915
+ if (typeof dupTo === "string")
916
+ return [dupTo];
917
+ if (dupTo) {
918
+ const duppedPath = new HuatuPath(dupTo, name);
919
+ // If the duplicated path is singled modelled, then assign the model to path
920
+ if (dupTo.models.length === 1) {
921
+ duppedPath.model = dupTo.models[0];
922
+ }
923
+ this.paths.push(duppedPath);
924
+ this.defines.push(duppedPath);
925
+ return [];
926
+ }
927
+ const model = this.resolveShapeDef(value);
928
+ if (typeof model === "string")
929
+ return [model];
930
+ const path = HuatuPath.fromModel(model, name);
931
+ this.paths.push(path);
932
+ // Add to Definable
933
+ this.defines.push(path);
934
+ return [];
935
+ }
936
+ tryPointOrPointSeries(def) {
937
+ if (!def)
938
+ return undefined;
939
+ const ps = this.pointSeries.find(s => s.name === def);
940
+ if (ps)
941
+ return ps.points;
942
+ const p = this.resolvePointFromExpr(def);
943
+ if (p)
944
+ return [p];
945
+ return undefined;
946
+ }
947
+ tryNumberSeries(def) {
948
+ if (!def)
949
+ return [];
950
+ const range = this.resolveRange(def);
951
+ if (range)
952
+ return range.numbers;
953
+ else
954
+ return this.resolveSeparatedNumbers(def);
955
+ }
956
+ resolveShapesFromDef(def) {
957
+ const params = def.split(";").map(p => p.trim()).filter(p => !!p);
958
+ if (params.length < 3)
959
+ return `Parameters are too less to build ShapeSeries!`;
960
+ if (this.pathSeries.find(x => x.name === params[0]))
961
+ return `'${params[0]}' is a ShapeSeries, ShapeSeries can only be derived from simple models!`;
962
+ const shapeType = params[0].toLowerCase();
963
+ function extendArray(arr, len) {
964
+ if (Array.isArray(arr)) {
965
+ if (arr.length >= len) {
966
+ return arr.filter((x, i) => i < len);
967
+ }
968
+ else {
969
+ for (let i = arr.length; i < len; i++) {
970
+ arr.push(arr[0]); // Always add with the first element;
971
+ }
972
+ return arr;
973
+ }
974
+ }
975
+ else {
976
+ return [...Array(len).keys()].map(x => arr);
977
+ }
978
+ }
979
+ function alignParams(...pms) {
980
+ // 1. pick out the undefined the undefined elements in the array and return undefined if array is empty
981
+ let array = pms
982
+ .map(x => {
983
+ if (Array.isArray(x)) {
984
+ const arrDefined = x.filter(y => y !== undefined);
985
+ if (arrDefined.length > 0)
986
+ return arrDefined;
987
+ else
988
+ return undefined;
989
+ }
990
+ else {
991
+ return x;
992
+ }
993
+ });
994
+ // 2. If some of the elements is undefined, then no necessity to continue
995
+ if (array.filter(x => x === undefined).length > 0)
996
+ return undefined;
997
+ const arrayDefined = array;
998
+ let lens = arrayDefined.map(x => Array.isArray(x) ? x.length : 1).filter(x => x > 1);
999
+ let minArrayLen = lens.length > 0 ? Math.min(...lens) : 1;
1000
+ return {
1001
+ arr: arrayDefined.map(x => extendArray(x, minArrayLen)),
1002
+ len: minArrayLen
1003
+ };
1004
+ }
1005
+ const getLineSegMethod = (method) => {
1006
+ if (!method || method === "zip" || method === "pair")
1007
+ return "zip";
1008
+ if (method === "interleave" || method === "cross")
1009
+ return "cross";
1010
+ return undefined;
1011
+ };
1012
+ switch (shapeType) {
1013
+ case "ln": // Line, LineSeg
1014
+ case "line": // Line, LineSeg
1015
+ case "ls":
1016
+ case "lineseg": // Try first param, to see it if is defined Line
1017
+ // Possible Formats of the define string, this can only build LineSegs
1018
+ // - ln/line; lineName; l1(1 to N); l2(1 to N)
1019
+ const line = this.paths.find(s => s.name === params[1]);
1020
+ if (line) { // The def is like: line; ln1; l1; l2;
1021
+ if (!(line.model instanceof Line))
1022
+ return `"${params[1]}" is not a Line!`;
1023
+ // Guess param2/param3 as range or var number
1024
+ let l1s = this.tryNumberSeries(params[2]);
1025
+ if (l1s.length < 1) {
1026
+ this.warnings.push(`No shape resolved from '${def}'! Length1 is empty!`);
1027
+ }
1028
+ let l2s = this.tryNumberSeries(params[3]);
1029
+ const lens = alignParams(l1s, l2s);
1030
+ if (lens) {
1031
+ return lens.arr[0].map((x, i) => line.model.buildSeg(x, lens.arr[1][i]));
1032
+ }
1033
+ else {
1034
+ return l1s.map(x => line.model.buildSeg(x));
1035
+ }
1036
+ }
1037
+ // Now we guess the first param is a point series or a single point
1038
+ // It might be defining Lines first. Possible formats:
1039
+ // - line; pointSeries (N points); deg (1 or N) ; l1, l2
1040
+ // - line; point(1 points); deg (1 or N) ; l1, l2
1041
+ // - line/ls/lineseg; point(1 points); deg (1 or N)
1042
+ // point and deg will compose line(s), witch each pair (l1, l2) to build multiple LineSegs
1043
+ // If no (l1, l2) present, then return lines
1044
+ const anchors = this.tryPointOrPointSeries(params[1]);
1045
+ if (anchors) {
1046
+ // Check if still point(s) followed, then it is defining line segs
1047
+ const lsp2 = this.tryPointOrPointSeries(params[2]);
1048
+ if (lsp2) {
1049
+ const method = getLineSegMethod(params[3]);
1050
+ let segs;
1051
+ if (method === "cross") {
1052
+ segs = anchors.map(x => lsp2.map(y => new LineSeg(x, y))).flat();
1053
+ }
1054
+ else {
1055
+ const lsAligned = alignParams(anchors, lsp2);
1056
+ segs = lsAligned
1057
+ ? [...Array(lsAligned.len).keys()]
1058
+ .map(i => new LineSeg(lsAligned.arr[0][i], lsAligned.arr[1][i]))
1059
+ : [];
1060
+ }
1061
+ if (segs.length < 1)
1062
+ return `No shaped resolved from '${def}'!`;
1063
+ this.infos.push(`${segs.length} lines resolved from '${params.filter((x, i) => i > 0).join("; ")}'`);
1064
+ return segs;
1065
+ }
1066
+ // Check if Line definition(s) followed
1067
+ const degs = this.tryNumberSeries(params[2]);
1068
+ const lnDegAligned = alignParams(anchors, degs);
1069
+ const lines = lnDegAligned?.arr[0]
1070
+ .map((a, i) => new Line(a, AngDeg.fitDeg(lnDegAligned?.arr[1][i])));
1071
+ if (!lines || lines.length < 1)
1072
+ return `No Shape resolved from '${def}'!`;
1073
+ const l1s = this.tryNumberSeries(params[3]);
1074
+ const l2s = this.tryNumberSeries(params[4]);
1075
+ if (l1s.length < 1) { // l1 is empty
1076
+ if (shapeType === "ls" || shapeType === "lineseg") {
1077
+ return `By define LineSeg Series, you need to specify lengths after a line!`;
1078
+ }
1079
+ return `You cannot define Line Series(from '${def}'), which is infinite in length thus cannot be drawn, Please define as LineSeg Series instead!`;
1080
+ }
1081
+ const linesAligned = l2s.length > 0
1082
+ ? alignParams(lines, l1s, l2s)
1083
+ : alignParams(lines, l1s);
1084
+ if (linesAligned) {
1085
+ const segs = linesAligned.arr[0]
1086
+ .map((l, i) => l.buildSeg(linesAligned.arr[1][i], l2s.length > 0 ? linesAligned.arr[2][i] : undefined));
1087
+ if (segs.length > 0) {
1088
+ this.infos.push(`${segs.length} lines resolved from '${params.filter((x, i) => i > 0).join("; ")}'`);
1089
+ return segs;
1090
+ }
1091
+ }
1092
+ else {
1093
+ return `No Shape resolved from '${def}'!`;
1094
+ }
1095
+ }
1096
+ else {
1097
+ return `To define Line(Seg)s, first parameter should be point(s)`;
1098
+ }
1099
+ case "circ":
1100
+ case "circle":
1101
+ // Format of a circle define: circle; (x, y); r
1102
+ const cs = this.tryPointOrPointSeries(params[1]);
1103
+ if (!cs)
1104
+ return `'${params[1]}' cannot resolve to be some point (series)!`;
1105
+ let rs = this.tryNumberSeries(params[2]);
1106
+ if (rs.length < 1)
1107
+ return `'${params[2]}' cannot resolve to be some radius!`;
1108
+ const crs = alignParams(cs, rs);
1109
+ if (!crs)
1110
+ return `Parameters '${params[1]}; ${params[2]}' invalid to resolve Circle Series!`;
1111
+ return [...Array(crs.len).keys()]
1112
+ .map(i => new Circle(crs.arr[0][i].x, crs.arr[0][i].y, crs.arr[1][i]));
1113
+ case "ell":
1114
+ case "ellipse":
1115
+ // Format of the Ellipse; ellipse; (x, y); rx; ry; deg
1116
+ if (params.length < 4)
1117
+ return `Parameters for the Ellipse is insufficient!`;
1118
+ const ecs = this.tryPointOrPointSeries(params[1]);
1119
+ if (!ecs)
1120
+ `Center Definition '${params[1]}' for Ellipse Series is incorrect!`;
1121
+ let rxs = this.tryNumberSeries(params[2]);
1122
+ if (rxs.length < 1)
1123
+ `RX definition '${params[2]}' resolved to no values!`;
1124
+ let rys = this.tryNumberSeries(params[3]);
1125
+ if (rys.length < 1)
1126
+ `RY definition '${params[3]}' resolved to no values!`;
1127
+ let degs = params[4] ? this.tryNumberSeries(params[4]) : 0;
1128
+ if (Array.isArray(degs) && degs.length < 1)
1129
+ return `Degree Definition '${params[4]}' to build Ellipse Series is incorrect!`;
1130
+ const ellParams = alignParams(ecs, rxs, rys, degs);
1131
+ if (!ellParams)
1132
+ return `Definition '${def}' resolved to no Ellipse Series!`;
1133
+ return [...Array(ellParams.len).keys()]
1134
+ .map(i => new Ellipse(ellParams.arr[0][i].x, ellParams.arr[0][i].y, ellParams.arr[1][i], ellParams.arr[2][i], AngDeg.fitDeg(ellParams.arr[3][i])));
1135
+ case "rect":
1136
+ case "rectangle":
1137
+ // Format of Rectangle:
1138
+ // - rect; bycenter; (x, y); w; h
1139
+ // - rect; (x1, y1); (x2, y2)
1140
+ // - rect; (x1, y1); w; h
1141
+ if (params[1].toLowerCase() === "bycenter" && params.length >= 5) {
1142
+ const rcs = this.tryPointOrPointSeries(params[2]);
1143
+ if (!rcs || rcs.length < 1)
1144
+ return `No Centers resolved for rects!`;
1145
+ const ws = this.tryNumberSeries(params[3]);
1146
+ if (ws.length < 1)
1147
+ return `No width resolved from '${params[3]}'!`;
1148
+ const hs = this.tryNumberSeries(params[4]);
1149
+ if (hs.length < 1)
1150
+ return `No Height resolved from '${params[4]}'!`;
1151
+ const rectParams = alignParams(rcs, ws, hs);
1152
+ if (rectParams) {
1153
+ return [...Array(rectParams.len).keys()]
1154
+ .map(i => Rect.fromCenterWH(rectParams.arr[0][i], rectParams.arr[1][i], rectParams.arr[2][i]));
1155
+ }
1156
+ }
1157
+ else {
1158
+ const p1s = this.tryPointOrPointSeries(params[1]);
1159
+ if (!p1s || p1s.length < 1)
1160
+ return `No Point resolved from '${params[1]}'`;
1161
+ const p2s = this.tryPointOrPointSeries(params[2]);
1162
+ if (p2s) { // It is made by two points
1163
+ const rectParams = alignParams(p1s, p2s);
1164
+ if (rectParams) {
1165
+ return [...Array(rectParams.len).keys()]
1166
+ .map(i => Rect.fromTwoPoints(rectParams.arr[0][i], rectParams.arr[1][i]))
1167
+ .filter(x => {
1168
+ if (x)
1169
+ return true;
1170
+ else {
1171
+ this.warnings.push(`Zero-Width/Height Rectangle resolved and discarded!`);
1172
+ return false;
1173
+ }
1174
+ });
1175
+ }
1176
+ }
1177
+ // Now guess it is made by anchor/w/h
1178
+ const ws = this.tryNumberSeries(params[2]);
1179
+ if (ws.length < 1)
1180
+ return `No width resolved from '${params[2]}'!`;
1181
+ const hs = this.tryNumberSeries(params[3]);
1182
+ if (hs.length < 1)
1183
+ return `No Height resolved from '${params[3]}'!`;
1184
+ const rectParams = alignParams(p1s, ws, hs);
1185
+ if (rectParams) {
1186
+ return [...Array(rectParams.len).keys()]
1187
+ .map(i => {
1188
+ const pTL = rectParams.arr[0][i];
1189
+ const w = rectParams.arr[1][i];
1190
+ const h = rectParams.arr[2][i];
1191
+ return new Rect(pTL.x, pTL.y, w, h);
1192
+ });
1193
+ }
1194
+ }
1195
+ // Default return error message
1196
+ return `No Rectangle resolved from '${def}'!`;
1197
+ case "pln":
1198
+ case "pline":
1199
+ case "polyline":
1200
+ const defPoints = params.filter((p, i) => i > 0)
1201
+ .map(p => this.tryPointOrPointSeries(p))
1202
+ .filter((p, i) => {
1203
+ if (p)
1204
+ return true;
1205
+ else {
1206
+ this.warnings.push(`${params[i + 1]} resolved to no point(s)!`);
1207
+ return false;
1208
+ }
1209
+ });
1210
+ if (defPoints.length < 2)
1211
+ return `Insufficient point(group/series)s to build polyline!`;
1212
+ const plnAligned = alignParams(...defPoints);
1213
+ if (plnAligned) {
1214
+ return [...Array(plnAligned.len).keys()]
1215
+ .map(i => new PolyLine(...plnAligned.arr.map(p => p[i])));
1216
+ }
1217
+ return `No Polyline resolved from given string!`;
1218
+ case "tri":
1219
+ case "triangle":
1220
+ const pA = this.tryPointOrPointSeries(params[1]);
1221
+ const pB = this.tryPointOrPointSeries(params[2]);
1222
+ const pC = this.tryPointOrPointSeries(params[3]);
1223
+ if (!pA)
1224
+ return `Point A definition '${params[1]}' is invalid!`;
1225
+ if (!pB)
1226
+ return `Point A definition '${params[2]}' is invalid!`;
1227
+ if (!pC)
1228
+ return `Point A definition '${params[3]}' is invalid!`;
1229
+ const triAligned = alignParams(pA, pB, pC);
1230
+ if (triAligned) {
1231
+ const tris = [...Array(triAligned.len).keys()]
1232
+ .map(i => new Triangle(triAligned.arr[0][i], triAligned.arr[1][i], triAligned.arr[2][i]))
1233
+ .filter(t => t.isValid);
1234
+ if (tris.length > 0)
1235
+ return tris;
1236
+ }
1237
+ return `No triangles resolved from '${def}'!`;
1238
+ case "plg":
1239
+ case "polygon":
1240
+ const plgPoints = params.filter((p, i) => i > 0)
1241
+ .map(p => this.tryPointOrPointSeries(p))
1242
+ .filter((p, i) => {
1243
+ if (p)
1244
+ return true;
1245
+ {
1246
+ this.warnings.push(`${params[i + 1]} resolved to no point(s)!`);
1247
+ return false;
1248
+ }
1249
+ });
1250
+ if (plgPoints.length < 2)
1251
+ return `Insufficient point(group/series)s to build polygon!`;
1252
+ const plgAligned = alignParams(...plgPoints);
1253
+ if (plgAligned) {
1254
+ return [...Array(plgAligned.len).keys()]
1255
+ .map(i => new Polygon(...plgAligned.arr.map(p => p[i])));
1256
+ }
1257
+ return `No Polygon resolved from given string!`;
1258
+ case "plgn":
1259
+ case "npolygon":
1260
+ const Ns = this.tryNumberSeries(params[1])
1261
+ .filter(x => Number.isInteger(x) && x >= 3);
1262
+ if (Ns.length < 1)
1263
+ return `Please give a integer for N (N >= 3), [${params[1]}] is invalid!`;
1264
+ const plgnCs = this.tryPointOrPointSeries(params[2]);
1265
+ if (!plgnCs)
1266
+ return `The center(s) specified for Regular Polygon '${params[2]}' is invalid!`;
1267
+ const plgnRs = this.tryNumberSeries(params[3]);
1268
+ if (plgnRs.length < 1)
1269
+ return `Please specify Radius for Regular Polygon design!`;
1270
+ let plgnDegs;
1271
+ if (params[4]) {
1272
+ plgnDegs = this.tryNumberSeries(params[4]);
1273
+ if (plgnDegs.length < 1)
1274
+ return `Start Degrees '${params[4]}' is invalid!`;
1275
+ }
1276
+ else {
1277
+ plgnDegs = [0];
1278
+ }
1279
+ const plgnAligned = alignParams(Ns, plgnCs, plgnRs, plgnDegs);
1280
+ if (plgnAligned) {
1281
+ return [...Array(plgnAligned.len).keys()]
1282
+ .map(i => Polygon.buildRegularPolygon(plgnAligned.arr[0][i], plgnAligned.arr[1][i], plgnAligned.arr[2][i], plgnAligned.arr[3][i]));
1283
+ }
1284
+ return `No Regular Polygon resolved from '${def}'!`;
1285
+ case "b2":
1286
+ case "bezier2":
1287
+ // Format of Bezier2: bezier2; (xStart, yStart); (xCtrl, yCtrl); (xEnd, yEnd)
1288
+ if (params.length < 4)
1289
+ return `Parameters to build Bezier2 are not sufficient!`;
1290
+ const b2p1 = this.tryPointOrPointSeries(params[1]);
1291
+ const b2p2 = this.tryPointOrPointSeries(params[2]);
1292
+ const b2p3 = this.tryPointOrPointSeries(params[3]);
1293
+ if (!b2p1)
1294
+ return `The Start point (${params[1]}) is invalid!`;
1295
+ if (!b2p2)
1296
+ return `The Control point (${params[2]}) is invalid!`;
1297
+ if (!b2p3)
1298
+ return `The End point (${params[3]}) is invalid!`;
1299
+ const b2Aligned = alignParams(b2p1, b2p2, b2p3);
1300
+ if (b2Aligned) {
1301
+ return [...Array(b2Aligned.len).keys()]
1302
+ .map(i => new Bezier2(b2Aligned.arr[0][i], b2Aligned.arr[1][i], b2Aligned.arr[2][i]));
1303
+ }
1304
+ return `No Quadratic Bezier Curve resolved from '${def}'!`;
1305
+ case "b3":
1306
+ case "bezier3":
1307
+ // Format of Bezier3: bezier3; (xStart, yStart); (xCtrl1, yCtrl1); (xCtrl2, yCtrl2); (xEnd, yEnd)
1308
+ if (params.length < 5)
1309
+ return `Parameters to build Bezier2 are not sufficient!`;
1310
+ const b3p1 = this.tryPointOrPointSeries(params[1]);
1311
+ const b3p2 = this.tryPointOrPointSeries(params[2]);
1312
+ const b3p3 = this.tryPointOrPointSeries(params[3]);
1313
+ const b3p4 = this.tryPointOrPointSeries(params[4]);
1314
+ if (!b3p1)
1315
+ return `The Start point (${params[1]}) is invalid!`;
1316
+ if (!b3p2)
1317
+ return `The Control point 1 (${params[2]}) is invalid!`;
1318
+ if (!b3p3)
1319
+ return `The Control point 2 (${params[3]}) is invalid!`;
1320
+ if (!b3p4)
1321
+ return `The End point (${params[4]}) is invalid!`;
1322
+ const b3Aligned = alignParams(b3p1, b3p2, b3p3, b3p4);
1323
+ if (b3Aligned) {
1324
+ return [...Array(b3Aligned.len).keys()]
1325
+ .map(i => new Bezier3(b3Aligned.arr[0][i], b3Aligned.arr[1][i], b3Aligned.arr[2][i], b3Aligned.arr[3][i]));
1326
+ }
1327
+ return `No Cubic Bezier Curve resolved from '${def}'!`;
1328
+ case "arc":
1329
+ // Possible formats:
1330
+ // - arc; circ1; start_deg; end_deg; ccw | cw
1331
+ // - arc; ell1; start_deg; end_deg; ccw | cw
1332
+ if (params.length < 5)
1333
+ return `Parameters to build an Arc are not sufficient!`;
1334
+ const dirMark = params[4].toLowerCase();
1335
+ if (dirMark !== "cw" && dirMark !== "ccw")
1336
+ return `CW/CCW mark not specified!`;
1337
+ let arcModels;
1338
+ let ells = this.pathSeries.find(p => p.name === params[1]);
1339
+ if (ells) {
1340
+ if (ells.models.length > 0 &&
1341
+ (ells.models[0] instanceof Circle || ells.models[0] instanceof Ellipse)) {
1342
+ arcModels = ells.models;
1343
+ }
1344
+ }
1345
+ else {
1346
+ let ell = this.paths.find(p => p.name === params[1]);
1347
+ if (ell && ell.model &&
1348
+ (ell.model instanceof Circle || ell.model instanceof Ellipse)) {
1349
+ arcModels = [ell.model];
1350
+ }
1351
+ }
1352
+ if (!ells)
1353
+ return `No Circle/Ellipse (series) by '${params[1]}'`;
1354
+ const startDegs = this.tryNumberSeries(params[2]);
1355
+ const endDegs = this.tryNumberSeries(params[3]);
1356
+ if (startDegs.length < 1)
1357
+ return `Start Degs (${params[2]}) for Arcs are invalid!`;
1358
+ if (endDegs.length < 1)
1359
+ return `End Degs (${params[3]}) for Arcs are invalid!`;
1360
+ const arcAligned = alignParams(arcModels, startDegs, endDegs);
1361
+ if (arcAligned) {
1362
+ return [...Array(arcAligned.len).keys()]
1363
+ .map(i => {
1364
+ const model = arcAligned.arr[0][i];
1365
+ if (model instanceof Circle) {
1366
+ return EllipticalArc.fromCircle(model, arcAligned.arr[1][i], arcAligned.arr[2][i], dirMark === "cw");
1367
+ }
1368
+ else if (model instanceof Ellipse) {
1369
+ return EllipticalArc.fromEllipse(model, arcAligned.arr[1][i], arcAligned.arr[2][i], dirMark === "cw");
1370
+ }
1371
+ else
1372
+ return undefined;
1373
+ })
1374
+ .filter(x => !!x);
1375
+ }
1376
+ return `No Arc(s) resolved from '${def}'!`;
1377
+ default:
1378
+ return `Shape type ${params[0]} is invalid!`;
1379
+ }
1380
+ }
1381
+ /**
1382
+ * Resolve the Shape Series defined
1383
+ * @param name Name of the shape series
1384
+ * @param value The definition of the series
1385
+ * @returns Error message if any
1386
+ */
1387
+ resolveShapeSeries(name, value) {
1388
+ // Check Names
1389
+ if (this.paths.find(s => s.name === name))
1390
+ return [`There is some shape named as ${name}!`];
1391
+ if (this.pathSeries.find(s => s.name === name))
1392
+ return [`There is already some Shape Series named as ${name}!`];
1393
+ const models = this.resolveShapesFromDef(value);
1394
+ if (typeof models === "string")
1395
+ return [models];
1396
+ if (models.length < 1)
1397
+ return [`No shape resolved from '${value}' for '${name}'!`];
1398
+ const series = HuatuPathSeries.fromModels(models, name);
1399
+ this.pathSeries.push(series);
1400
+ // Add to Definable
1401
+ this.defines.push(series);
1402
+ return [];
1403
+ }
1404
+ getResolvedXPoints(id1, id2) {
1405
+ const solved = this.solvedXPoints.find(p => (p.path1 === id1 && p.path2 === id2) || (p.path1 === id2 && p.path2 === id1));
1406
+ return solved?.points;
1407
+ }
1408
+ makeXPoints(id1, id2) {
1409
+ const ifResolved = this.getResolvedXPoints(id1, id2);
1410
+ if (ifResolved)
1411
+ return {
1412
+ result: "Success",
1413
+ errors: [],
1414
+ points: ifResolved
1415
+ };
1416
+ const errors = [];
1417
+ const md1 = this.paths.find(s => s.name === id1), md2 = this.paths.find(s => s.name === id2);
1418
+ if (!md1)
1419
+ errors.push(`Shape "${id1}" has not been defined!`);
1420
+ if (!md2)
1421
+ errors.push(`Shape "${id2}" has not been defined!`);
1422
+ if (!md1 || !md2)
1423
+ return {
1424
+ result: "Error",
1425
+ errors,
1426
+ points: []
1427
+ };
1428
+ if (!md1.path && !md1.model)
1429
+ errors.push(`Shape ${id1} is not well defined!`);
1430
+ if (!md2.path && !md2.model)
1431
+ errors.push(`Shape ${id2} is not well defined!`);
1432
+ if (errors.length > 0)
1433
+ return {
1434
+ result: "Invalid",
1435
+ errors: errors,
1436
+ points: []
1437
+ };
1438
+ let xps = (md1.path && md2.path) ? md1.path.models.map(x1 => md2.path.models.map(x2 => Intersection.getXpoints(x1, x2)).flat()).flat()
1439
+ : (md1.path && md2.model) ? md1.path.models.map(x1 => Intersection.getXpoints(x1, md2.model)).flat()
1440
+ : (md1.model && md2.path) ? md2.path.models.map(x2 => Intersection.getXpoints(md1.model, x2)).flat()
1441
+ : Intersection.getXpoints(md1.model, md2.model);
1442
+ xps = Point.deDupPoints(xps);
1443
+ // Push to reserve
1444
+ this.solvedXPoints.push({
1445
+ path1: id1,
1446
+ path2: id2,
1447
+ points: xps
1448
+ });
1449
+ // Return the successful result
1450
+ return {
1451
+ result: "Success",
1452
+ errors: [],
1453
+ points: xps,
1454
+ };
1455
+ }
1456
+ /**
1457
+ * Resolve the Intersection point
1458
+ * @param name Name of the intersection point
1459
+ * @param value Presentation to resolve the point
1460
+ * @returns Error message
1461
+ */
1462
+ resolveXPoint(name, value) {
1463
+ // Check Name
1464
+ if (this.points.find(s => s.name === name))
1465
+ return [`There has already been a point named as ${name}!`];
1466
+ if (this.pointSeries.find(s => s.name === name))
1467
+ return [`There is some Point Series which is named as ${name}!`];
1468
+ const params = value.split(";").map(p => p.trim()).filter(p => !!p);
1469
+ if (params.length < 2)
1470
+ return [`To calculate Intersection, you need give two shapes!`];
1471
+ const xpRes = this.makeXPoints(params[0], params[1]);
1472
+ if (xpRes.result === "Error" || xpRes.result === "Invalid")
1473
+ return xpRes.errors;
1474
+ const xp = Intersection.pickPoint(xpRes.points, params[2]);
1475
+ if (!xp)
1476
+ return [`No intersection resolved for ${name}!`];
1477
+ this.addPoint(new HuatuPoint(xp.x, xp.y, name, "intersection"));
1478
+ return [];
1479
+ }
1480
+ /**
1481
+ * Resolve Calculated Point, this normally will get e.g. some point on a circle,
1482
+ * center(s) of a triangle, middle point of some line-segment, etc.
1483
+ * @param name Name of the Calculated point
1484
+ * @param value Presentation to resolve the point, the possible format of the CPoint:
1485
+ * - circle; deg deg running along the circle, e.g. 'c1;30'
1486
+ * - ellipse; deg deg running along the ellipse e.g. 'ell1; -90'
1487
+ * - line; l, l could be any value
1488
+ * - lineseg; t t in [0, 1]
1489
+ * - bezier2; t t in [0, 1]
1490
+ * - bezier3; t t in [0, 1]
1491
+ * - polygon; N; [t] t-th point on the N-th side, t has the same meaning of lineseg, if t is not present, then N is the N-th vertex
1492
+ * - polyline; N; [t] t- th point on the N-th seg, t has the same meaning of Lineseg, if t is not present, then N is the N-th point
1493
+ * - arc; t t-th point on the arc, from start angle to end ang, i.e. t=0 means start, and t=1 means end
1494
+ * - triangle; 1/2/3; [t] t-th point on the 1/2/3-th side, t has the same meaning of lineseg, is t is not present, then it means vertex
1495
+ * - triangle; cIn/cOut Center of the inner/outer circle
1496
+ * - triangle; cG Center of gravity
1497
+ * - triangle; A/B/C 1/2/3-th vertex of the triangle
1498
+ * - rectangle; nw/sw/se/ne nw/sw/se/ne vertex point of the rect
1499
+ * - rectangle; n/w/s/e; t t-th point on the N/W/S/E side, t has the same meaning of lineseg
1500
+ * - rectangle; nm/wm/sm/em middle point on N/W/S/E side, easy to see that 'nm' means 'n; 0.5'
1501
+ * @returns Error message
1502
+ */
1503
+ resolveCPoint(name, value) {
1504
+ // Check Name
1505
+ if (this.points.find(s => s.name === name))
1506
+ return [`There has already been a point named as ${name}!`];
1507
+ if (this.pointSeries.find(s => s.name === name))
1508
+ return [`There is some Point Series which is named as ${name}!`];
1509
+ const params = value.split(";").map(p => p.trim()).filter(p => !!p);
1510
+ if (params.length < 2)
1511
+ return [`To Get a point from model, you need at least two parameters!`];
1512
+ const md = this.paths.find(s => s.name === params[0]);
1513
+ if (!md || !md.model)
1514
+ return [`Shape "${params[0]}" has not been defined!`];
1515
+ const p1num = this.resolveVarExpr(params[1]); // This is the first parameter, and in most cases, it is enough
1516
+ const p2num = params[2] ? this.resolveVarExpr(params[2]) : undefined;
1517
+ const p1str = params[1].toLowerCase(); // It is some dedicated string to specify points, like p1, ne, sw, etc. then use it, in lower case
1518
+ const point = p1num === undefined ? md.model.getPoint(p1str, p2num) : md.model.getPoint(p1num, p2num);
1519
+ if (point) {
1520
+ this.addPoint(new HuatuPoint(point.x, point.y, name, "calculated"));
1521
+ return [];
1522
+ }
1523
+ // Check if find normal point
1524
+ if (p1str === "np" && md.path && PathSegmentedPath.isSegment(md.path)) {
1525
+ let npT = this.resolveVarExpr(params[2]);
1526
+ let npDir = params[3].toLowerCase();
1527
+ let npDist = this.resolveVarExpr(params[4]);
1528
+ if (npT !== undefined && WtMath.isIn0and1(npT) &&
1529
+ (npDir === "left" || npDir === "right") &&
1530
+ npDist !== undefined) {
1531
+ let np = md.path.auxModel?.getNormalPoint(npT, npDist, npDir);
1532
+ if (np) {
1533
+ this.addPoint(new HuatuPoint(np.x, np.y, name, "calculated"));
1534
+ return [];
1535
+ }
1536
+ else {
1537
+ return [`Failed to calculate Normal Point from path '${params[0]}'`];
1538
+ }
1539
+ }
1540
+ else {
1541
+ return [`Parameters incorrect to Calculate Normal Point from path '${params[0]}'`];
1542
+ }
1543
+ }
1544
+ // Default return of error message
1545
+ return [`No valid result to calculate CPoint '${name}' from '${value}'`];
1546
+ }
1547
+ /**
1548
+ * A small function to resolve the Range definition input by use
1549
+ * @param def The definition string for a range, it should be like 'start|step|end'
1550
+ * @returns The resolved range, or undefined if resolve failed
1551
+ */
1552
+ resolveRange(def) {
1553
+ if (!def)
1554
+ return undefined;
1555
+ const match = def.match(WT_REGEX.RANGE);
1556
+ if (!match)
1557
+ return undefined;
1558
+ const valStart = this.resolveVarExpr(match[1].trim()), valStep = this.resolveVarExpr(match[2].trim()), valEnd = this.resolveVarExpr(match[3].trim());
1559
+ if (valStart === undefined) {
1560
+ this.warnings.push(`'${match[1]}' is not a valid Range Start!`);
1561
+ return undefined;
1562
+ }
1563
+ if (valStep === undefined) {
1564
+ this.warnings.push(`'${match[2]}' is not a valid Range Step!`);
1565
+ return undefined;
1566
+ }
1567
+ if (valEnd === undefined) {
1568
+ this.warnings.push(`'${match[3]}' is not a valid Range End!`);
1569
+ return undefined;
1570
+ }
1571
+ const range = new NumberRange(valStart, valStep, valEnd);
1572
+ if (WtMath.isZero(range.step)) {
1573
+ this.warnings.push(`Range step is zero ([${def}], step is zero, or start == end), will use the only number of [Range Start]!`);
1574
+ }
1575
+ return range;
1576
+ }
1577
+ /**
1578
+ * Resolve the Point Series definitions
1579
+ * @param name Name of the Point Series
1580
+ * @param value The definition string of the Point Series, e.g. "circ1; 0 | 15 | 60", which means get the points on
1581
+ * Circle "circ1", with the degrees starting from 0 to 60(not included) with interval of 15.
1582
+ * And we can the format of "0 | 15 | 60" a range
1583
+ * The possible formats that could produce point series are:
1584
+ * - p1; p2; p3; p4; ... Just simple list of defined points
1585
+ * - circle; deg[] or deg_range Discrete degs or deg range running along the circle, e.g. 'c1;0 | 5 | 50' or 'c1; 0, 23, 33, 44'
1586
+ * - ellipse; deg[] or deg_range Discrete degs or deg range running along the Ellipse, e.g. 'ell1; 20 | 5 | 180' or 'ell1; 3, 4, 8, 90'
1587
+ * - line; l[] or l_range, Discrete length collection or a range of length
1588
+ * - lineseg; t[] or t_range Discrete t's or range of t, t need to be in [0, 1];
1589
+ * - bezier2; t[] or t_range Discrete t's or range of t, t need to be in [0, 1];
1590
+ * - bezier3; t[] or t_range Discrete t's or range of t, t need to be in [0, 1];
1591
+ * - polygon; N; t[] Discrete t's or range of t, on the N-th side of Polygon
1592
+ * - polygon; n1, n2, n3,... The n-th vertex of the polygon, n1, n2, n3... need to be in [1, N]
1593
+ * - polyline; N; [t] Discrete t's or range of t, on the N-th segment of Polyline, N needs to be in [1, N-1]
1594
+ * - polyline; n1, n2, n3,... The n-th point of Polyline, n1, n2, n3... needs to be in [1, N]
1595
+ * - arc; t[] or t_range Discrete t's or range of t, t need to be in [0, 1];
1596
+ * - triangle; [1/2/3/A/B/C/cIn/cOut/cG] The 1/2/3/A/B/C/cIn/cOut/cG point of the triangle, in collection
1597
+ * - rectangle; [nw/sw/se/ne/nm/wm/sm/em/n/e/s/w] nw/sw/se/ne/nm/wm/sm/em/n/e/s/w point of the rect, in collection, same meaning as in CPoint resolving
1598
+ * - rectangle; north/west/south/east; t[] or t_range t's or range of t points on the N/W/S/E side, t is in [0, 1]
1599
+ * @returns Error Message if any
1600
+ */
1601
+ resolvePointSeries(name, value) {
1602
+ // Check Name
1603
+ if (this.points.find(s => s.name === name))
1604
+ return [`There is a point named as ${name}!`];
1605
+ if (this.pointSeries.find(s => s.name === name))
1606
+ return [`There is already some Point Series named as ${name}!`];
1607
+ const makeT = (t) => {
1608
+ const num = this.resolveVarExpr(t);
1609
+ if (num !== undefined)
1610
+ return num;
1611
+ return t.toLowerCase();
1612
+ };
1613
+ const params = value.split(";").map(p => p.trim()).filter(p => !!p);
1614
+ if (params.length < 2)
1615
+ return [`To make point series from Model, you need at least two parameters!`];
1616
+ // Guess the first param is some Shape series
1617
+ const guessShapeSeries = this.pathSeries.find(ps => ps.name === params[0]);
1618
+ if (guessShapeSeries) {
1619
+ const posRange = this.resolveRange(params[1]);
1620
+ let posVals = posRange ? posRange.numbers : params[1].split(/[,\s]/).map(x => makeT(x));
1621
+ const tVals = this.tryNumberSeries(params[2]);
1622
+ const pts = [];
1623
+ // Iterate and push to array
1624
+ for (let i = 0; i < guessShapeSeries.models.length; i++) {
1625
+ const model = guessShapeSeries.models[i];
1626
+ for (let j = 0; j < posVals.length; j++) {
1627
+ if (tVals.length > 0) {
1628
+ for (let k = 0; k < tVals.length; k++) {
1629
+ const p = model.getPoint(posVals[j], tVals[k]);
1630
+ if (p)
1631
+ pts.push(p);
1632
+ else
1633
+ this.warnings.push(`Point not found by specifying '${params[0]}; ${posVals[j]}; ${tVals[k]}' !`);
1634
+ }
1635
+ }
1636
+ else {
1637
+ const p = model.getPoint(posVals[j]);
1638
+ if (p)
1639
+ pts.push(p);
1640
+ else
1641
+ this.warnings.push(`Point not found by specifying '${params[0]}; ${posVals[j]}' !`);
1642
+ }
1643
+ }
1644
+ }
1645
+ if (pts.length < 1) {
1646
+ this.warnings.push(`No points Resolved based on Shape Series '${params[0]}'`);
1647
+ }
1648
+ else {
1649
+ this.pointSeries.push(new HuatuPointSeries(name, ...pts));
1650
+ }
1651
+ return [];
1652
+ }
1653
+ // Guess the first parameter is a point, then the whole list should be points
1654
+ const p0Point = this.resolvePointFromExpr(params[0]);
1655
+ const validPoints = [];
1656
+ if (p0Point) { // The first is a point, so assume the def string is a list of points
1657
+ params.forEach((param, i) => {
1658
+ const p = this.resolvePointFromExpr(param);
1659
+ if (!p) {
1660
+ this.warnings.push(`'${param}' cannot be resolved to a point!`);
1661
+ }
1662
+ else {
1663
+ validPoints.push(p);
1664
+ }
1665
+ });
1666
+ }
1667
+ else {
1668
+ // Guess the first param is a model/shape
1669
+ const md = this.paths.find(s => s.name === params[0]);
1670
+ if (!md)
1671
+ return [`Shape "${params[0]}" has not been defined!`];
1672
+ // Check if the pointSeries is calculating intersections
1673
+ if (params[1].toLocaleLowerCase() === 'x') {
1674
+ if (!params[2])
1675
+ return [`Model 2 is not presented when resolving intersections!`];
1676
+ const xpRes = this.makeXPoints(params[0], params[2]);
1677
+ if (xpRes.result === "Error" || xpRes.result === "Invalid")
1678
+ return xpRes.errors;
1679
+ if (xpRes.points.length < 1) {
1680
+ this.warnings.push(`No points found for Intersections of '${params[0]}' and '${params[2]}'!`);
1681
+ }
1682
+ else {
1683
+ this.pointSeries.push(new HuatuPointSeries(name, ...xpRes.points));
1684
+ }
1685
+ return [];
1686
+ }
1687
+ if (!md.model)
1688
+ return [`Shape "${params[0]}" is not a BASIC SHAPE, cannot make point series from it!`];
1689
+ let pointsGot;
1690
+ const p1Range = this.resolveRange(params[1]); // Guess the first parameter as a range
1691
+ if (p1Range && p1Range.count() > 0) {
1692
+ pointsGot = p1Range.numbers
1693
+ .map(t => md.model.getPoint(t))
1694
+ .filter(p => !!p);
1695
+ }
1696
+ else {
1697
+ const p1Array = params[1].split(/[,\s/]/).map(x => x.trim()).filter(x => !!x).map(x => makeT(x));
1698
+ let ts = this.tryNumberSeries(params[2]);
1699
+ if (ts.length > 0) {
1700
+ pointsGot = p1Array.map(p1 => ts.map(t => md.model.getPoint(p1, t)))
1701
+ .flat()
1702
+ .filter(x => !!x);
1703
+ }
1704
+ else {
1705
+ // In this case, all the parameters are not splitted in to two parts, and they should be treated as a flat list of parameter 1
1706
+ pointsGot = params.filter((p, i) => i > 0)
1707
+ .map(p => p.split(/[,\s]/)
1708
+ .map(x => x.trim())
1709
+ .filter(x => !!x)
1710
+ .map(x => makeT(x)))
1711
+ .flat()
1712
+ .map(t => md.model.getPoint(t))
1713
+ .filter(x => !!x);
1714
+ }
1715
+ }
1716
+ validPoints.push(...pointsGot);
1717
+ }
1718
+ if (validPoints.length < 1)
1719
+ return [`No Point resolved with '${value}'for '${name}' as PointSeries!`];
1720
+ this.pointSeries.push(new HuatuPointSeries(name, ...validPoints));
1721
+ this.infos.push(`${validPoints.length} points resolved for Point Series '${name}'!`);
1722
+ return [];
1723
+ }
1724
+ /**
1725
+ * A shortcut function to resolve a single segment which is one of the compound path.
1726
+ * Only the Valid segment types, i.e. LineSeg, Arc, Bezier2 and Bezier3 are accepted
1727
+ * as segments
1728
+ * @param def The input of the segment
1729
+ * @returns The resolved segmented path, or close mark
1730
+ */
1731
+ resolveSegmentedPath(def) {
1732
+ // If the segment definition starts with three dots, it will be parsed as expanded segments from defined models
1733
+ if (def.startsWith("...")) {
1734
+ def = def.substring(3).trim();
1735
+ const posSemiColon = def.indexOf(";");
1736
+ const modelName = posSemiColon > 0 ? def.substring(0, posSemiColon).trim() : def;
1737
+ const htPath = this.paths.find(x => x.name === modelName);
1738
+ if (!htPath) {
1739
+ this.warnings.push(`Path Segment [${def}] Failure: "${modelName}" not found!`);
1740
+ return undefined;
1741
+ }
1742
+ else {
1743
+ const params = parseStrIntoKeyValuePair(def.substring(posSemiColon), {
1744
+ keys: [
1745
+ ["radius"],
1746
+ ["cwOrCcw"],
1747
+ ["reverse"]
1748
+ ],
1749
+ ints: [],
1750
+ numbers: [],
1751
+ booleans: ["reverse"],
1752
+ allowAppend: [],
1753
+ qualifiers: {
1754
+ "radius": "number[]",
1755
+ },
1756
+ blindGuess: {
1757
+ "cwOrCcw": (x) => {
1758
+ let xLc = x.toLowerCase();
1759
+ if (xLc === "cw" || xLc === "ccw")
1760
+ return xLc;
1761
+ else
1762
+ return undefined;
1763
+ }
1764
+ }
1765
+ });
1766
+ if (htPath.path instanceof PathTriangle ||
1767
+ htPath.path instanceof PathRect ||
1768
+ htPath.path instanceof PathPolygon ||
1769
+ htPath.path instanceof PathPolyline ||
1770
+ htPath.path instanceof PathCircle ||
1771
+ htPath.path instanceof PathEllipse) {
1772
+ return htPath.path.toPathSegments(params.body.radius, params.body.cwOrCcw);
1773
+ }
1774
+ else if (htPath.path instanceof PathSegmentedPath) {
1775
+ return htPath.path.getSegs(params.body.reverse, params.body.radius);
1776
+ }
1777
+ else {
1778
+ this.warnings.push(`Path Segment [${def}] Failure: "${modelName}" is a type that not allowed to be expanded!`);
1779
+ return undefined;
1780
+ }
1781
+ }
1782
+ }
1783
+ // Check if duplicating operation
1784
+ if (/\s*(dupTo)\s*;.*/i.test(def)) {
1785
+ let dupped = this.tryAsPathDuplication(def, true);
1786
+ if (typeof dupped === "string") {
1787
+ this.warnings.push(dupped);
1788
+ return undefined;
1789
+ }
1790
+ if (dupped) {
1791
+ if (PathSegmentedPath.isSegment(dupped))
1792
+ return dupped;
1793
+ if (dupped instanceof PathSegmentedPath)
1794
+ return dupped.segments;
1795
+ }
1796
+ this.warnings.push(`Cannot make duplicated path(seg)s by '${def}'!`);
1797
+ return undefined;
1798
+ }
1799
+ // Treat as Segment Definitions
1800
+ const params = def.split(";").map(x => x.trim()).filter(x => !!x);
1801
+ if (params[0].toLowerCase() === "close")
1802
+ return "CLOSE_MARK";
1803
+ const definedModel = this.paths.find(s => s.name === params[0]);
1804
+ const isReverse = !!definedModel && !!params[1] && params[1].toLowerCase() === "reverse";
1805
+ const model = definedModel ? definedModel.model : this.resolveShapeDef(def);
1806
+ if (typeof model === "string" || !model) {
1807
+ this.warnings.push(`Path Segment [${def}] Failure: "${model}"!`);
1808
+ return undefined;
1809
+ }
1810
+ ;
1811
+ const path = PathSegmentedPath.fromModel(model);
1812
+ return isReverse ? path?.reverse() : path;
1813
+ }
1814
+ tryPathTransform(def, name) {
1815
+ const params = def.split(";").map(x => x.trim()).filter(x => !!x);
1816
+ if (params.length < 3)
1817
+ return undefined;
1818
+ const path = this.paths.find(x => x.name === params[1]);
1819
+ if (!path)
1820
+ return undefined;
1821
+ const transType = params[0].toLowerCase();
1822
+ if (transType === "movex") {
1823
+ const dx = this.resolveVarExpr(params[2]);
1824
+ return dx !== undefined ? path.moveBy(dx, 0, name) : undefined;
1825
+ }
1826
+ else if (transType === "movey") {
1827
+ const dy = this.resolveVarExpr(params[2]);
1828
+ return dy !== undefined ? path.moveBy(0, dy, name) : undefined;
1829
+ }
1830
+ else if (transType === "moveby" || transType === "move") {
1831
+ const pt = this.resolvePointFromExpr(params[2]);
1832
+ return pt !== undefined ? path.moveBy(pt.x, pt.y, name) : undefined;
1833
+ }
1834
+ else if (transType === "rotate" || transType === "rotateby") {
1835
+ const deg = this.resolveVarExpr(params[2]);
1836
+ const at = params[3] ? this.resolvePointFromExpr(params[3]) : undefined;
1837
+ return deg === undefined ? undefined : path.rotateBy(deg, at, name);
1838
+ }
1839
+ else {
1840
+ return undefined;
1841
+ }
1842
+ }
1843
+ /**
1844
+ * Resolve Path definition, the input might be a string, or string array
1845
+ * @param name Name of the Calculated point
1846
+ * @param value Presentation to resolve the path
1847
+ * @returns Error message
1848
+ */
1849
+ resolvePath(name, value) {
1850
+ // Check Name
1851
+ if (this.paths.find(s => s.name === name))
1852
+ return [`There has already been a Path named as ${name}!`];
1853
+ if (this.pathSeries.find(s => s.name === name))
1854
+ return [`There is some Path Series named as ${name}!`];
1855
+ // Check if modeled path, the value is the model name if true
1856
+ if (typeof value === "string") {
1857
+ let path = this.tryPathTransform(value, name);
1858
+ if (!path) {
1859
+ const tryAsSegPath = this.resolveSegmentedPath(value);
1860
+ if (!tryAsSegPath) {
1861
+ const model = this.resolveShapeDef(value);
1862
+ if (typeof model === "string")
1863
+ return [`Shape (${value}) resolving failed: ${model}`];
1864
+ path = HuatuPath.fromModel(model, name);
1865
+ }
1866
+ else {
1867
+ if (tryAsSegPath === "CLOSE_MARK")
1868
+ return [`You cannot define a path by just a 'close' mark!`];
1869
+ const segPath = new PathSegmentedPath();
1870
+ if (Array.isArray(tryAsSegPath)) {
1871
+ segPath.addSeg(...tryAsSegPath);
1872
+ }
1873
+ else {
1874
+ segPath.addSeg(tryAsSegPath);
1875
+ }
1876
+ path = new HuatuPath(segPath, name);
1877
+ }
1878
+ }
1879
+ this.paths.push(path);
1880
+ // Add to Definable
1881
+ this.defines.push(path);
1882
+ return []; // Resolved if model matched
1883
+ }
1884
+ // It should be treated as Segmented path with each list item as one segment
1885
+ // If some path consisting of multiple Line(seg)s, then please organize it in the way of
1886
+ // Segmented Path, i.e. write it in a list
1887
+ const path = new PathSegmentedPath();
1888
+ const models = value.map(s => this.resolveSegmentedPath(s))
1889
+ .filter(m => !!m);
1890
+ for (let idx = 0; idx < models.length; idx++) {
1891
+ const seg = models[idx];
1892
+ if (typeof seg === "string") {
1893
+ if (seg === "CLOSE_MARK" && path.segments.length > 0) {
1894
+ path.segments[path.segments.length - 1].closePath = true;
1895
+ }
1896
+ }
1897
+ else {
1898
+ if (Array.isArray(seg)) {
1899
+ path.addSeg(...seg);
1900
+ }
1901
+ else {
1902
+ path.addSeg(seg);
1903
+ }
1904
+ }
1905
+ }
1906
+ if (path.segments.length < 1)
1907
+ return [`No path segment(s) resolved for (${name})!`];
1908
+ const segPath = new HuatuPath(path, name);
1909
+ this.paths.push(segPath);
1910
+ // Add to Definable
1911
+ this.defines.push(segPath);
1912
+ return [];
1913
+ }
1914
+ /**
1915
+ * Check if the user has given a valid string to specify the pattern, if it is a name for "predefined"
1916
+ * pattern, then the pattern will be copied into the in-use collection with the same name. Or it
1917
+ * needs to be some pattern defined by user manually.
1918
+ * @param ptn The defined name or pre-defined name for the Pattern user wants to use
1919
+ * @returns The Pattern or undefined if not found
1920
+ */
1921
+ checkPattern(ptn) {
1922
+ if (!ptn)
1923
+ return undefined;
1924
+ const tryPattern = this.patterns.find(p => p.name === ptn);
1925
+ if (tryPattern) {
1926
+ tryPattern.refCount++;
1927
+ return tryPattern;
1928
+ }
1929
+ const sysPattern = predefinedPatterns[ptn];
1930
+ if (sysPattern) {
1931
+ const pattern = new HuatuPattern(ptn, sysPattern);
1932
+ this.patterns.push(pattern);
1933
+ pattern.refCount++;
1934
+ this.defines.push(pattern);
1935
+ return pattern;
1936
+ }
1937
+ return undefined;
1938
+ }
1939
+ /**
1940
+ * Check if the user has given a valid string to specify the marker, if it is a name for "predefined"
1941
+ * marker, then the marker will be copied into the in-use collection with the same name. Or it
1942
+ * needs to be some marker defined by user manually.
1943
+ * @param ptn The defined name or pre-defined name for the Marker user wants to use
1944
+ * @returns The Marker or undefined if not found
1945
+ */
1946
+ checkMarker(mkr) {
1947
+ if (!mkr)
1948
+ return undefined;
1949
+ const tryMarker = this.markers.find(p => p.name === mkr);
1950
+ if (tryMarker) {
1951
+ tryMarker.refCount++;
1952
+ return tryMarker;
1953
+ }
1954
+ const sysMarker = predefinedMarkers[mkr];
1955
+ if (sysMarker) {
1956
+ const marker = new HuatuMarker(mkr, sysMarker);
1957
+ this.markers.push(marker);
1958
+ marker.refCount++;
1959
+ this.defines.push(marker);
1960
+ return marker;
1961
+ }
1962
+ return undefined;
1963
+ }
1964
+ parseStyleFromKeyValueResults(parseRes) {
1965
+ const params = parseRes.body;
1966
+ // Handle Orphans
1967
+ const realOrphans = [];
1968
+ parseRes.orphans.forEach(orphan => {
1969
+ const sty = this.styles.find(x => x.name === orphan);
1970
+ if (sty) {
1971
+ if (params.styles)
1972
+ params.styles += `,${orphan}`;
1973
+ else
1974
+ params.styles = orphan;
1975
+ }
1976
+ else {
1977
+ realOrphans.push(orphan);
1978
+ }
1979
+ });
1980
+ realOrphans.push(...parseRes.unluckyOrphans);
1981
+ if (realOrphans.length > 0) {
1982
+ this.warnings.push(`Options [${realOrphans.map(x => `"${x}"`).join(",")}] cannot be resolved to make a drawing style!`);
1983
+ }
1984
+ const failedPairs = [...parseRes.failedPairs];
1985
+ // Handle Sub Keys
1986
+ let paramFill = {}, paramStroke = {}, paramMarker = {};
1987
+ if (parseRes.subKeys && parseRes.subKeys["fill"]) {
1988
+ let subRes = parseRes.subKeys["fill"];
1989
+ assignOnlyDefined(paramFill, subRes.body);
1990
+ if (subRes.orphans.length > 0) {
1991
+ this.warnings.push(`Options [${subRes.orphans.map(x => `"${x}"`).join(",")}] cannot be resolved to make 'Fill' attributes!`);
1992
+ }
1993
+ failedPairs.push(...subRes.failedPairs);
1994
+ }
1995
+ if (parseRes.subKeys && parseRes.subKeys["stroke"]) {
1996
+ let subRes = parseRes.subKeys["stroke"];
1997
+ assignOnlyDefined(paramStroke, subRes.body);
1998
+ if (subRes.unluckyOrphans.length > 0) {
1999
+ subRes.unluckyOrphans.forEach(x => {
2000
+ if (x === "round" && !paramStroke.lineJoin) {
2001
+ paramStroke.lineJoin = "round";
2002
+ }
2003
+ else {
2004
+ subRes.orphans.push(x);
2005
+ }
2006
+ });
2007
+ }
2008
+ if (subRes.orphans.length > 0) {
2009
+ const realStrokeOrphans = [];
2010
+ for (let i = 0; i < subRes.orphans.length; i++) {
2011
+ if (!paramStroke.width) {
2012
+ let tryWidth = this.resolveVarExpr(subRes.orphans[i]);
2013
+ if (tryWidth !== undefined && tryWidth > 0) {
2014
+ paramStroke.width = tryWidth;
2015
+ }
2016
+ }
2017
+ else {
2018
+ realStrokeOrphans.push(subRes.orphans[i]);
2019
+ }
2020
+ }
2021
+ if (realStrokeOrphans.length > 0) {
2022
+ this.warnings.push(`Options [${realStrokeOrphans.map(x => `"${x}"`).join(",")}] cannot be resolved to make 'Stroke' attributes!`);
2023
+ }
2024
+ }
2025
+ failedPairs.push(...subRes.failedPairs);
2026
+ }
2027
+ if (parseRes.subKeys && parseRes.subKeys["marker"]) {
2028
+ let subRes = parseRes.subKeys["marker"];
2029
+ assignOnlyDefined(paramMarker, subRes.body);
2030
+ let i = 0;
2031
+ while (subRes.orphans.length > 0 && i < 3) {
2032
+ if (!paramMarker.end) {
2033
+ paramMarker.end = subRes.orphans.shift();
2034
+ }
2035
+ else if (!paramMarker.start) {
2036
+ paramMarker.start = subRes.orphans.shift();
2037
+ }
2038
+ else if (!paramMarker.mid) {
2039
+ paramMarker.mid = subRes.orphans.shift();
2040
+ }
2041
+ i++;
2042
+ }
2043
+ if (subRes.orphans.length > 0) {
2044
+ this.warnings.push(`Options [${subRes.orphans.map(x => `"${x}"`).join(",")}] cannot be resolved to make 'Marker' attributes!`);
2045
+ }
2046
+ failedPairs.push(...subRes.failedPairs);
2047
+ }
2048
+ failedPairs.forEach(x => this.warnings.push(`['${x.key}': '${x.value}'] are not valid for Style Design!`));
2049
+ // Now pick the attributes in the results to build a style
2050
+ const style = new HuatuStyle();
2051
+ if (params.styles) {
2052
+ const styles = params.styles
2053
+ .split(/[,\s]/)
2054
+ .map(x => x.trim())
2055
+ .filter(x => !!x)
2056
+ .map(x => {
2057
+ const sty = this.styles.find(s => s.name === x);
2058
+ if (!sty)
2059
+ this.warnings.push(`Style '${x}' is not defined, or it is not a style!`);
2060
+ return sty;
2061
+ })
2062
+ .filter(x => !!x);
2063
+ if (styles.length > 0)
2064
+ style.refStyles.push(...styles);
2065
+ }
2066
+ if (params.fillColor)
2067
+ style.fillColor = params.fillColor;
2068
+ else if (paramFill.color)
2069
+ style.fillColor = paramFill.color;
2070
+ if (params.fillOpacity)
2071
+ style.fillOpacity = fitNumInRange(params.fillOpacity, 0, 1);
2072
+ else if (paramFill.opacity)
2073
+ style.fillOpacity = fitNumInRange(paramFill.opacity, 0, 1);
2074
+ if (params.pattern) {
2075
+ const ptn = this.checkPattern(params.pattern);
2076
+ if (ptn)
2077
+ style.fillPattern = ptn;
2078
+ else
2079
+ this.warnings.push(`'${params.pattern}' is not a pattern!`);
2080
+ }
2081
+ else if (paramFill.pattern) {
2082
+ const ptn = this.checkPattern(paramFill.pattern);
2083
+ if (ptn)
2084
+ style.fillPattern = ptn;
2085
+ else
2086
+ this.warnings.push(`'${paramFill.pattern}' is not a pattern!`);
2087
+ }
2088
+ if (params.fillRule)
2089
+ style.fillRule = params.fillRule;
2090
+ else if (paramFill.rule)
2091
+ style.fillRule = paramFill.rule;
2092
+ if (params.gradient) {
2093
+ style.fillGradient = params.gradient;
2094
+ // TODO: add when Gradient implemented
2095
+ }
2096
+ else if (paramFill.gradient) {
2097
+ style.fillGradient = paramFill.gradient;
2098
+ // TODO: add when gradient implemented
2099
+ }
2100
+ if (params.strokeColor)
2101
+ style.strokeColor = params.strokeColor;
2102
+ else if (paramStroke.color)
2103
+ style.strokeColor = paramStroke.color;
2104
+ if (params.strokeWidth)
2105
+ style.strokeWidth = params.strokeWidth;
2106
+ else if (paramStroke.width)
2107
+ style.strokeWidth = paramStroke.width;
2108
+ if (params.strokeOpacity)
2109
+ style.strokeOpacity = params.strokeOpacity;
2110
+ else if (paramStroke.opacity)
2111
+ style.strokeOpacity = paramStroke.opacity;
2112
+ if (params.lineCap)
2113
+ style.strokeLineCap = params.lineCap;
2114
+ else if (paramStroke.lineCap)
2115
+ style.strokeLineCap = paramStroke.lineCap;
2116
+ if (params.lineJoin)
2117
+ style.strokeLineJoin = params.lineJoin;
2118
+ else if (paramStroke.lineJoin)
2119
+ style.strokeLineJoin = paramStroke.lineJoin;
2120
+ if (params.dashArray && params.dashArray.length > 0)
2121
+ style.strokeDashArray = params.dashArray;
2122
+ else if (paramStroke.dashArray && paramStroke.dashArray.length > 0)
2123
+ style.strokeDashArray = paramStroke.dashArray;
2124
+ if (params.dashOffset)
2125
+ style.strokeDashOffset = params.dashOffset;
2126
+ else if (paramStroke.dashOffset)
2127
+ style.strokeDashOffset = paramStroke.dashOffset;
2128
+ let mkrId;
2129
+ mkrId = paramMarker.start ? paramMarker.start
2130
+ : params.markerStart ? params.markerStart
2131
+ : undefined;
2132
+ if (mkrId) {
2133
+ const mkr = this.checkMarker(mkrId);
2134
+ if (mkr)
2135
+ style.markerStart = mkr;
2136
+ else
2137
+ this.warnings.push(`Marker '${mkrId}' is not a marker!`);
2138
+ }
2139
+ mkrId = paramMarker.mid ? paramMarker.mid
2140
+ : params.markerMid ? params.markerMid
2141
+ : undefined;
2142
+ if (mkrId) {
2143
+ const mkr = this.checkMarker(mkrId);
2144
+ if (mkr)
2145
+ style.markerMid = mkr;
2146
+ else
2147
+ this.warnings.push(`Marker '${mkrId}' is not a marker!`);
2148
+ }
2149
+ mkrId = paramMarker.end ? paramMarker.end
2150
+ : params.markerEnd ? params.markerEnd
2151
+ : undefined;
2152
+ if (mkrId) {
2153
+ const mkr = this.checkMarker(mkrId);
2154
+ if (mkr)
2155
+ style.markerEnd = mkr;
2156
+ else
2157
+ this.warnings.push(`Marker '${mkrId}' is not a marker!`);
2158
+ }
2159
+ if (params.clip) {
2160
+ let clip = this.clips.find(c => c.name === params.clip);
2161
+ if (clip) {
2162
+ style.clipPath = clip;
2163
+ clip.refCount++;
2164
+ }
2165
+ }
2166
+ // Handle global opacity var `opaque`
2167
+ if (params.opaque && WtMath.isIn0and1(params.opaque)) {
2168
+ if (style.fillOpacity === undefined)
2169
+ style.fillOpacity = params.opaque;
2170
+ if (style.strokeOpacity === undefined)
2171
+ style.strokeOpacity = params.opaque;
2172
+ }
2173
+ return style;
2174
+ }
2175
+ /**
2176
+ * Resolve Style definition, the input might be a string, or a style object
2177
+ * @param name Name of the style
2178
+ * @param value Presentation to resolve the style
2179
+ * @returns Error message
2180
+ */
2181
+ resolveStyle(name, value) {
2182
+ const parseRes = parseStrIntoKeyValuePair(value, HuatuStyle.optsStyle);
2183
+ const style = this.parseStyleFromKeyValueResults(parseRes);
2184
+ style.name = name;
2185
+ style.copyRefAttrs();
2186
+ // TODO: Mask will not be included
2187
+ this.styles.push(style);
2188
+ return [];
2189
+ }
2190
+ /**
2191
+ * The entry function to resolve a LabelStyle defined
2192
+ * @param name Name of the LabelStyle defined
2193
+ * @param value The definition string/object for the LabelStyle
2194
+ * @returns Error message if any
2195
+ */
2196
+ resolveLabelStyle(name, value) {
2197
+ const optsLabelStyle = {
2198
+ keys: [
2199
+ ["refs", "styles", "style", "ref"],
2200
+ ["textAlign", "align"],
2201
+ ["bold"],
2202
+ ["italic"],
2203
+ ["addLine", "line"],
2204
+ ["fontName", "font"],
2205
+ ["color", "fontColor"],
2206
+ ["fontSize", "size", "fs"],
2207
+ ],
2208
+ booleans: ["bold", "italic"],
2209
+ ints: [],
2210
+ numbers: [
2211
+ "fontSize", "size", "fs",
2212
+ ],
2213
+ allowAppend: ["refs", "styles", "style", "ref"],
2214
+ qualifiers: {
2215
+ "color": isValidColor,
2216
+ "fontColor": isValidColor,
2217
+ "textAlign": HuatuLabelStyle.TEXT_ALIGNS,
2218
+ "align": HuatuLabelStyle.TEXT_ALIGNS,
2219
+ "addLine": HuatuLabelStyle.DECORATION_LINES,
2220
+ "line": HuatuLabelStyle.DECORATION_LINES,
2221
+ },
2222
+ blindGuess: {
2223
+ "textAlign": HuatuLabelStyle.TEXT_ALIGNS,
2224
+ "addLine": [
2225
+ "over", "overline",
2226
+ "under", "underline",
2227
+ "middleline", "through", "strike-through", "strikethrough"
2228
+ ],
2229
+ "fontName": HuatuLabelStyle.PREDEFINED_FONT_NAMES,
2230
+ "color": isValidColor,
2231
+ }
2232
+ };
2233
+ let res;
2234
+ let lsParams;
2235
+ if (typeof value === "string") {
2236
+ res = parseStrIntoKeyValuePair(value, optsLabelStyle);
2237
+ lsParams = res.body;
2238
+ const realOrphans = [];
2239
+ res.orphans.forEach(orphan => {
2240
+ let orphanAdopted = false;
2241
+ if (this.labelStyles.find(x => x.name === orphan)) {
2242
+ lsParams.refs = lsParams.refs ? orphan : (lsParams.refs + `, ${orphan}`);
2243
+ orphanAdopted = true;
2244
+ }
2245
+ if (!orphanAdopted) {
2246
+ let fsStr = /^([1-9]\d*)p[xt]$/i.exec(orphan);
2247
+ if (fsStr && !lsParams.fontSize) {
2248
+ lsParams.fontSize = parseFloat(fsStr[1]);
2249
+ orphanAdopted = true;
2250
+ }
2251
+ }
2252
+ if (!orphanAdopted) {
2253
+ realOrphans.push(orphan);
2254
+ }
2255
+ });
2256
+ const orphans = realOrphans.map(x => `'${x}'`).join(", ");
2257
+ this.warnings.push(`[Resolving Label Style '${name}']: ${orphans} could not be resolved!`);
2258
+ }
2259
+ else {
2260
+ res = parseStrIntoKeyValuePair(value, optsLabelStyle);
2261
+ lsParams = res.body;
2262
+ res.failedPairs.forEach(p => {
2263
+ this.warnings.push(`[Resolving Label Style '${name}']: {'${p.key}': '${p.value}'} could not be understood!`);
2264
+ });
2265
+ }
2266
+ const lblStyle = new HuatuLabelStyle(name);
2267
+ if (lsParams.refs) {
2268
+ lsParams.refs.split(/[,\s]/i)
2269
+ .map(x => x.trim())
2270
+ .filter(x => !!x)
2271
+ .forEach(x => {
2272
+ const sty = this.labelStyles.find(s => s.name === x);
2273
+ if (sty) {
2274
+ if (!lblStyle.refStyles.find(s => s === sty)) {
2275
+ lblStyle.refStyles.push(sty);
2276
+ }
2277
+ else {
2278
+ this.warnings.push(`[Resolving Label Style]: Style '${x}' has been already added to the label!`);
2279
+ }
2280
+ }
2281
+ else {
2282
+ this.warnings.push(`[Resolving Label Style]: '${x}' is not a valid defined Label Style!`);
2283
+ }
2284
+ });
2285
+ }
2286
+ if (lsParams.textAlign)
2287
+ lblStyle.textAlign = lsParams.textAlign;
2288
+ if (lsParams.color)
2289
+ lblStyle.color = lsParams.color;
2290
+ if (lsParams.fontName)
2291
+ lblStyle.font = lsParams.fontName;
2292
+ if (lsParams.bold)
2293
+ lblStyle.bold = lsParams.bold;
2294
+ if (lsParams.italic)
2295
+ lblStyle.italic = lsParams.italic;
2296
+ if (lsParams.addLine)
2297
+ lblStyle.addLine = lsParams.addLine;
2298
+ if (lsParams.fontSize)
2299
+ lblStyle.fontSize = lsParams.fontSize;
2300
+ this.labelStyles.push(lblStyle);
2301
+ return [];
2302
+ }
2303
+ /**
2304
+ * Resolve Symbol definition, the input might be a string, or a string list
2305
+ * @param name Name of the Symbol
2306
+ * @param value Presentation to resolve the symbol
2307
+ * @returns Error message
2308
+ */
2309
+ resolveSymbol(name, value) {
2310
+ // Check Name
2311
+ const nameError = this.isDrawingNameValid(name, "Symbol");
2312
+ if (nameError)
2313
+ return [nameError];
2314
+ // A symbol will accept path/pathSeries and symbol
2315
+ const allowedTypes = ["path", "pathSeries", "symbol"];
2316
+ // If it is only one line, i.d. the symbol has only one definition, We assume that it is the shape it contains
2317
+ if (typeof value === "string") {
2318
+ const item = this.resolveAsDrawingItem(value, allowedTypes);
2319
+ if (typeof item === "string")
2320
+ return [item]; // Return the error message
2321
+ const symbol = new HuatuSymbol(name);
2322
+ symbol.addElement(item);
2323
+ this.defines.push(symbol); // Add to Definable
2324
+ this.symbols.push(symbol);
2325
+ return [];
2326
+ }
2327
+ // If the definition is a list, we assume that it contains multiple drawing definitions for the Symbol
2328
+ const errs = [];
2329
+ const symbol = new HuatuSymbol(name);
2330
+ for (let i = 0; i < value.length; i++) {
2331
+ const def = value[i];
2332
+ // Check if Desc
2333
+ const tryDesc = WtHuatu.tryDesc(def);
2334
+ if (tryDesc) {
2335
+ symbol.desc = tryDesc;
2336
+ continue;
2337
+ }
2338
+ // Check if ViewBox
2339
+ const tryViewBox = this.tryViewBox(def);
2340
+ if (typeof tryViewBox === "string")
2341
+ errs.push(tryViewBox);
2342
+ else if (tryViewBox instanceof SvgBBox) {
2343
+ symbol.viewBox = tryViewBox;
2344
+ }
2345
+ else {
2346
+ // Assume it is some drawing item
2347
+ const item = this.resolveAsDrawingItem(def, allowedTypes);
2348
+ if (typeof item === "string")
2349
+ errs.push(item);
2350
+ else
2351
+ symbol.addElement(item);
2352
+ }
2353
+ }
2354
+ this.defines.push(symbol); // Add to Definable
2355
+ this.symbols.push(symbol);
2356
+ // Return the errors when resolving
2357
+ return errs;
2358
+ }
2359
+ /**
2360
+ * A tool function to convert the user-defined radius-/scale-function into Function
2361
+ * and check if the function could produce valid result, which should be positive number.
2362
+ * @param funcDef The definition of the function, e.g. 'x => (x + 2)*1.5
2363
+ * @param paramCount How many inputs the function will take, normally, 1 or 2
2364
+ * @returns The function or undefined
2365
+ */
2366
+ testUserFunc(funcDef, paramCount) {
2367
+ try {
2368
+ const tryFunc = eval(funcDef);
2369
+ if (!tryFunc || typeof tryFunc !== "function")
2370
+ return undefined;
2371
+ if (paramCount === 1) {
2372
+ let x = tryFunc(1);
2373
+ // Return the function if no error
2374
+ if (typeof x === "number" && x > 0)
2375
+ return tryFunc;
2376
+ }
2377
+ else if (paramCount === 2) {
2378
+ let x = tryFunc(1, 2);
2379
+ // Return the function if no error
2380
+ if (typeof x === "number" && x > 0)
2381
+ return tryFunc;
2382
+ }
2383
+ // NOTE: For paramCount greater than 2, no definition, will be filtered out
2384
+ }
2385
+ catch (error) {
2386
+ return undefined;
2387
+ }
2388
+ return undefined;
2389
+ }
2390
+ /**
2391
+ * Resolve Array definition, the input might be a string, or the object describing the array
2392
+ * @param name Name of the Array
2393
+ * @param value Presentation to resolve the array, shortcut string, or the object
2394
+ * @returns Error message
2395
+ */
2396
+ resolveArray(name, value) {
2397
+ // Check Name
2398
+ const nameError = this.isDrawingNameValid(name, "Block"); // Symbol array actually some group/block
2399
+ if (nameError)
2400
+ return [nameError];
2401
+ const parseRes = parseStrIntoKeyValuePair(value, HuatuSymbolArray.optsSymbolArray);
2402
+ const param = parseRes.body;
2403
+ let symbol;
2404
+ const realOrphans = [];
2405
+ if (parseRes.failedPairs.length > 0) {
2406
+ const pairs = parseRes.failedPairs.map(x => `[${x.key}: ${x.value}]`).join(",");
2407
+ this.warnings.push(`${pairs} are not valid to design a Symbol Array!`);
2408
+ }
2409
+ if (parseRes.unluckyOrphans.length > 0) {
2410
+ this.warnings.push(`${parseRes.unluckyOrphans.map(x => `'${x}`).join(",")} might be repeating to build a Symbol Array!`);
2411
+ }
2412
+ parseRes.orphans.forEach(orphan => {
2413
+ const lcOrphan = orphan.toLowerCase();
2414
+ // Try if a symbol
2415
+ const sym = this.symbols.find(x => x.name === orphan);
2416
+ if (sym) {
2417
+ if (!symbol)
2418
+ symbol = sym;
2419
+ else
2420
+ this.warnings.push(`[Symbol Array resolving]: Symbol already set, '${orphan}' will be discarded!`);
2421
+ }
2422
+ // Try if a Point
2423
+ else if (this.resolvePointFromExpr(orphan)) {
2424
+ if (!param.startPoint)
2425
+ param.startPoint = orphan;
2426
+ else if (!param.center)
2427
+ param.center = orphan;
2428
+ else
2429
+ realOrphans.push(orphan);
2430
+ }
2431
+ // Try if method in Upcase
2432
+ else if (!param.method && HuatuSymbolArray.METHODS.includes(lcOrphan)) {
2433
+ param.method = lcOrphan;
2434
+ }
2435
+ else {
2436
+ realOrphans.push(orphan);
2437
+ }
2438
+ });
2439
+ if (!param.method)
2440
+ return [`Arraying Method should be specified to build a Symbol Array!`];
2441
+ if (!param.symbol && !symbol)
2442
+ return [`Symbol should be specified when building a Symbol Array!`];
2443
+ if (param.symbol) {
2444
+ const sym = this.symbols.find(x => x.name === param.symbol);
2445
+ if (sym)
2446
+ symbol = sym;
2447
+ else if (!symbol)
2448
+ return [`'${param.symbol}' is not a valid Symbol!`];
2449
+ }
2450
+ if (realOrphans.length > 0) {
2451
+ this.warnings.push(`${realOrphans.map(x => `'${x}'`).join(",")} are not valid to build a Symbol Array!`);
2452
+ }
2453
+ // Now assign the params to build an array
2454
+ const pStr = param.startPoint || param.center;
2455
+ if (!pStr)
2456
+ return [`For Symbol Array, Start/Center Point must be specified!`];
2457
+ const point = this.resolvePointFromExpr(pStr);
2458
+ if (!point)
2459
+ return [`Start Point '${pStr}' could not be resolved into a point!`];
2460
+ let array = new HuatuSymbolArray(name, symbol, param.desc);
2461
+ if (param.alignCenter)
2462
+ array.alignCenter = param.alignCenter;
2463
+ let count = 1;
2464
+ if (param.count)
2465
+ count = param.count;
2466
+ if (param.method === "line" || param.method === "2d") {
2467
+ let dx, dy, rotDeg;
2468
+ if (param.deltaX) {
2469
+ let tryVar = this.resolveVarExpr(param.deltaX);
2470
+ if (tryVar)
2471
+ dx = tryVar;
2472
+ }
2473
+ if (param.deltaY) {
2474
+ let tryVar = this.resolveVarExpr(param.deltaY);
2475
+ if (tryVar)
2476
+ dy = tryVar;
2477
+ }
2478
+ if (param.rotDeg) {
2479
+ let deg = this.resolveVarExpr(param.rotDeg);
2480
+ if (deg)
2481
+ rotDeg = deg;
2482
+ }
2483
+ if (param.method === "line") {
2484
+ let scale;
2485
+ if (param.scale) {
2486
+ const tryFunc = this.testUserFunc(param.scale, 1);
2487
+ if (tryFunc)
2488
+ scale = tryFunc;
2489
+ else {
2490
+ const tryNum = this.resolveVarExpr(param.scale);
2491
+ if (tryNum)
2492
+ scale = tryNum;
2493
+ else
2494
+ return [`The Scale '${param.scale}' cannot be resolved!`];
2495
+ }
2496
+ }
2497
+ array.configLine(point, count, dx, dy, scale, rotDeg);
2498
+ }
2499
+ else {
2500
+ let countX = 1, countY = 1;
2501
+ if (param.countX)
2502
+ countX = param.countX;
2503
+ if (param.countY)
2504
+ countY = param.countY;
2505
+ let scale;
2506
+ if (param.scale) {
2507
+ const tryFunc = this.testUserFunc(param.scale, 2);
2508
+ if (tryFunc)
2509
+ scale = tryFunc;
2510
+ else {
2511
+ const tryNum = this.resolveVarExpr(param.scale);
2512
+ if (tryNum)
2513
+ scale = tryNum;
2514
+ else
2515
+ return [`The Scale '${param.scale}' cannot be resolved!`];
2516
+ }
2517
+ }
2518
+ array.config2D(point, countX, countY, dx, dy, scale, rotDeg);
2519
+ }
2520
+ }
2521
+ else { // This is polar
2522
+ let radius, scale, startDeg, deltaDeg;
2523
+ if (param.radius) {
2524
+ const tryFunc = this.testUserFunc(param.radius, 1);
2525
+ if (tryFunc)
2526
+ radius = tryFunc;
2527
+ else {
2528
+ const tryNum = this.resolveVarExpr(param.radius);
2529
+ if (tryNum)
2530
+ radius = tryNum;
2531
+ else
2532
+ return [`The Radius '${param.radius}' cannot be resolved!`];
2533
+ }
2534
+ }
2535
+ else {
2536
+ radius = symbol.bbox.h * 10; // Make the default radius if not specified
2537
+ this.warnings.push(`No Radius specified for Symbol Array, will use default, which is 10x Symbol BBOX height!`);
2538
+ }
2539
+ if (param.scale) {
2540
+ const tryFunc = this.testUserFunc(param.scale, 1);
2541
+ if (tryFunc)
2542
+ scale = tryFunc;
2543
+ else {
2544
+ const tryNum = this.resolveVarExpr(param.scale);
2545
+ if (tryNum && tryNum > 0)
2546
+ scale = tryNum;
2547
+ else
2548
+ return [`Scale definition '${param.scale}' resolves to no result!`];
2549
+ }
2550
+ }
2551
+ if (param.startDeg) {
2552
+ let tryDeg = this.resolveVarExpr(param.startDeg);
2553
+ if (tryDeg)
2554
+ startDeg = tryDeg;
2555
+ }
2556
+ if (param.deltaDeg) {
2557
+ let tryDeg = this.resolveVarExpr(param.deltaDeg);
2558
+ if (tryDeg)
2559
+ deltaDeg = tryDeg;
2560
+ }
2561
+ array.configPolar(count, point, radius, scale, startDeg, deltaDeg, param.notRotate);
2562
+ }
2563
+ this.arrays.push(array);
2564
+ return [];
2565
+ }
2566
+ /**
2567
+ * Resolve Clip Path definition, the input might be a string, or a string list
2568
+ * @param name Name of the Clip Path
2569
+ * @param value Presentation to resolve the clip path
2570
+ * @returns Error message
2571
+ */
2572
+ resolveClip(name, value) {
2573
+ // Check Name
2574
+ if (this.clips.find(c => c.name === name))
2575
+ return [`Clip '${name}' has already been defined, please choose another name!`];
2576
+ const resolveClipPath = (def) => {
2577
+ const matchClipRule = def.match(/^(clip-)?rule[\s]*[,;\s][\s]*(nonzero|evenodd)$/i);
2578
+ if (matchClipRule)
2579
+ return matchClipRule[2];
2580
+ const tryPath = this.paths.find(p => p.name === def);
2581
+ if (tryPath && tryPath.model)
2582
+ return [tryPath.model];
2583
+ const tryPathSeries = this.pathSeries.find(ps => ps.name === def);
2584
+ if (tryPathSeries && tryPathSeries.models.length > 0)
2585
+ return tryPathSeries.models;
2586
+ const resolvedShape = this.resolveShapeDef(def);
2587
+ if (typeof resolvedShape === "string")
2588
+ return resolvedShape;
2589
+ else
2590
+ return [resolvedShape];
2591
+ };
2592
+ const defs = typeof value === "string"
2593
+ ? [value]
2594
+ : value;
2595
+ const errs = [];
2596
+ const models = [];
2597
+ let clipRule;
2598
+ defs.map(d => resolveClipPath(d.trim()))
2599
+ .forEach(x => {
2600
+ if (typeof x === "string") {
2601
+ let xlc = x.toLowerCase();
2602
+ if (xlc === "nonzero" || xlc === "evenodd") {
2603
+ clipRule = xlc;
2604
+ }
2605
+ else
2606
+ errs.push(x);
2607
+ }
2608
+ else
2609
+ models.push(...x);
2610
+ });
2611
+ if (models.length > 0) {
2612
+ const paths = models.map(m => HuatuPath.model2Path(m))
2613
+ .filter(p => p !== undefined);
2614
+ if (paths.length !== models.length)
2615
+ this.warnings.push(`Some Model to path conversion failed when resolving clip path '${name}'!`);
2616
+ if (paths.length < 1)
2617
+ errs.push(`No Path resolved for clip '${name}'!`);
2618
+ const clip = new HuatuClipPath(name, paths);
2619
+ if (clipRule)
2620
+ clip.clipRule = clipRule;
2621
+ this.defines.push(clip);
2622
+ this.clips.push(clip);
2623
+ }
2624
+ // Return the errors when resolving
2625
+ return errs;
2626
+ }
2627
+ /**
2628
+ * Resolve Pattern definition, the input might be an object, describe how the pattern is designed
2629
+ * @param name Name of the Pattern
2630
+ * @param value Presentation to resolve the pattern
2631
+ * @returns Error message
2632
+ */
2633
+ resolvePattern(name, value) {
2634
+ // Check name duplicating
2635
+ if (this.patterns.find(p => p.name === name))
2636
+ return [`Pattern '${name}' has already been defined!`];
2637
+ const opts = {
2638
+ keys: [
2639
+ ["type"],
2640
+ ["lineDensity", "density", "dotsDensity", "squareDensity"],
2641
+ ["rotDeg", "deg", "lineDeg"],
2642
+ ["lineWeight", "lineWidth"],
2643
+ ["lineStyle", "style"],
2644
+ ["dashDensity", "dashedDensity", "dottedDensity"],
2645
+ ["size"],
2646
+ ["color"],
2647
+ ["opacity"],
2648
+ ["bgColor", "background"],
2649
+ ["bgOpacity"],
2650
+ ["sparseK"]
2651
+ ],
2652
+ allowAppend: [],
2653
+ booleans: [],
2654
+ numbers: [
2655
+ "lineDensity", "density", "dotsDensity", "squareDensity",
2656
+ "rotDeg", "deg", "lineDeg",
2657
+ "lineWeight", "lineWidth",
2658
+ "dashDensity", "dashedDensity", "dottedDensity",
2659
+ "size",
2660
+ "opacity",
2661
+ "bgOpacity",
2662
+ "sparseK",
2663
+ ],
2664
+ ints: [],
2665
+ qualifiers: {
2666
+ "color": isValidColor,
2667
+ "bgColor": isValidColor,
2668
+ "background": isValidColor,
2669
+ },
2670
+ blindGuess: {
2671
+ "lineStyle": ["solid", "dashed", "dotted"],
2672
+ "color": isValidColor,
2673
+ "type": HuatuPattern.getAllPatternTypes(),
2674
+ }
2675
+ };
2676
+ const ptnParams = {};
2677
+ if (typeof value === "string") {
2678
+ // If the definition is presented in a string
2679
+ const res = parseStrIntoKeyValuePair(value, opts);
2680
+ assignOnlyDefined(ptnParams, res.body);
2681
+ if (res.orphans.length > 0) {
2682
+ this.warnings.push(`Segments [${res.orphans.map(x => `'${x}'`).join(",")}] were not able to be resolved when making a Pattern!`);
2683
+ }
2684
+ if (res.unluckyOrphans.length > 0) {
2685
+ this.warnings.push(`[${res.unluckyOrphans.map(x => `'${x}'`).join(",")}] were failed to set to it's owner key in default, maybe repeated assigning!`);
2686
+ }
2687
+ }
2688
+ else {
2689
+ // If the definition is in object form
2690
+ const res = parseStrIntoKeyValuePair(value, opts);
2691
+ Object.assign(ptnParams, res.body);
2692
+ if (res.failedPairs.length > 0) {
2693
+ const warnings = res.failedPairs.map((x, i) => `${i + 1}) '${x.key}: ${x.value}'`).join("\n");
2694
+ this.warnings.push(`Parameters failed when making a pattern: \n${warnings}`);
2695
+ }
2696
+ }
2697
+ const ptn = HuatuPattern.buildPatternFromParams(ptnParams);
2698
+ if (typeof ptn === "string")
2699
+ return [ptn];
2700
+ this.patterns.push(new HuatuPattern(name, ptn));
2701
+ return [];
2702
+ }
2703
+ /**
2704
+ * Resolve Marker definition, the input might be an object, describe how the marker is designed
2705
+ * @param name Name of the Marker
2706
+ * @param value Presentation to resolve the Marker
2707
+ * @returns Error message
2708
+ */
2709
+ resolveMarker(name, value) {
2710
+ // Check name duplicating
2711
+ if (this.markers.find(p => p.name === name))
2712
+ return [`Marker '${name}' has already been defined!`];
2713
+ const opts = {
2714
+ keys: [
2715
+ ["type"],
2716
+ ["size"],
2717
+ ["width", "w"],
2718
+ ["height", "h"],
2719
+ ["r"],
2720
+ ["rx"],
2721
+ ["ry"],
2722
+ ["n"],
2723
+ ["direction"],
2724
+ ["centerEmpty"],
2725
+ ["rIn"],
2726
+ ["rOut"],
2727
+ ["degOffset", "offset"],
2728
+ ["degOpen", "angOpen"],
2729
+ ["len", "length"],
2730
+ ["k"],
2731
+ ["closeArrow"],
2732
+ ["makeSmooth", "smooth"],
2733
+ ["primaryColor", "priColor", "primeColor", "color"],
2734
+ ["secondaryColor", "secColor"],
2735
+ ["fillColor", "fill"],
2736
+ ["fillOpacity", "opacity"],
2737
+ ["strokeColor", "stroke"],
2738
+ ["strokeWidth"],
2739
+ ],
2740
+ allowAppend: [],
2741
+ booleans: ["centerEmpty", "closeArrow", "makeSmooth", "smooth"],
2742
+ numbers: [
2743
+ "fillOpacity", "opacity",
2744
+ "r", "rx", "ry", "rIn", "rOut",
2745
+ "width", "height", "size",
2746
+ "degOffset", "offset",
2747
+ "degOpen", "angOpen",
2748
+ "len", "length",
2749
+ "k",
2750
+ "fillOpacity", "strokeWidth",
2751
+ ],
2752
+ ints: ["n"],
2753
+ qualifiers: {
2754
+ "direction": ["up", "down", "left", "right"],
2755
+ "color": isValidColor,
2756
+ "fillColor": isValidColor,
2757
+ "stroke": isValidColor,
2758
+ "strokeColor": isValidColor,
2759
+ "primaryColor": isValidColor,
2760
+ "priColor": isValidColor,
2761
+ "primeColor": isValidColor,
2762
+ "secondaryColor": isValidColor,
2763
+ "secColor": isValidColor,
2764
+ },
2765
+ blindGuess: {
2766
+ "direction": ["up", "down", "left", "right"],
2767
+ "type": HuatuMarker.getAllMarkerTypeWords(),
2768
+ "primaryColor": isValidColor,
2769
+ }
2770
+ };
2771
+ const mkrParams = {};
2772
+ if (typeof value === "string") {
2773
+ // If the definition is presented in a string
2774
+ const res = parseStrIntoKeyValuePair(value, opts);
2775
+ Object.assign(mkrParams, res.body);
2776
+ if (res.orphans.length > 0) {
2777
+ this.warnings.push(`Segments [${res.orphans.map(x => `'${x}'`).join(",")}] were not able to be resolved when making a Marker!`);
2778
+ }
2779
+ if (res.unluckyOrphans.length > 0) {
2780
+ this.warnings.push(`[${res.unluckyOrphans.map(x => `'${x}'`).join(",")}] were failed to set to it's owner key in default, maybe repeated assigning!`);
2781
+ }
2782
+ }
2783
+ else {
2784
+ // If the definition is in object form
2785
+ const res = parseStrIntoKeyValuePair(value, opts);
2786
+ Object.assign(mkrParams, res.body);
2787
+ if (res.failedPairs.length > 0) {
2788
+ const warnings = res.failedPairs.map((x, i) => `${i + 1}) '${x.key}: ${x.value}'`).join("\n");
2789
+ this.warnings.push(`Parameters failed when making a marker: \n${warnings}`);
2790
+ }
2791
+ }
2792
+ const mkr = HuatuMarker.buildMarkerFromParams(name, mkrParams);
2793
+ if (typeof mkr === "string")
2794
+ return [mkr];
2795
+ const huatuMkr = new HuatuMarker(name, mkr);
2796
+ huatuMkr.primaryColor = mkrParams.primaryColor;
2797
+ huatuMkr.secondaryColor = mkrParams.secondaryColor;
2798
+ this.markers.push(huatuMkr);
2799
+ return [];
2800
+ }
2801
+ parseLabelDef(format, text) {
2802
+ // Label Format should be: label; [format] ||| [text]
2803
+ // For the format part, it should be like
2804
+ // (location | w); w | h; at; (location); rot; 35; (anchor)pos; center; align; center; [anchor]offset; font; lishu; bold; italic; addline; overline;
2805
+ const optsLabel = {
2806
+ keys: [
2807
+ ["refs", "styles", "style", "ref"],
2808
+ ["location", "at", "position", "pos", "loc"],
2809
+ ["offset"],
2810
+ ["anchor"],
2811
+ ["width", "w"],
2812
+ ["height", "h"],
2813
+ ["textAlign", "align"],
2814
+ ["bold"],
2815
+ ["italic"],
2816
+ ["addLine", "line"],
2817
+ ["lineHeight", "lnh"],
2818
+ ["fontName", "font"],
2819
+ ["color", "fontColor"],
2820
+ ["fontSize", "size", "fs"],
2821
+ ["rotDeg", "deg"],
2822
+ ["bgColor", "background", "bg"],
2823
+ ["borderColor", "bdrColor", "bdr"],
2824
+ ],
2825
+ booleans: ["bold", "italic"],
2826
+ ints: [],
2827
+ numbers: [
2828
+ "fontSize", "size", "fs",
2829
+ "rotDeg", "deg",
2830
+ "lineHeight", "lnh"
2831
+ ],
2832
+ allowAppend: ["refs", "styles", "style", "ref"],
2833
+ qualifiers: {
2834
+ "color": isValidColor,
2835
+ "fontColor": isValidColor,
2836
+ "anchor": HuatuLabel.ANCHOR_POSITIONS,
2837
+ "textAlign": HuatuLabelStyle.TEXT_ALIGNS,
2838
+ "align": HuatuLabelStyle.TEXT_ALIGNS,
2839
+ "addLine": HuatuLabelStyle.DECORATION_LINES,
2840
+ "bgColor": isValidColor,
2841
+ "background": isValidColor,
2842
+ "bg": isValidColor,
2843
+ "borderColor": isValidColor,
2844
+ "bdrColor": isValidColor,
2845
+ "bdr": isValidColor,
2846
+ },
2847
+ blindGuess: {
2848
+ "textAlign": HuatuLabelStyle.TEXT_ALIGNS,
2849
+ "anchor": HuatuLabel.ANCHOR_POSITIONS,
2850
+ "addLine": [
2851
+ "over", "overline",
2852
+ "under", "underline",
2853
+ "middleline", "through", "strike-through", "strikethrough"
2854
+ ],
2855
+ "fontName": HuatuLabelStyle.PREDEFINED_FONT_NAMES,
2856
+ "color": isValidColor,
2857
+ "bgColor": isValidColor,
2858
+ }
2859
+ };
2860
+ const res = parseStrIntoKeyValuePair(format, optsLabel);
2861
+ const resBody = res.body;
2862
+ // Handle Unlucky orphans
2863
+ res.unluckyOrphans.forEach(orphan => {
2864
+ // To see if it is the keyword 'Center'
2865
+ if (orphan === "center") {
2866
+ if (resBody.textAlign === "center" && !resBody.anchor) {
2867
+ resBody.anchor = "center";
2868
+ }
2869
+ else {
2870
+ this.warnings.push(`[Label Resolving]: '${orphan}' could not be used, maybe repeating.`);
2871
+ }
2872
+ }
2873
+ else {
2874
+ // Throw to orphans to reprocess
2875
+ res.orphans.push(orphan);
2876
+ }
2877
+ });
2878
+ // Re-check Orphans
2879
+ const realOrphans = [];
2880
+ res.orphans.forEach(orphan => {
2881
+ let orphanProcessed = false;
2882
+ // To see if it font size definition
2883
+ let fsStr = /^([1-9]\d*)p[xt]$/i.exec(orphan);
2884
+ if (fsStr && !resBody.fontSize) {
2885
+ resBody.fontSize = parseFloat(fsStr[1]);
2886
+ orphanProcessed = true;
2887
+ }
2888
+ // To see if it is some label style
2889
+ if (!orphanProcessed) {
2890
+ let sty = this.labelStyles.find(s => s.name === orphan);
2891
+ if (sty) {
2892
+ if (resBody.refs)
2893
+ resBody.refs += `, ${orphan}`;
2894
+ else
2895
+ resBody.refs = orphan;
2896
+ orphanProcessed = true;
2897
+ }
2898
+ }
2899
+ // To see if it is some Point-Like
2900
+ if (!orphanProcessed) {
2901
+ let p = this.resolvePointFromExpr(orphan);
2902
+ if (p) {
2903
+ if (!resBody.location)
2904
+ resBody.location = orphan;
2905
+ else if (!resBody.offset)
2906
+ resBody.offset = orphan;
2907
+ else
2908
+ this.warnings.push(`[Label Resolving]: Too many point assigning '${orphan}'!`);
2909
+ orphanProcessed = true;
2910
+ }
2911
+ }
2912
+ // Throw to Orphan if it is not processed
2913
+ if (!orphanProcessed) {
2914
+ realOrphans.push(orphan);
2915
+ }
2916
+ });
2917
+ // Add warning about Orphans
2918
+ if (realOrphans.length > 0) {
2919
+ const orphans = realOrphans.map(x => `'${x}'`).join(", ");
2920
+ this.warnings.push(`[Label Resolving]: segments ${orphans} could not be resolved!`);
2921
+ }
2922
+ let width = resBody.width ? this.resolveVarExpr(resBody.width) : undefined;
2923
+ let height = resBody.height ? this.resolveVarExpr(resBody.height) : undefined;
2924
+ const label = new HuatuLabel(text, width, height);
2925
+ if (resBody.location) {
2926
+ let p = this.resolvePointFromExpr(resBody.location);
2927
+ if (p)
2928
+ label.location = p;
2929
+ else {
2930
+ this.warnings.push(`Label Location '${resBody.location}' cannot resolve to some point, Will use (0, 0)`);
2931
+ }
2932
+ }
2933
+ if (resBody.offset) {
2934
+ let p = this.resolvePointFromExpr(resBody.offset);
2935
+ if (p)
2936
+ label.anchorOffset = p;
2937
+ }
2938
+ if (resBody.refs) {
2939
+ resBody.refs.split(/[,\s]/)
2940
+ .map(x => x.trim())
2941
+ .filter(x => !!x)
2942
+ .forEach(x => {
2943
+ const sty = this.labelStyles.find(s => s.name === x);
2944
+ if (sty) {
2945
+ if (!label.styles.find(s => s === sty)) {
2946
+ label.styles.push(sty);
2947
+ }
2948
+ else {
2949
+ this.warnings.push(`Style '${x}' has been already added to the label!`);
2950
+ }
2951
+ }
2952
+ else {
2953
+ this.warnings.push(`'${x}' is not valid defined Label Style!`);
2954
+ }
2955
+ });
2956
+ }
2957
+ if (resBody.anchor)
2958
+ label.setAnchor(resBody.anchor);
2959
+ if (resBody.textAlign)
2960
+ label.textAlign = resBody.textAlign;
2961
+ if (resBody.bold)
2962
+ label.bold = resBody.bold;
2963
+ if (resBody.italic)
2964
+ label.italic = resBody.italic;
2965
+ if (resBody.addLine)
2966
+ label.addLine = resBody.addLine;
2967
+ if (resBody.fontName)
2968
+ label.font = resBody.fontName;
2969
+ if (resBody.color)
2970
+ label.color = resBody.color;
2971
+ if (resBody.fontSize)
2972
+ label.fontSize = resBody.fontSize;
2973
+ if (resBody.lineHeight)
2974
+ label.lineHeight = resBody.lineHeight;
2975
+ if (resBody.rotDeg)
2976
+ label.rotDeg = resBody.rotDeg;
2977
+ if (resBody.bgColor)
2978
+ label.bgColor = resBody.bgColor;
2979
+ if (resBody.borderColor)
2980
+ label.borderColor = resBody.borderColor;
2981
+ return label;
2982
+ }
2983
+ parseStyleAndTransformString(def) {
2984
+ const parseRes = parseStrIntoKeyValuePair(def, HuatuDrawing.optsDrawingStyleAndTransform);
2985
+ const params = parseRes.body;
2986
+ // Transform
2987
+ const transform = new HuatuTransform();
2988
+ if (params.at) {
2989
+ const p = this.resolvePointFromExpr(params.at);
2990
+ if (p) {
2991
+ transform.rotCx = p.x;
2992
+ transform.rotCy = p.y;
2993
+ }
2994
+ else {
2995
+ this.warnings.push(`'${params.at}' cannot be resolved to a point!`);
2996
+ }
2997
+ }
2998
+ if (params.move) {
2999
+ const p = this.resolvePointFromExpr(params.move);
3000
+ if (p) {
3001
+ transform.x = p.x;
3002
+ transform.y = p.y;
3003
+ }
3004
+ else {
3005
+ this.warnings.push(`'${params.move}' cannot be resolved to a point!`);
3006
+ }
3007
+ }
3008
+ if (params.rotDeg)
3009
+ transform.rotDeg = this.resolveVarExpr(params.rotDeg);
3010
+ if (params.scale) {
3011
+ if (params.scale > 0)
3012
+ transform.scale = params.scale;
3013
+ else
3014
+ this.warnings.push(`Value of Scale could not be negative!`);
3015
+ }
3016
+ const style = this.parseStyleFromKeyValueResults(parseRes);
3017
+ let smooth;
3018
+ if (parseRes.subKeys && parseRes.subKeys["smooth"]) {
3019
+ let smoothVar = parseRes.subKeys["smooth"].body;
3020
+ smooth = {
3021
+ mode: smoothVar.mode,
3022
+ factor: smoothVar.factor,
3023
+ };
3024
+ // Make lineJoin round if not set
3025
+ if (!style.strokeLineJoin) {
3026
+ style.strokeLineJoin = "round";
3027
+ }
3028
+ }
3029
+ return {
3030
+ style,
3031
+ transform,
3032
+ alignCenter: params.alignCenter,
3033
+ smooth,
3034
+ radius: params.radius,
3035
+ };
3036
+ }
3037
+ /**
3038
+ * Resolve the "modelled" drawing by definition
3039
+ * @param def The definition of the drawing of a shape, It might have two formats:
3040
+ * - Ad-Hoc Shape definitions and style/transform settings
3041
+ * - Referring a defined (named) module, and specifying style/transform
3042
+ * @param allowedTypes The allowed sets of module that this function call will search, it is designed in the way that
3043
+ * Different Drawing Group will accept different types of modules *
3044
+ * @returns The resolved Drawing or error message
3045
+ */
3046
+ resolveDrawingOfShapes(def, allowedTypes) {
3047
+ // let format, style, transform, alignCenter;
3048
+ const tryShapeAndDrawing = WtHuatu.REGEX_SHAPE_AND_DRAWING_DEF.exec(def);
3049
+ if (tryShapeAndDrawing) {
3050
+ const shape = this.resolveShapeDef(tryShapeAndDrawing[1]);
3051
+ if (typeof shape === "string")
3052
+ return shape;
3053
+ const fmtRes = this.parseStyleAndTransformString(tryShapeAndDrawing[2]);
3054
+ let path = HuatuPath.model2Path(shape);
3055
+ if (path instanceof PathPolyline && fmtRes.smooth) {
3056
+ path = path.toSmoothed(fmtRes.smooth.factor, fmtRes.smooth.mode);
3057
+ }
3058
+ else if (fmtRes.radius &&
3059
+ (path instanceof PathPolyline ||
3060
+ path instanceof PathPolygon ||
3061
+ path instanceof PathTriangle ||
3062
+ path instanceof PathRect)) {
3063
+ path = path.toAngleArced(fmtRes.radius);
3064
+ }
3065
+ const htPath = new HuatuPath(path);
3066
+ htPath.setModel(shape);
3067
+ return new HuatuDrawing(htPath, fmtRes.style, fmtRes.transform);
3068
+ }
3069
+ // If it is not a ad-hoc shape (and thus path) define, then it should refer some other defined modules
3070
+ // And the first parameter is the name
3071
+ let name, nameIdx = undefined, fmtStr;
3072
+ const scPos = def.indexOf(";");
3073
+ if (scPos < 0) {
3074
+ name = def.trim();
3075
+ fmtStr = undefined;
3076
+ }
3077
+ else {
3078
+ name = def.substring(0, scPos).trim();
3079
+ fmtStr = def.substring(scPos + 1);
3080
+ }
3081
+ if (!name) {
3082
+ return `A module (name) to draw needs to be specified!`;
3083
+ }
3084
+ // Check if it is referring some point in a Point Series
3085
+ let originalName = name;
3086
+ const match = name.match(WT_REGEX.VAR_WITH_INDEXING);
3087
+ if (match) {
3088
+ name = match[1];
3089
+ nameIdx = parseInt(match[3] || match[4]);
3090
+ }
3091
+ // Check if the item is PointSeries, if so, it will be converted to PathSeries of circles
3092
+ const ps = this.pointSeries.find(x => x.name === name);
3093
+ if (ps) {
3094
+ let psR;
3095
+ const psSty = new HuatuStyle();
3096
+ if (fmtStr) {
3097
+ const parseRes = parseStrIntoKeyValuePair(fmtStr, HuatuPointSeries.optsPointSeriesDrawing);
3098
+ const params = parseRes.body;
3099
+ if (params.size) {
3100
+ let rEval = this.resolveVarExpr(params.size);
3101
+ if (!rEval || rEval < 0) {
3102
+ this.warnings.push(`Size/Radius (${params.size}) when drawing Point Series not valid, will use default (1)!`);
3103
+ }
3104
+ else {
3105
+ psR = rEval;
3106
+ }
3107
+ }
3108
+ const subFill = parseRes.subKeys ? parseRes.subKeys["fill"] : undefined;
3109
+ const subStroke = parseRes.subKeys ? parseRes.subKeys["stroke"] : undefined;
3110
+ if (params.fillColor)
3111
+ psSty.fillColor = params.fillColor;
3112
+ else if (subFill && subFill.body["color"])
3113
+ psSty.fillColor = subFill.body["color"];
3114
+ if (params.fillOpacity !== undefined && WtMath.isIn0and1(params.fillOpacity))
3115
+ psSty.fillOpacity = params.fillOpacity;
3116
+ else if (subFill &&
3117
+ subFill.body["opacity"] !== undefined &&
3118
+ WtMath.isIn0and1(subFill.body["opacity"]))
3119
+ psSty.fillOpacity = subFill.body["opacity"];
3120
+ if (params.strokeColor)
3121
+ psSty.strokeColor = params.strokeColor;
3122
+ else if (subStroke && subStroke.body["color"])
3123
+ psSty.strokeColor = subStroke.body["color"];
3124
+ if (params.strokeWidth)
3125
+ psSty.strokeWidth = params.strokeWidth;
3126
+ else if (subStroke && subStroke.body["width"])
3127
+ psSty.strokeWidth = subStroke.body["width"];
3128
+ if (params.strokeOpacity !== undefined && WtMath.isIn0and1(params.strokeOpacity))
3129
+ psSty.fillOpacity = params.fillOpacity;
3130
+ else if (subStroke &&
3131
+ subStroke.body["opacity"] !== undefined &&
3132
+ WtMath.isIn0and1(subStroke.body["opacity"]))
3133
+ psSty.strokeOpacity = subStroke.body["opacity"];
3134
+ if (params.dashArray)
3135
+ psSty.strokeDashArray = params.dashArray;
3136
+ else if (subStroke && subStroke.body["dashArray"])
3137
+ psSty.strokeDashArray = subStroke.body["dashArray"];
3138
+ let notUsed = [];
3139
+ if (parseRes.orphans.length > 0) {
3140
+ notUsed = parseRes.orphans.map(x => `"${x}"`);
3141
+ }
3142
+ if (parseRes.failedPairs.length > 0) {
3143
+ notUsed.push(...parseRes.failedPairs.map(x => `"[${x.key}]:${x.value}"`));
3144
+ }
3145
+ if (subFill && subFill.orphans) {
3146
+ notUsed.push(...subFill.orphans.map(x => `"fill:${x}"`));
3147
+ }
3148
+ if (subStroke && subStroke.orphans) {
3149
+ notUsed.push(...subStroke.orphans.map(x => `"fill:${x}"`));
3150
+ }
3151
+ if (notUsed.length > 0) {
3152
+ this.warnings.push(`Parameters [${notUsed.join(",")}] not used for PointSeries drawing`);
3153
+ }
3154
+ }
3155
+ if (nameIdx !== undefined && (nameIdx < 1 || nameIdx > ps.points.length))
3156
+ return `Index out of range when drawing one point from PointSeries (${originalName})`;
3157
+ const circles = (nameIdx ? [ps.points[nameIdx - 1]] : ps.points).map(p => new Circle(p.x, p.y, psR || 1));
3158
+ const psDrawing = HuatuPathSeries.fromModels(circles);
3159
+ if (!psSty.fillColor)
3160
+ psSty.fillColor = "currentColor";
3161
+ return new HuatuDrawing(psDrawing, psSty);
3162
+ }
3163
+ // Shape/Path referring
3164
+ let fmtRes = fmtStr ? this.parseStyleAndTransformString(fmtStr) : undefined;
3165
+ let elemSources = [];
3166
+ if (allowedTypes.includes("shape") || allowedTypes.includes("path")) {
3167
+ elemSources.push(...this.paths);
3168
+ }
3169
+ if (allowedTypes.includes("shapeSeries") || allowedTypes.includes("pathSeries")) {
3170
+ elemSources.push(...this.pathSeries);
3171
+ }
3172
+ if (allowedTypes.includes("symbol")) {
3173
+ elemSources.push(...this.symbols);
3174
+ }
3175
+ if (allowedTypes.includes("array")) {
3176
+ elemSources.push(...this.arrays);
3177
+ }
3178
+ if (allowedTypes.includes("block")) {
3179
+ elemSources.push(...this.blocks);
3180
+ }
3181
+ if (allowedTypes.includes("layer")) {
3182
+ elemSources.push(...this.layers);
3183
+ }
3184
+ if (allowedTypes.includes("component")) {
3185
+ elemSources.push(...this.compPlacements);
3186
+ }
3187
+ const element = elemSources.find(x => x.name === name);
3188
+ if (element) {
3189
+ if (element.refCount)
3190
+ element.refCount++;
3191
+ let eleFound = element;
3192
+ if (nameIdx !== undefined) {
3193
+ if (!(element instanceof HuatuPathSeries))
3194
+ return `Draw (${originalName}) failed, because ${name} is not ShapeSeries`;
3195
+ if (nameIdx < 1 || nameIdx > element.paths.length)
3196
+ return `Index out of range when drawing one path from PathSeries (${originalName})`;
3197
+ eleFound = new HuatuPath(element.paths[nameIdx - 1]);
3198
+ }
3199
+ if (fmtRes && element instanceof HuatuPath && element.path) {
3200
+ if (fmtRes.smooth && element.path instanceof PathPolyline) {
3201
+ // If Polyline and smooth, then reset the path to smoothed path
3202
+ eleFound = new HuatuPath(element.path.toSmoothed(fmtRes.smooth.factor, fmtRes.smooth.mode));
3203
+ }
3204
+ else if (fmtRes.radius &&
3205
+ (element.path instanceof PathPolygon ||
3206
+ element.path instanceof PathPolyline ||
3207
+ element.path instanceof PathRect ||
3208
+ element.path instanceof PathTriangle)) {
3209
+ // If radius specified, then make radius at angles
3210
+ eleFound = new HuatuPath(element.path.toAngleArced(fmtRes.radius));
3211
+ }
3212
+ }
3213
+ // Make component placement some drawing
3214
+ if (element instanceof HuatuComponentPlacement) {
3215
+ eleFound = new HuatuCompDrawing(element);
3216
+ // Remove Transform and AlignCenter settings for Component placement
3217
+ if (fmtRes) {
3218
+ const msgs = [];
3219
+ if (fmtRes.transform) {
3220
+ if (fmtRes.transform.rotCx || fmtRes.transform.rotCy || fmtRes.transform.rotDeg) {
3221
+ let msgDeg = fmtRes.transform.rotDeg ? ` ${fmtRes.transform.rotDeg.toFixed(2)} deg` : ``;
3222
+ let msgLoc = fmtRes.transform.rotCx || fmtRes.transform.rotCy
3223
+ ? ` at (${fmtRes.transform.rotCx ? fmtRes.transform.rotCx.toFixed(2) : 0}, ${fmtRes.transform.rotCy ? fmtRes.transform.rotCy.toFixed(2) : 0})`
3224
+ : ``;
3225
+ msgs.push(`Rotate${msgDeg}${msgLoc}`);
3226
+ }
3227
+ if (fmtRes.transform.scale) {
3228
+ msgs.push(`Scale(${fmtRes.transform.scale})`);
3229
+ }
3230
+ if (fmtRes.transform.x || fmtRes.transform.y) {
3231
+ msgs.push(`Move to (${fmtRes.transform.x ? fmtRes.transform.x.toFixed(2) : 0}, ${fmtRes.transform.y ? fmtRes.transform.y.toFixed(2) : 0})`);
3232
+ }
3233
+ fmtRes.transform = undefined;
3234
+ }
3235
+ if (fmtRes.alignCenter) {
3236
+ msgs.push("alignCenter");
3237
+ fmtRes.alignCenter = undefined;
3238
+ }
3239
+ if (msgs.length > 0) {
3240
+ this.warnings.push(`No Transform(scale/move/rotate) and alignCenter supported for a component placement, what you specified (${msgs.join(';')}) will be ignored!`);
3241
+ }
3242
+ }
3243
+ }
3244
+ return new HuatuDrawing(eleFound, fmtRes?.style, fmtRes?.transform, fmtRes?.alignCenter);
3245
+ }
3246
+ else {
3247
+ return `[${name}] is not found in defined modules!`;
3248
+ }
3249
+ }
3250
+ resolveWireTerminal(def) {
3251
+ if (!def)
3252
+ return undefined;
3253
+ // 1. Check if Component (placement) 's port
3254
+ const tryPlacementCall = /^([a-z][a-z0-9_]*)[.]([0-9a-zA-Z]+)$/i.exec(def);
3255
+ if (tryPlacementCall) {
3256
+ const plcName = tryPlacementCall[1];
3257
+ const portName = tryPlacementCall[2];
3258
+ const plc = this.compPlacements.find(x => x.name === plcName);
3259
+ if (plc) {
3260
+ const port = plc.portsSettled.find(p => p.name === portName);
3261
+ if (port) {
3262
+ return port;
3263
+ }
3264
+ else {
3265
+ this.warnings.push(`Port '${portName}' cannot found in Component Placement '${plcName}'`);
3266
+ }
3267
+ }
3268
+ }
3269
+ const tryPointExpr = this.resolvePointFromExpr(def);
3270
+ if (tryPointExpr)
3271
+ return tryPointExpr;
3272
+ // TODO: implement it
3273
+ // 2. Check if it is a point referencing some shape/path, etc, or purely some point definition
3274
+ return undefined;
3275
+ }
3276
+ parseWireDef(def) {
3277
+ const res = parseStrIntoKeyValuePair(def, HuatuWire.optsHuatuWire);
3278
+ const params = res.body;
3279
+ if (!params.from) {
3280
+ return `You must provide 'From' terminal to define a wire. '${def}' lacks some!`;
3281
+ }
3282
+ let net;
3283
+ if (!params.to) {
3284
+ if (params.net) {
3285
+ net = this.nets.find(x => x.name === params.net);
3286
+ if (!net) {
3287
+ return `No 'To' terminal and the net you refer (${params.net}) doesn't exist`;
3288
+ }
3289
+ }
3290
+ else {
3291
+ return `No 'To' terminal and no net defined for the wire '${def}'`;
3292
+ }
3293
+ }
3294
+ let errMsg = [];
3295
+ const termFrom = this.resolveWireTerminal(params.from);
3296
+ if (!termFrom) {
3297
+ errMsg.push(`Wire terminal 'From' (${params.from}) cannot resolve to a valid point!`);
3298
+ }
3299
+ let termTo;
3300
+ if (params.to) {
3301
+ termTo = this.resolveWireTerminal(params.to);
3302
+ if (!termTo) {
3303
+ errMsg.push(`Wire terminal 'To' (${params.to}) cannot resolve to a valid point!`);
3304
+ }
3305
+ }
3306
+ if (errMsg.length > 0)
3307
+ return errMsg.join("\n");
3308
+ const wire = new HuatuWire(termFrom, termTo, params.type);
3309
+ // Check other parameters of that wire
3310
+ if (params.color)
3311
+ wire.strokeColor = params.color;
3312
+ if (params.radius)
3313
+ wire.radius = params.radius;
3314
+ if (params.turn) {
3315
+ const turn = this.resolveVarExpr(params.turn);
3316
+ if (turn === undefined) {
3317
+ this.warnings.push(`Turn parameter '${params.turn}' of wire def is invalid, it cannot resolve to a number!`);
3318
+ }
3319
+ else {
3320
+ if (turn < 0 || turn > 1) {
3321
+ this.warnings.push(`Turn parameter should be in [0, 1], what you specified '${params.turn}' resolved to '${turn}'`);
3322
+ }
3323
+ wire.turn = turn;
3324
+ }
3325
+ }
3326
+ if (params.turnX) {
3327
+ const turnX = this.resolvePointCoorsExpr(params.turnX);
3328
+ if (turnX === undefined) {
3329
+ this.warnings.push(`TurnX parameter '${params.turnX}' of wire def is invalid, it cannot resolve to a number!`);
3330
+ }
3331
+ else {
3332
+ wire.turnX = turnX;
3333
+ }
3334
+ }
3335
+ if (params.turnY) {
3336
+ const turnY = this.resolvePointCoorsExpr(params.turnY);
3337
+ if (turnY === undefined) {
3338
+ this.warnings.push(`TurnY parameter '${params.turnY}' of wire def is invalid, it cannot resolve to a number!`);
3339
+ }
3340
+ else {
3341
+ wire.turnY = turnY;
3342
+ }
3343
+ }
3344
+ if (params.turnAt) {
3345
+ const turnAt = this.resolvePointFromExpr(params.turnAt);
3346
+ if (turnAt === undefined) {
3347
+ this.warnings.push(`TurnAt parameter '${params.turnY}' of wire def is invalid, it cannot resolve to a Point!`);
3348
+ }
3349
+ else {
3350
+ wire.turnAt = turnAt;
3351
+ }
3352
+ }
3353
+ if (params.net) {
3354
+ // TODO: handle Net
3355
+ }
3356
+ if (params.lineWidth) {
3357
+ const lw = this.resolveVarExpr(params.lineWidth);
3358
+ if (lw === undefined || lw < 0) {
3359
+ this.warnings.push(`lineWidth parameter '${params.lineWidth}' of wire def is invalid, it cannot resolve to a positive number!`);
3360
+ }
3361
+ else {
3362
+ wire.lineWidth = lw;
3363
+ }
3364
+ }
3365
+ if (params.joint) {
3366
+ wire.jointColor = params.joint;
3367
+ }
3368
+ if (params.jointSize) {
3369
+ const size = parseFloat(params.jointSize);
3370
+ if (size > 0) {
3371
+ wire.jointSize = size;
3372
+ }
3373
+ else {
3374
+ this.warnings.push(`Wire joint size should be positive, '${[params.jointSize]}' is not!`);
3375
+ }
3376
+ }
3377
+ if (params.squareJoint) {
3378
+ wire.isSquareJoint = true;
3379
+ }
3380
+ // Resolve Points
3381
+ wire.tryResolvePoints();
3382
+ return new HuatuWireDrawing(wire, this);
3383
+ }
3384
+ parseAngleMarkDef(def) {
3385
+ const res = parseStrIntoKeyValuePair(def, HuatuAngleMark.optsAngleMarkDef);
3386
+ const params = res.body;
3387
+ if (!params.pA)
3388
+ return `First Point (pA) not found when making Angle Mark: '${def}'`;
3389
+ if (!params.pB)
3390
+ return `Second Point (pB) not found when making Angle Mark: '${def}'`;
3391
+ if (!params.pC)
3392
+ return `Third Point (pC) not found when making Angle Mark: '${def}'`;
3393
+ const pA = this.resolvePointFromExpr(params.pA);
3394
+ if (!pA)
3395
+ return `First Point (pA, def: '${params.pA}') Cannot resolve to a Point`;
3396
+ const pB = this.resolvePointFromExpr(params.pB);
3397
+ if (!pB)
3398
+ return `Second Point (pB, def: '${params.pB}') Cannot resolve to a Point`;
3399
+ const pC = this.resolvePointFromExpr(params.pC);
3400
+ if (!pC)
3401
+ return `Third Point (pC, def: '${params.pC}') Cannot resolve to a Point`;
3402
+ const am = new HuatuAngleMark(pA, pB, pC);
3403
+ // Handle Stroke
3404
+ if (params.lineWidth) {
3405
+ const lw = this.resolveVarExpr(params.lineWidth);
3406
+ if (lw) {
3407
+ am.setStroke(lw, params.strokeColor);
3408
+ }
3409
+ }
3410
+ else if (params.strokeColor) {
3411
+ am.setStroke(HuatuAngleMark.DEFAULT_ARC_LINE_WIDTH, params.strokeColor);
3412
+ }
3413
+ // Handle Fill
3414
+ if (res.subKeys && res.subKeys["fill"]) {
3415
+ const fillVals = res.subKeys["fill"].body;
3416
+ if (!params.fillColor && fillVals["color"]) {
3417
+ params.fillColor = fillVals["color"];
3418
+ }
3419
+ if (params.fillOpacity === undefined && fillVals["opacity"] !== undefined && WtMath.isIn0and1(fillVals["opacity"])) {
3420
+ params.fillOpacity = fillVals["opacity"];
3421
+ }
3422
+ }
3423
+ if (params.fillColor && params.fillColor.toLowerCase() !== "none") {
3424
+ am.setFill(params.fillColor, params.fillOpacity);
3425
+ }
3426
+ // Make mark
3427
+ const size = params.size ? this.resolveVarExpr(params.size) : undefined;
3428
+ am.makeMark(params.type || "line1", size || HuatuAngleMark.DEFAULT_ARC_SIZE, params.large, params.showPerp, params.startArrow, params.labelPos);
3429
+ // Handle Label
3430
+ am.setLabelText(params.label, params.showDeg, params.degDigits);
3431
+ const lw = params.labelWidth ? this.resolveVarExpr(params.labelWidth) : undefined;
3432
+ const lh = params.labelHeight ? this.resolveVarExpr(params.labelHeight) : undefined;
3433
+ const ts = params.textSize ? this.resolveVarExpr(params.textSize) : undefined;
3434
+ const offset = params.labelOffset ? this.resolvePointFromExpr(params.labelOffset) : undefined;
3435
+ am.makeLabel(params.anchor, lw, lh, ts, params.textColor, offset);
3436
+ // Make failed pairs to warnings
3437
+ const failed = res.failedPairs.map(x => `${x.key}: '${x.value}'`);
3438
+ failed.push(...res.orphans);
3439
+ if (failed.length > 0) {
3440
+ this.warnings.push(`Attributes not valid to make Angle Mark: [${failed.join(", ")}]`);
3441
+ }
3442
+ return am;
3443
+ }
3444
+ parseDimMarkDef(def) {
3445
+ const res = parseStrIntoKeyValuePair(def, HuatuDimMark.optsDimMarkDef);
3446
+ const params = res.body;
3447
+ if (!params.from)
3448
+ return `Start Point of the Dimension Mark not found in '${def}'`;
3449
+ if (!params.to)
3450
+ return `End Point of the Dimension Mark not found in '${def}'`;
3451
+ const pStart = this.resolvePointFromExpr(params.from);
3452
+ if (!pStart)
3453
+ return `Start Point (def: '${params.from}') Cannot resolve to a Point`;
3454
+ const pEnd = this.resolvePointFromExpr(params.to);
3455
+ if (!pEnd)
3456
+ return `End Point (def: '${params.to}') Cannot resolve to a Point`;
3457
+ const dimMark = new HuatuDimMark(pStart, pEnd);
3458
+ if (!dimMark.isValid)
3459
+ return `Dimension Mark '${def}' Cannot resolve to a valid mark, start/end points are possibly identical!`;
3460
+ if (dimMark.isZeroDim)
3461
+ return `Dimension Mark '${def}' has zero Length to mark!`;
3462
+ let type = (params.type || "line").toLowerCase();
3463
+ type = type.startsWith("h") ? "hori"
3464
+ : type.startsWith("v") ? "vert"
3465
+ : "line";
3466
+ const conf = {
3467
+ type: type,
3468
+ dir: params.dir || "right",
3469
+ };
3470
+ // Judge and assign configs
3471
+ if (params.gap) {
3472
+ let gap = this.resolveVarExpr(params.gap);
3473
+ if (gap !== undefined && gap >= 0) {
3474
+ conf.gap = gap;
3475
+ }
3476
+ else {
3477
+ this.warnings.push(`Gap def '${params.gap}' cannot resolve to some number, will be ignored!`);
3478
+ }
3479
+ }
3480
+ if (params.barWidth) {
3481
+ let width = this.resolveVarExpr(params.barWidth);
3482
+ if (width && width > 0) {
3483
+ conf.barWidth = width;
3484
+ }
3485
+ else {
3486
+ this.warnings.push(`Bar Width def '${params.barWidth}' cannot resolve to some number, will be ignored!`);
3487
+ }
3488
+ }
3489
+ if (params.lineWidth) {
3490
+ let width = this.resolveVarExpr(params.lineWidth);
3491
+ if (width && width > 0) {
3492
+ conf.lineWidth = width;
3493
+ }
3494
+ else {
3495
+ this.warnings.push(`Line Width def '${params.lineWidth}' cannot resolve to some number, will be ignored!`);
3496
+ }
3497
+ }
3498
+ if (params.barLen) {
3499
+ let len = this.resolveVarExpr(params.barLen);
3500
+ if (len && len > 0) {
3501
+ conf.barLength = len;
3502
+ }
3503
+ else {
3504
+ this.warnings.push(`Bar Length def '${params.barLen}' cannot resolve to some number, will be ignored!`);
3505
+ }
3506
+ }
3507
+ if (params.linePos !== undefined && WtMath.isIn0and1(params.linePos)) {
3508
+ conf.linePos = params.linePos;
3509
+ }
3510
+ else if (params.linePos) {
3511
+ this.warnings.push(`Dimension Mark Line Position should be in [0,1], your input '${params.linePos}' is invalid`);
3512
+ }
3513
+ if (!!params.noStartBar)
3514
+ conf.noStartBar = true;
3515
+ if (!!params.noEndBar)
3516
+ conf.noEndBar = true;
3517
+ if (!!params.noStartArrow)
3518
+ conf.noStartArrow = true;
3519
+ if (!!params.noEndArrow)
3520
+ conf.noEndArrow = true;
3521
+ if (params.barColor)
3522
+ conf.barColor = params.barColor;
3523
+ if (params.lineColor)
3524
+ conf.lineColor = params.lineColor;
3525
+ if (params.arrow)
3526
+ conf.arrowType = params.arrow;
3527
+ if (params.label)
3528
+ conf.labelText = params.label;
3529
+ if (params.labelWidth) {
3530
+ let width = this.resolveVarExpr(params.labelWidth);
3531
+ if (width && width > 0) {
3532
+ conf.labelWidth = width;
3533
+ }
3534
+ else {
3535
+ this.warnings.push(`Label Width ('${params.labelWidth}') of DimMark cannot resolve to some number, will be ignored!`);
3536
+ }
3537
+ }
3538
+ if (params.labelHeight) {
3539
+ let height = this.resolveVarExpr(params.labelHeight);
3540
+ if (height && height > 0) {
3541
+ conf.labelHeight = height;
3542
+ }
3543
+ else {
3544
+ this.warnings.push(`Label Height ('${params.labelHeight}') of DimMark cannot resolve to some number, will be ignored!`);
3545
+ }
3546
+ }
3547
+ if (params.textSize) {
3548
+ let size = this.resolveVarExpr(params.textSize);
3549
+ if (size && size > 0) {
3550
+ conf.labelFontSize = size;
3551
+ }
3552
+ else {
3553
+ this.warnings.push(`Label Text Size ('${params.textSize}') of DimMark cannot resolve to some number, will be ignored!`);
3554
+ }
3555
+ }
3556
+ if (params.textColor)
3557
+ conf.labelColor = params.textColor;
3558
+ if (!!params.labelOnLine)
3559
+ conf.labelOnLine = true;
3560
+ if (params.labelFill)
3561
+ conf.labelBackground = params.labelFill;
3562
+ if (params.labelBorder)
3563
+ conf.labelBorderColor = params.labelBorder;
3564
+ if (!!params.rotateLabel)
3565
+ conf.rotateLabel = true;
3566
+ if (params.remotePos) {
3567
+ let pos = this.resolvePointFromExpr(params.remotePos);
3568
+ if (pos) {
3569
+ conf.remotePos = pos;
3570
+ }
3571
+ else {
3572
+ this.warnings.push(`Remote Position ('${params.remotePos}') of DimMark cannot resolve to some Point, will be ignored!`);
3573
+ }
3574
+ }
3575
+ if (!!params.showLength)
3576
+ conf.showLength = true;
3577
+ if (params.lenDigits !== undefined)
3578
+ conf.lenDigits = params.lenDigits;
3579
+ // Make mark
3580
+ dimMark.makeMark(conf);
3581
+ if (!dimMark.label) {
3582
+ this.warnings.push(`Label or showLength should be specified for a DimMark to make it display something, but the def '${def}' didn't`);
3583
+ // Make it invalid
3584
+ dimMark.isValid = false;
3585
+ }
3586
+ // Make failed pairs to warnings
3587
+ const failed = res.failedPairs.map(x => `${x.key}: '${x.value}'`);
3588
+ failed.push(...res.orphans);
3589
+ if (failed.length > 0) {
3590
+ this.warnings.push(`Attributes not valid to make Dimension Mark: [${failed.join(", ")}]`);
3591
+ }
3592
+ return dimMark;
3593
+ }
3594
+ /**
3595
+ * Unified function to resolve drawing definition
3596
+ * @param def The unified definition of a "drawing", including a label, or named modules
3597
+ * @param allowedTypes See the explanation in function `resolveDrawingOfShapes`
3598
+ * @returns The final draw or error message
3599
+ */
3600
+ resolveAsDrawingItem(def, allowedTypes) {
3601
+ if (typeof def !== "string")
3602
+ return `The Drawing define might be incorrect, it is [${JSON.stringify(def)}], please note the colon(:) sign which might be semi-colon(;)!`;
3603
+ // To see if it is a label presented
3604
+ const tryLabel = WtHuatu.REGEX_LABEL_DEF.exec(def);
3605
+ if (tryLabel) {
3606
+ const lblFormat = tryLabel[1];
3607
+ if (!lblFormat)
3608
+ return `Label Location/Format should be present!`;
3609
+ // Replace the ever escaped colon (:) in the form of "\:" if the user want to input multiple line text
3610
+ // with some colon contained
3611
+ const lblText = tryLabel[2].trim().replace(/\\:/g, ":");
3612
+ // For empty labels, will not return error, just leave it as empty
3613
+ // if (!lblText) return `Label Text could not be empty!`;
3614
+ return this.parseLabelDef(lblFormat, lblText);
3615
+ }
3616
+ // To see if it is an angle mark
3617
+ const tryAngleMark = WtHuatu.REGEX_ANGLE_MARK_DEF.exec(def);
3618
+ if (tryAngleMark) {
3619
+ return this.parseAngleMarkDef(tryAngleMark[1].trim());
3620
+ }
3621
+ // To see if it is an Dimension Mark
3622
+ const tryDimMark = WtHuatu.REGEX_DIM_MARK_DEF.exec(def);
3623
+ if (tryDimMark) {
3624
+ return this.parseDimMarkDef(tryDimMark[1].trim());
3625
+ }
3626
+ // To See if it is a WIRE
3627
+ const tryWire = WtHuatu.REGEX_WIRE_DEF.exec(def);
3628
+ if (tryWire) {
3629
+ if (!allowedTypes.includes("wire")) {
3630
+ return `Wire definition only allowed under Block/Layer/Job, others not!`;
3631
+ }
3632
+ return this.parseWireDef(tryWire[1].trim());
3633
+ }
3634
+ // If not label, then treat it as some shape drawing
3635
+ return this.resolveDrawingOfShapes(def, allowedTypes);
3636
+ }
3637
+ isDrawingNameValid(name, level) {
3638
+ if (this.paths.find(s => s.name === name))
3639
+ return `There has been a Path named as ${name}!`;
3640
+ if (this.pathSeries.find(s => s.name === name))
3641
+ return `There is some Path Series named as ${name}!`;
3642
+ if (this.symbols.find(s => s.name === name))
3643
+ return `There is some Symbol named as ${name}!`;
3644
+ if (level === "Block" || level === "Layer") {
3645
+ if (this.arrays.find(s => s.name === name))
3646
+ return `There is some Symbol Array named as ${name}!`;
3647
+ if (this.blocks.find(s => s.name === name))
3648
+ return `There is Block named as ${name}!`;
3649
+ if (level === "Layer") {
3650
+ if (this.layers.find(s => s.name === name))
3651
+ return `There is some Layer named as ${name}!`;
3652
+ }
3653
+ }
3654
+ }
3655
+ /**
3656
+ * Resolve Block definition, the input might be a string, or a string list
3657
+ * @param name Name of the Block
3658
+ * @param value Presentation to resolve the Block
3659
+ * @returns Error message
3660
+ */
3661
+ resolveBlock(name, value) {
3662
+ // Check Name
3663
+ const nameError = this.isDrawingNameValid(name, "Block");
3664
+ if (nameError)
3665
+ return [nameError];
3666
+ const allowedTypes = [
3667
+ "shape", "shapeSeries",
3668
+ "path", "pathSeries",
3669
+ "symbol", "array", "block",
3670
+ "component", "wire"
3671
+ ];
3672
+ // If it is only one line, i.d. the block has only one definition, We assume that it is the shape it contains
3673
+ if (typeof value === "string") {
3674
+ const item = this.resolveAsDrawingItem(value, allowedTypes);
3675
+ if (typeof item === "string")
3676
+ return [item]; // Return the error message
3677
+ const block = new HuatuBlock(name);
3678
+ block.addElement(item);
3679
+ this.defines.push(block); // Add to Definable
3680
+ this.blocks.push(block);
3681
+ return [];
3682
+ }
3683
+ // If the definition is a list, we assume that it contains multiple drawing definitions for the Block
3684
+ const errs = [];
3685
+ const block = new HuatuBlock(name);
3686
+ for (let i = 0; i < value.length; i++) {
3687
+ const def = value[i];
3688
+ const tryDesc = WtHuatu.tryDesc(def);
3689
+ if (tryDesc) {
3690
+ block.desc = tryDesc;
3691
+ }
3692
+ else {
3693
+ const item = this.resolveAsDrawingItem(def, allowedTypes);
3694
+ if (typeof item === "string")
3695
+ errs.push(item);
3696
+ else
3697
+ block.addElement(item);
3698
+ }
3699
+ }
3700
+ this.defines.push(block); // Add to Definable
3701
+ this.blocks.push(block);
3702
+ // Return the errors when resolving
3703
+ return errs;
3704
+ }
3705
+ /**
3706
+ * Resolve Layer definition, the input might be a string, or a string list
3707
+ * @param name Name of the Layer
3708
+ * @param value Presentation to resolve the Layer
3709
+ * @returns Error message
3710
+ */
3711
+ resolveLayer(name, value) {
3712
+ // Check Name
3713
+ const nameError = this.isDrawingNameValid(name, "Layer");
3714
+ if (nameError)
3715
+ return [nameError];
3716
+ const allowedTypes = [
3717
+ "shape", "shapeSeries",
3718
+ "path", "pathSeries",
3719
+ "symbol", "array", "block",
3720
+ "component", "wire"
3721
+ ];
3722
+ // If it is only one line, i.d. the layer has only one definition, We assume that it is the shape it contains
3723
+ if (typeof value === "string") {
3724
+ const item = this.resolveAsDrawingItem(value, allowedTypes);
3725
+ if (typeof item === "string")
3726
+ return [item]; // Return the error message
3727
+ const layer = new HuatuLayer(name);
3728
+ layer.addElement(item);
3729
+ this.layers.push(layer);
3730
+ return [];
3731
+ }
3732
+ // If the definition is a list, we assume that it contains multiple drawing definitions for the Layer
3733
+ const errs = [];
3734
+ const layer = new HuatuLayer(name);
3735
+ for (let i = 0; i < value.length; i++) {
3736
+ const def = value[i];
3737
+ const tryDesc = WtHuatu.tryDesc(def);
3738
+ if (tryDesc) {
3739
+ layer.desc = tryDesc;
3740
+ }
3741
+ else {
3742
+ const item = this.resolveAsDrawingItem(def, allowedTypes);
3743
+ if (typeof item === "string")
3744
+ errs.push(item);
3745
+ else
3746
+ layer.addElement(item);
3747
+ }
3748
+ }
3749
+ this.layers.push(layer);
3750
+ // Return the errors when resolving
3751
+ return errs;
3752
+ }
3753
+ resolveViewBoxFromDef(def) {
3754
+ const vbDefs = def.trim().split(";").map(x => x.trim()).filter(x => !!x);
3755
+ if (vbDefs.length < 2) {
3756
+ // Try to see if the def is organized in x,y,w,h form
3757
+ const numStrs = def.split(",").map(x => x.trim()).filter(x => !!x);
3758
+ const nums = numStrs
3759
+ .map(x => this.resolveVarExpr(x))
3760
+ .filter(x => x !== undefined);
3761
+ if (nums.length < 4)
3762
+ return `Parameters '${def}' insufficient/invalid to build a ViewBox!`;
3763
+ if (nums[2] > 0 && nums[3] > 0)
3764
+ return new SvgBBox(nums[0], nums[1], nums[2], nums[3]);
3765
+ else
3766
+ return `'${numStrs[2]}' or '${numStrs[3]}' resolves to non-positive value!`;
3767
+ }
3768
+ const p1 = this.resolvePointFromExpr(vbDefs[0]);
3769
+ if (!p1)
3770
+ return `Parameter '${vbDefs[0]}' is not a point!`;
3771
+ const p2 = this.resolvePointFromExpr(vbDefs[1]);
3772
+ if (p2)
3773
+ return SvgBBox.parseFromTwoPoints(p1, p2);
3774
+ if (vbDefs.length >= 3) {
3775
+ let w = this.resolveVarExpr(vbDefs[1]);
3776
+ let h = this.resolveVarExpr(vbDefs[2]);
3777
+ if (w && w > 0 && h && h > 0) {
3778
+ return new SvgBBox(p1.x, p1.y, w, h);
3779
+ }
3780
+ else {
3781
+ return `Width '${vbDefs[1]}' or Height '${vbDefs[2]}' resolved to no valid length!`;
3782
+ }
3783
+ }
3784
+ return `Parameters insufficient to build a ViewBox!`;
3785
+ }
3786
+ tryViewBox(def) {
3787
+ const match = /^(viewbox|viewarea|viewbetween)\s*;(.*)$/i.exec(def);
3788
+ if (!match)
3789
+ return undefined;
3790
+ return this.resolveViewBoxFromDef(match[2])
3791
+ || `'${def}' failed resolving to a valid view box!`;
3792
+ }
3793
+ trySize(def) {
3794
+ const match = /^(size)\s*;(.*)$/i.exec(def);
3795
+ if (!match)
3796
+ return undefined;
3797
+ const size = match[2].trim().split(";").map(x => x.trim()).filter(x => !!x);
3798
+ if (size.length < 1)
3799
+ return `'${match[2]}' has no size data presented!`;
3800
+ if (size.length >= 1) {
3801
+ const p = this.resolvePointFromExpr(size[0]);
3802
+ if (p) {
3803
+ if (p.x > 0 && p.y > 0)
3804
+ return p;
3805
+ else
3806
+ return `'${size[0]}' resolves to no valid(positive) Width/Height combination!`;
3807
+ }
3808
+ }
3809
+ const w = this.resolveVarExpr(size[0]);
3810
+ if (!w || w <= 0)
3811
+ return `'${size[0]}' resolves to no valid positive length for width!`;
3812
+ if (size.length >= 2) {
3813
+ const h = this.resolveVarExpr(size[1]);
3814
+ if (h && h > 0)
3815
+ return new SvgPoint(w, h);
3816
+ else
3817
+ return `'${size[1]}' resolves to no valid positive length for Height!`;
3818
+ }
3819
+ return new SvgPoint(w, 0);
3820
+ }
3821
+ resolveJobDrawingItem(draw, allowedTypes) {
3822
+ // Try Description
3823
+ const tryDesc = WtHuatu.tryDesc(draw);
3824
+ if (tryDesc)
3825
+ return `desc: ${tryDesc}`;
3826
+ // TO see if it is ViewBox define
3827
+ const tryViewBox = this.tryViewBox(draw);
3828
+ if (tryViewBox !== undefined)
3829
+ return tryViewBox;
3830
+ // Try Width or Height
3831
+ const trySize = this.trySize(draw);
3832
+ if (trySize !== undefined)
3833
+ return trySize;
3834
+ // If not special meaning, then try drawing
3835
+ return this.resolveAsDrawingItem(draw, allowedTypes);
3836
+ }
3837
+ /**
3838
+ * Resolve the defined job(s) from YML design file, 'draw' is one of the jobs
3839
+ * @param jobName Name of the job, if it is 'draw', then the default global draw
3840
+ * @param def The drawing design string to compose the final draw/job
3841
+ * @returns Errors if any
3842
+ */
3843
+ resolveJob(jobName, def) {
3844
+ if (!def)
3845
+ return [`Job '${jobName}' has no Definition!`];
3846
+ if (this.jobs.find(j => j.name === jobName))
3847
+ return [`Job '${jobName}' has already been defined!`];
3848
+ const errs = [];
3849
+ const job = new HuatuJob(this, jobName);
3850
+ const drawItems = [];
3851
+ if (typeof def === "object" && !Array.isArray(def)) { // Assume it is the IHuatuYmlJob data
3852
+ if (def.desc)
3853
+ job.desc = def.desc;
3854
+ if (def.config)
3855
+ job.config = WtHuatu.resolveConfig(def.config);
3856
+ if (def.size) {
3857
+ const size = this.resolvePointFromExpr(def.size);
3858
+ if (size) {
3859
+ if (size.x > 0)
3860
+ job.width = size.x;
3861
+ if (size.y > 0)
3862
+ job.height = size.y;
3863
+ }
3864
+ }
3865
+ if (def.height) {
3866
+ const height = this.resolveVarExpr(def.height);
3867
+ if (height && height > 0)
3868
+ job.height = height;
3869
+ }
3870
+ if (def.width) {
3871
+ const width = this.resolveVarExpr(def.width);
3872
+ if (width && width > 0)
3873
+ job.width = width;
3874
+ }
3875
+ let vbDef = def.viewBox || def.viewArea || def.viewBetween;
3876
+ if (vbDef) {
3877
+ const vb = this.resolveViewBoxFromDef(vbDef);
3878
+ if (typeof vb === "string")
3879
+ errs.push(vb);
3880
+ else if (vb)
3881
+ job.viewBox = vb;
3882
+ else {
3883
+ this.warnings.push(`'${vbDef}' cannot resolve to a valid View Box!`);
3884
+ }
3885
+ }
3886
+ if (def.drawings) {
3887
+ if (Array.isArray(def.drawings))
3888
+ drawItems.push(...def.drawings.filter(x => !!x));
3889
+ else if (typeof def.drawings === "string")
3890
+ drawItems.push(def.drawings);
3891
+ }
3892
+ }
3893
+ else if (typeof def === "string") {
3894
+ drawItems.push(def);
3895
+ }
3896
+ else if (Array.isArray(def)) {
3897
+ drawItems.push(...def);
3898
+ }
3899
+ if (drawItems.length < 1)
3900
+ return [`Job '${jobName}' has no valid items presented!`];
3901
+ const allowedTypes = [
3902
+ "shape", "shapeSeries",
3903
+ "path", "pathSeries",
3904
+ "symbol", "array",
3905
+ "block", "layer",
3906
+ "component", "wire",
3907
+ ];
3908
+ drawItems.forEach(d => {
3909
+ const draw = this.resolveJobDrawingItem(d, allowedTypes);
3910
+ if (typeof draw === "string") {
3911
+ if (draw.startsWith("desc"))
3912
+ job.desc = draw.substring(6);
3913
+ else
3914
+ errs.push(draw);
3915
+ }
3916
+ else if (draw instanceof SvgPoint) {
3917
+ job.width = draw.x;
3918
+ if (draw.y > 0)
3919
+ job.height = draw.y;
3920
+ }
3921
+ else if (draw instanceof SvgBBox) {
3922
+ job.viewBox = draw;
3923
+ job.vbExplicit = true;
3924
+ }
3925
+ else if (draw) {
3926
+ job.drawings.push(draw);
3927
+ }
3928
+ });
3929
+ // Re-Check ViewBox
3930
+ if (!job.viewBox) {
3931
+ if (job.huatu.viewBox) {
3932
+ job.viewBox = job.huatu.viewBox;
3933
+ job.vbExplicit = true;
3934
+ }
3935
+ else {
3936
+ job.viewBox = job.calcViewBox();
3937
+ }
3938
+ }
3939
+ if (!job.desc)
3940
+ job.desc = job.huatu.desc;
3941
+ this.jobs.push(job);
3942
+ return errs;
3943
+ }
3944
+ buildSvg(buildType, jobName, withSize) {
3945
+ const job = this.jobs.find(j => j.name === (jobName ?? "draw"));
3946
+ if (job)
3947
+ return job.buildSvg(buildType, withSize);
3948
+ else
3949
+ return "";
3950
+ }
3951
+ buildSvgAsHtml(buildType, jobName, indent, withSize) {
3952
+ const job = this.jobs.find(j => j.name === (jobName ?? "draw"));
3953
+ if (job)
3954
+ return job.buildHtml(buildType, indent, withSize);
3955
+ else
3956
+ return "";
3957
+ }
3958
+ buildSvgStandalone(buildType, jobName, indent, withSize) {
3959
+ const job = this.jobs.find(j => j.name === (jobName ?? "draw"));
3960
+ if (job)
3961
+ return job.buildSvgStandalone(buildType, indent, withSize);
3962
+ else
3963
+ return "";
3964
+ }
3965
+ /** --------------- Component part ------------------------------------------------------------------------------ */
3966
+ resolveComponentDef(name, def) {
3967
+ const compDef = new HuatuComponentDef(name, def);
3968
+ if (!compDef.isValid) {
3969
+ return compDef.errors
3970
+ .map(x => x.msgs ? x.msgs.map(msg => `${x.title}: ${msg}`) : [x.title])
3971
+ .flat();
3972
+ }
3973
+ else {
3974
+ // Convert Errors to warnings if any
3975
+ if (compDef.errors.length > 0) {
3976
+ compDef.errors.forEach(e => {
3977
+ this.warnings.push(`Errors Found When resolving component "${name}" => ${e.title}: `
3978
+ + (e.msgs && e.msgs.length > 0 ? e.msgs.map((m, idx) => `${idx + 1})${m}`).join("; ") : ""));
3979
+ });
3980
+ }
3981
+ // Add the definition
3982
+ this.compDefs.push(compDef);
3983
+ return [];
3984
+ }
3985
+ }
3986
+ resolveComponentPlacement(name, def) {
3987
+ const errors = [];
3988
+ let ivObj = {};
3989
+ let defToParse;
3990
+ const isIvKey = (key) => !!/\$[a-zA-Z][a-zA-Z0-9_]*$/i.exec(key);
3991
+ if (typeof def === "string") {
3992
+ // Filter out the segments of input vars
3993
+ const src = def.split(";")
3994
+ .map(x => x.trim())
3995
+ .filter(x => !!x);
3996
+ const filtered = [];
3997
+ let i = 0;
3998
+ while (i < src.length) {
3999
+ if (isIvKey(src[i])) {
4000
+ if ((i + 1) < src.length) {
4001
+ if (!isIvKey(src[i + 1])) {
4002
+ ivObj[src[i]] = src[i + 1];
4003
+ i += 2;
4004
+ }
4005
+ else {
4006
+ i += 1;
4007
+ }
4008
+ }
4009
+ }
4010
+ else {
4011
+ filtered.push(src[i]);
4012
+ i++;
4013
+ }
4014
+ }
4015
+ defToParse = filtered.join(";");
4016
+ }
4017
+ else {
4018
+ defToParse = {};
4019
+ Object.keys(def).forEach(x => {
4020
+ if (x.startsWith("$")) {
4021
+ ivObj[x] = def[x];
4022
+ }
4023
+ else {
4024
+ defToParse[x] = def[x];
4025
+ }
4026
+ });
4027
+ }
4028
+ // Parse it
4029
+ const parseRes = parseStrIntoKeyValuePair(defToParse, HuatuComponentPlacement.optsComponentPlacement);
4030
+ const params = parseRes.body;
4031
+ if (!params.component) {
4032
+ return [`Placement "${name}" Failed, no component definition specified!`];
4033
+ }
4034
+ const compDef = this.compDefs.find(x => x.name === params.component);
4035
+ if (!compDef) {
4036
+ return [`Component Definition "${params.component}" not found!`];
4037
+ }
4038
+ if (!params.location) {
4039
+ return [`Location is not provided for Placement "${name}"`];
4040
+ }
4041
+ const loc = this.resolvePointFromExpr(params.location);
4042
+ if (!loc) {
4043
+ return [`Component Placement location "${params.location}" cannot be resolved`];
4044
+ }
4045
+ // Report unhandled params to warnings
4046
+ if (parseRes.failedPairs.length > 0) {
4047
+ const pairs = parseRes.failedPairs.map((x, i) => `${i + 1}) "${x.key}": "${x.value}"`).join(", ");
4048
+ this.warnings.push(`Parameters unhandled when making component placement "${name}": ${pairs}`);
4049
+ }
4050
+ // Handle Input Vars
4051
+ const givenIvs = {};
4052
+ const ivNames = Object.keys(ivObj)
4053
+ .filter(x => x.startsWith("$"))
4054
+ .map(x => x.substring(1))
4055
+ .filter(x => !!x);
4056
+ ivNames.forEach(ivName => {
4057
+ const iv = compDef.inputVars.find(x => x.name === ivName);
4058
+ if (iv) {
4059
+ let ivVal = ivObj[`$${ivName}`];
4060
+ if (!ivVal || typeof ivVal !== "string" || !ivVal.trim()) {
4061
+ errors.push(`Input Var [$${ivName}] takes some empty/invalid value!`);
4062
+ }
4063
+ else {
4064
+ ivVal = ivVal.trim();
4065
+ let ivValAsColor = isValidColor(ivVal);
4066
+ if (iv.type === "color") {
4067
+ if (ivValAsColor) {
4068
+ givenIvs[ivName] = ivValAsColor;
4069
+ }
4070
+ else {
4071
+ errors.push(`"${ivVal}" is not some valid color for Input Var "${ivName}" which is defined as some color`);
4072
+ }
4073
+ }
4074
+ else if (iv.type === "number") {
4075
+ const ivNumber = this.resolveVarExpr(ivVal);
4076
+ if (ivNumber !== undefined) {
4077
+ givenIvs[ivName] = ivNumber;
4078
+ }
4079
+ else {
4080
+ errors.push(`"${ivVal}" cannot resolve to be a number for Input Var "${ivName}"`);
4081
+ }
4082
+ }
4083
+ else {
4084
+ // For Text vars, just copy it, no conversion needed
4085
+ givenIvs[ivName] = ivVal;
4086
+ }
4087
+ }
4088
+ }
4089
+ else {
4090
+ errors.push(`Input Var [$${ivName}] doesn't exist in Component Definition!`);
4091
+ }
4092
+ });
4093
+ let rotDeg;
4094
+ if (params.rotDeg) {
4095
+ rotDeg = this.resolveVarExpr(params.rotDeg);
4096
+ if (rotDeg === undefined) {
4097
+ errors.push(`Component Placement Rotate Deg "${params.rotDeg}" cannot be resolved!`);
4098
+ }
4099
+ }
4100
+ // Create the placement
4101
+ const placement = new HuatuComponentPlacement(name, loc, compDef, givenIvs, params.mirror, rotDeg, params.refPoint, params.rotateLabel);
4102
+ // Adjust mirroring and rotation
4103
+ if (!placement.isValid || !placement.huatu.draw) {
4104
+ errors.push(`Placement [${name}] cannot be resolved, there is no drawing body!`);
4105
+ }
4106
+ else {
4107
+ // Add the placement
4108
+ this.compPlacements.push(placement);
4109
+ }
4110
+ return errors;
4111
+ }
4112
+ // This function is to resolve the Component Port definitions after the component is defined/placed
4113
+ parseComponentPorts(obj) {
4114
+ if (!obj.ports)
4115
+ return [];
4116
+ if (!Array.isArray(obj.ports) || obj.ports.length < 1)
4117
+ return ["The Ports definition must be presented as a list!"];
4118
+ const errors = [];
4119
+ const portDefs = obj.ports;
4120
+ portDefs.forEach(p => {
4121
+ const parseRes = parseStrIntoKeyValuePair(p, HuatuComponentPort.optsComponentPort);
4122
+ if (!parseRes.body.name) {
4123
+ errors.push(`No Name resolved in port definition: "${p}"`);
4124
+ }
4125
+ else if (!/^[0-9a-zA-Z]+$/.test(parseRes.body.name)) {
4126
+ errors.push(`Port Name can only be combination of a to z, A to Z, and 0 -9, [${parseRes.body.name}] is not qualified, please pick another one!`);
4127
+ }
4128
+ else if (!parseRes.body.anchor) {
4129
+ errors.push(`No Anchor Point resolved in Port Definition "${p}"`);
4130
+ }
4131
+ else {
4132
+ const anchor = this.resolvePointFromExpr(parseRes.body.anchor);
4133
+ if (!anchor) {
4134
+ errors.push(`Anchor Point Definition "${parseRes.body.anchor}" cannot be resolved!`);
4135
+ }
4136
+ else if (this.compPorts.find(x => x.name === parseRes.body.name)) {
4137
+ errors.push(`Port "${parseRes.body.name}" has already been defined!`);
4138
+ }
4139
+ else {
4140
+ const newPort = new HuatuComponentPort(parseRes.body.name, anchor);
4141
+ if (parseRes.body.attr)
4142
+ newPort.attr = parseRes.body.attr;
4143
+ if (parseRes.body.rayDeg) {
4144
+ let deg = this.resolveVarExpr(parseRes.body.rayDeg);
4145
+ if (deg === undefined) {
4146
+ errors.push(`[comp port] Ray Deg expression '${parseRes.body.rayDeg}' cannot be resolved!`);
4147
+ }
4148
+ else {
4149
+ newPort.rayDeg = deg;
4150
+ }
4151
+ }
4152
+ if (parseRes.body.minHoldLen) {
4153
+ let len = this.resolveVarExpr(parseRes.body.minHoldLen);
4154
+ if (len === undefined) {
4155
+ errors.push(`[comp port] Hold length expression '${parseRes.body.minHoldLen}' cannot be resolved!`);
4156
+ }
4157
+ else {
4158
+ newPort.minHoldLen = len;
4159
+ }
4160
+ }
4161
+ this.compPorts.push(newPort);
4162
+ }
4163
+ }
4164
+ });
4165
+ return errors;
4166
+ }
4167
+ /** --------------- Static Methods ------------------------------------------------------------------------------ */
4168
+ static tryDesc(def) {
4169
+ const match = /^desc\s*;(.*)$/i.exec(def);
4170
+ if (match)
4171
+ return match[1].trim();
4172
+ else
4173
+ return undefined;
4174
+ }
4175
+ static resolveConfig(cfg) {
4176
+ const config = {};
4177
+ config.labelBackground = isValidColor(cfg.labelBackground
4178
+ || cfg.labelBackgroundColor
4179
+ || cfg.labelBg
4180
+ || cfg.labelBgColor);
4181
+ config.labelBorder = isValidColor(cfg.labelBorder || cfg.labelBorderColor);
4182
+ config.markCenters = booleanCheck(cfg.markCenters);
4183
+ config.markBezierAuxLines = booleanCheck(cfg.markBezierAuxLines);
4184
+ config.markNamedPoints = isValidColor(cfg.markNamedPoints);
4185
+ config.markPathPoints = isValidColor(cfg.markPathPoints);
4186
+ if (cfg.markSize)
4187
+ config.markSize = parseFloat(cfg.markSize) ?? 2; // Default Size of Marking Point
4188
+ config.markPositions = isValidColor(cfg.markPositions || cfg.markAnchors);
4189
+ config.printUnusedModule = booleanCheck(cfg.printUnusedModule);
4190
+ config.showBBox = isValidColor(cfg.showBBox);
4191
+ // TODO: For more configurations in the future, please append here
4192
+ return config;
4193
+ }
4194
+ static buildEmbeddedStyles() {
4195
+ if (!globalThis.wthtJobBuildId) {
4196
+ globalThis.wthtJobBuildId = genHex(8);
4197
+ }
4198
+ const styles = [];
4199
+ // Class label-border/box
4200
+ let attrs = [];
4201
+ const htConfig = globalThis.wtHuatuConfig;
4202
+ if (htConfig) {
4203
+ if (htConfig.labelBackground)
4204
+ attrs.push(["fill", htConfig.labelBackground]);
4205
+ else
4206
+ attrs.push(["fill", "none"]);
4207
+ if (htConfig.labelBorder)
4208
+ attrs.push(["stroke", htConfig.labelBorder], ["stroke-width", "0.2"] // Here, the label box border could only be 0.2
4209
+ );
4210
+ }
4211
+ else {
4212
+ attrs.push(["fill", "none"]);
4213
+ }
4214
+ const buildStyle = (clsName) => `.${clsName}-${globalThis.wthtJobBuildId} { ${attrs.map(a => `${a[0]}: ${a[1]};`).join(" ")} }`;
4215
+ styles.push(buildStyle("wthuatu-label-box"));
4216
+ // Label Container
4217
+ attrs = [
4218
+ ["width", "100%"],
4219
+ ["height", "100%"],
4220
+ ["padding", "0"],
4221
+ ["display", "flex"],
4222
+ ];
4223
+ styles.push(buildStyle("huatu-label-container"));
4224
+ // Label Text
4225
+ attrs = [
4226
+ ["margin", "auto 0"],
4227
+ ["text-align", "center"],
4228
+ ["max-height", "100%"],
4229
+ ["width", "100%"],
4230
+ ["line-height", "1.0"],
4231
+ ];
4232
+ styles.push(buildStyle("huatu-label-text"));
4233
+ // Class of Position/Anchor Marks
4234
+ attrs = [];
4235
+ if (htConfig && htConfig.markPositions) {
4236
+ attrs.push(["fill", htConfig.markPositions]);
4237
+ styles.push(buildStyle("wthuatu-mark-position"));
4238
+ }
4239
+ // Class of Centers
4240
+ attrs = [];
4241
+ if (htConfig && htConfig.markCenters) {
4242
+ attrs.push(["stroke", "grey"], ["stroke-width", "0.1"]);
4243
+ styles.push(buildStyle("wthuatu-mark-centers"));
4244
+ }
4245
+ // Class of Bezier Control Lines and Points
4246
+ attrs = [];
4247
+ if (htConfig && htConfig.markBezierAuxLines) {
4248
+ attrs.push(["fill", "aqua"], ["stroke", "aqua"], ["stroke-width", "0.1"]);
4249
+ styles.push(buildStyle("wthuatu-mark-bezier-control"));
4250
+ }
4251
+ // Class of Named Points
4252
+ attrs = [];
4253
+ if (htConfig && htConfig.markNamedPoints) {
4254
+ attrs.push(["fill", htConfig.markNamedPoints]);
4255
+ styles.push(buildStyle("wthuatu-mark-named-points"));
4256
+ }
4257
+ // Class of Path Points
4258
+ attrs = [];
4259
+ if (htConfig && htConfig.markPathPoints) {
4260
+ attrs.push(["fill", htConfig.markPathPoints]);
4261
+ styles.push(buildStyle("wthuatu-mark-path-points"));
4262
+ }
4263
+ // Class of BBOX
4264
+ attrs = [];
4265
+ if (htConfig && htConfig.showBBox) {
4266
+ attrs.push(["fill", "none"], ["stroke", htConfig.showBBox], ["stroke-width", "0.05"]);
4267
+ styles.push(buildStyle("wthuatu-bbox-ref"));
4268
+ }
4269
+ // TODO: make more if required
4270
+ return styles.join("\n");
4271
+ }
4272
+ static RESERVED_NAMES = [
4273
+ "sin", "cos", "tan", "atan", "asin", "acos",
4274
+ "dsin", "dcos", "dtan", "datan", "dasin", "dacos",
4275
+ "exp", "log", "log10", "sqrt",
4276
+ "pi", "math",
4277
+ "this", "globalthis", "wtvarfuncs",
4278
+ ];
4279
+ }