pcb-scene3d-viewer 1.0.1

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 (65) hide show
  1. package/AGENTS.md +67 -0
  2. package/COMMERCIAL-LICENSE.md +15 -0
  3. package/CONTRIBUTING.md +14 -0
  4. package/LICENSE +19 -0
  5. package/LICENSES/AGPL-3.0-or-later.txt +235 -0
  6. package/LICENSES/CC-BY-SA-4.0.txt +170 -0
  7. package/LICENSES/LGPL-2.1-or-later.txt +176 -0
  8. package/LICENSES/LicenseRef-PolyForm-Noncommercial-1.0.0.txt +131 -0
  9. package/NOTICE.md +36 -0
  10. package/README.md +128 -0
  11. package/REUSE.toml +16 -0
  12. package/docs/api.md +148 -0
  13. package/docs/circuitjson.md +190 -0
  14. package/docs/model-format.md +117 -0
  15. package/docs/testing.md +23 -0
  16. package/package.json +65 -0
  17. package/spec/library-scope.md +36 -0
  18. package/src/PcbModelArchiveExporter.mjs +320 -0
  19. package/src/PcbScene3dArcUtils.mjs +27 -0
  20. package/src/PcbScene3dBoardAssemblyPlacement.mjs +36 -0
  21. package/src/PcbScene3dBoardAssemblyPresentation.mjs +859 -0
  22. package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +537 -0
  23. package/src/PcbScene3dBoardMaterialPalette.mjs +40 -0
  24. package/src/PcbScene3dBoardShapeFactory.mjs +895 -0
  25. package/src/PcbScene3dBoardSolderMaskFactory.mjs +613 -0
  26. package/src/PcbScene3dCameraRig.mjs +168 -0
  27. package/src/PcbScene3dCircuitJsonAdapter.mjs +545 -0
  28. package/src/PcbScene3dController.mjs +956 -0
  29. package/src/PcbScene3dCopperDetailFilter.mjs +490 -0
  30. package/src/PcbScene3dCopperFactory.mjs +559 -0
  31. package/src/PcbScene3dCopperTextFactory.mjs +534 -0
  32. package/src/PcbScene3dCutoutGeometryFilter.mjs +873 -0
  33. package/src/PcbScene3dDetailCoordinateNormalizer.mjs +65 -0
  34. package/src/PcbScene3dDrillCutoutFilter.mjs +224 -0
  35. package/src/PcbScene3dDrillPathFactory.mjs +362 -0
  36. package/src/PcbScene3dDrillVoidFactory.mjs +268 -0
  37. package/src/PcbScene3dExternalModelLoadOrder.mjs +54 -0
  38. package/src/PcbScene3dExternalModels.mjs +968 -0
  39. package/src/PcbScene3dFallbackVisibility.mjs +82 -0
  40. package/src/PcbScene3dInteractionHints.mjs +56 -0
  41. package/src/PcbScene3dMountRig.mjs +53 -0
  42. package/src/PcbScene3dOutlineBuilder.mjs +210 -0
  43. package/src/PcbScene3dPadFactory.mjs +553 -0
  44. package/src/PcbScene3dPresetState.mjs +48 -0
  45. package/src/PcbScene3dRenderGroupVisibility.mjs +134 -0
  46. package/src/PcbScene3dRuntime.mjs +996 -0
  47. package/src/PcbScene3dRuntimeBoardMeshes.mjs +99 -0
  48. package/src/PcbScene3dSelectionStyler.mjs +252 -0
  49. package/src/PcbScene3dShapePathFactory.mjs +220 -0
  50. package/src/PcbScene3dShellRenderer.mjs +131 -0
  51. package/src/PcbScene3dSilkscreenFactory.mjs +854 -0
  52. package/src/PcbScene3dSilkscreenStrokeWidthResolver.mjs +81 -0
  53. package/src/PcbScene3dStepLoader.mjs +611 -0
  54. package/src/PcbScene3dStrokeFont.mjs +671 -0
  55. package/src/PcbScene3dStrokeGeometryBuilder.mjs +322 -0
  56. package/src/PcbScene3dText.mjs +99 -0
  57. package/src/PcbScene3dTrueTypeTextFactory.mjs +885 -0
  58. package/src/PcbScene3dViaFactory.mjs +176 -0
  59. package/src/PcbScene3dViewCompensation.mjs +109 -0
  60. package/src/PcbScene3dViewScale.mjs +24 -0
  61. package/src/PcbScene3dViewportResize.mjs +35 -0
  62. package/src/PcbScene3dWorkerClient.mjs +123 -0
  63. package/src/index.mjs +1 -0
  64. package/src/scene3d.mjs +44 -0
  65. package/src/styles/scene3d.css +295 -0
@@ -0,0 +1,559 @@
1
+ import { PcbScene3dArcUtils } from './PcbScene3dArcUtils.mjs'
2
+ import { PcbScene3dPadFactory } from './PcbScene3dPadFactory.mjs'
3
+ import { PcbScene3dCopperTextFactory } from './PcbScene3dCopperTextFactory.mjs'
4
+
5
+ /**
6
+ * Builds copper-detail meshes for the interactive 3D PCB scene.
7
+ */
8
+ export class PcbScene3dCopperFactory {
9
+ static #TOP_COPPER_LAYER_ID = 1
10
+ static #BOTTOM_COPPER_LAYER_ID = 32
11
+ static #FULL_CIRCLE_EPSILON = 0.001
12
+ static #ROUND_CAP_SEGMENTS = 16
13
+
14
+ /**
15
+ * Builds the combined top and bottom copper group.
16
+ * @param {any} THREE
17
+ * @param {{ tracks?: any[], arcs?: any[], pads?: any[], vias?: any[], copperTexts?: any[] }} detail
18
+ * @param {number} topZ
19
+ * @param {number} bottomZ
20
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
21
+ * @param {{ coordinateSystem?: string }} [options]
22
+ * @returns {any}
23
+ */
24
+ static buildGroup(
25
+ THREE,
26
+ detail,
27
+ topZ,
28
+ bottomZ,
29
+ normalizeBoardPoint,
30
+ options = {}
31
+ ) {
32
+ const group = new THREE.Group()
33
+ const topGroup = PcbScene3dCopperFactory.#buildSideGroup(
34
+ THREE,
35
+ {
36
+ tracks: PcbScene3dCopperFactory.#filterTracks(
37
+ detail?.tracks,
38
+ 'top'
39
+ ),
40
+ arcs: PcbScene3dCopperFactory.#filterArcs(detail?.arcs, 'top'),
41
+ pads: detail?.pads || [],
42
+ vias: detail?.vias || [],
43
+ copperTexts: detail?.copperTexts || []
44
+ },
45
+ Math.abs(Number(topZ || 0)),
46
+ normalizeBoardPoint,
47
+ false,
48
+ options
49
+ )
50
+ const bottomGroup = PcbScene3dCopperFactory.#buildSideGroup(
51
+ THREE,
52
+ {
53
+ tracks: PcbScene3dCopperFactory.#filterTracks(
54
+ detail?.tracks,
55
+ 'bottom'
56
+ ),
57
+ arcs: PcbScene3dCopperFactory.#filterArcs(
58
+ detail?.arcs,
59
+ 'bottom'
60
+ ),
61
+ pads: detail?.pads || [],
62
+ vias: detail?.vias || [],
63
+ copperTexts: detail?.copperTexts || []
64
+ },
65
+ Math.abs(Number(bottomZ || 0)),
66
+ normalizeBoardPoint,
67
+ true,
68
+ options
69
+ )
70
+
71
+ if (topGroup.children.length) {
72
+ group.add(topGroup)
73
+ }
74
+ if (bottomGroup.children.length) {
75
+ group.add(bottomGroup)
76
+ }
77
+
78
+ return group
79
+ }
80
+
81
+ /**
82
+ * Builds one side-specific copper group.
83
+ * @param {any} THREE
84
+ * @param {{ tracks?: any[], arcs?: any[], pads?: any[], vias?: any[], copperTexts?: any[] }} detail
85
+ * @param {number} z
86
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
87
+ * @param {boolean} mirrorY
88
+ * @param {{ coordinateSystem?: string }} [options]
89
+ * @returns {any}
90
+ */
91
+ static #buildSideGroup(
92
+ THREE,
93
+ detail,
94
+ z,
95
+ normalizeBoardPoint,
96
+ mirrorY,
97
+ options = {}
98
+ ) {
99
+ const group = new THREE.Group()
100
+ const trackMesh = PcbScene3dCopperFactory.#buildTrackMesh(
101
+ THREE,
102
+ detail?.tracks || [],
103
+ z,
104
+ normalizeBoardPoint,
105
+ mirrorY
106
+ )
107
+ const arcMesh = PcbScene3dCopperFactory.#buildArcMesh(
108
+ THREE,
109
+ detail?.arcs || [],
110
+ z,
111
+ normalizeBoardPoint,
112
+ mirrorY
113
+ )
114
+ const padGroup = PcbScene3dPadFactory.buildGroup(
115
+ THREE,
116
+ detail?.pads || [],
117
+ z,
118
+ normalizeBoardPoint,
119
+ {
120
+ side: mirrorY ? 'bottom' : 'top',
121
+ mirrorY
122
+ }
123
+ )
124
+ const textGroup = PcbScene3dCopperTextFactory.buildGroup(
125
+ THREE,
126
+ detail?.copperTexts || [],
127
+ z + 0.25,
128
+ normalizeBoardPoint,
129
+ {
130
+ glyphYUp: PcbScene3dCopperFactory.#usesYUpGlyphs(options),
131
+ side: mirrorY ? 'bottom' : 'top',
132
+ mirrorY
133
+ }
134
+ )
135
+
136
+ if (trackMesh) {
137
+ group.add(trackMesh)
138
+ }
139
+ if (arcMesh) {
140
+ group.add(arcMesh)
141
+ }
142
+ if (padGroup.children.length) {
143
+ group.add(padGroup)
144
+ }
145
+ if (textGroup.children.length) {
146
+ group.add(textGroup)
147
+ }
148
+ if (mirrorY && group.children.length) {
149
+ group.rotation.x = Math.PI
150
+ }
151
+
152
+ return group
153
+ }
154
+
155
+ /**
156
+ * Checks whether copper text glyph strokes are already in y-up scene space.
157
+ * @param {{ coordinateSystem?: string } | undefined} options
158
+ * @returns {boolean}
159
+ */
160
+ static #usesYUpGlyphs(options) {
161
+ return String(options?.coordinateSystem || '') === 'kicad-3d-y-up'
162
+ }
163
+
164
+ /**
165
+ * Builds one widened copper-track mesh for one face.
166
+ * @param {any} THREE
167
+ * @param {{ x1?: number, y1?: number, x2?: number, y2?: number, width?: number }[]} tracks
168
+ * @param {number} z
169
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
170
+ * @param {boolean} mirrorY
171
+ * @returns {any | null}
172
+ */
173
+ static #buildTrackMesh(THREE, tracks, z, normalizeBoardPoint, mirrorY) {
174
+ const positions = []
175
+
176
+ for (const track of tracks) {
177
+ const start = PcbScene3dCopperFactory.#normalizePoint(
178
+ normalizeBoardPoint,
179
+ Number(track?.x1 || 0),
180
+ Number(track?.y1 || 0),
181
+ mirrorY
182
+ )
183
+ const end = PcbScene3dCopperFactory.#normalizePoint(
184
+ normalizeBoardPoint,
185
+ Number(track?.x2 || 0),
186
+ Number(track?.y2 || 0),
187
+ mirrorY
188
+ )
189
+ PcbScene3dCopperFactory.#appendTrackTriangles(
190
+ positions,
191
+ start,
192
+ end,
193
+ Number(track?.width || 0),
194
+ z
195
+ )
196
+ }
197
+
198
+ return PcbScene3dCopperFactory.#buildStrokeMesh(THREE, positions)
199
+ }
200
+
201
+ /**
202
+ * Builds one widened copper-arc mesh for one face.
203
+ * @param {any} THREE
204
+ * @param {{ x?: number, y?: number, radius?: number, startAngle?: number, endAngle?: number, width?: number }[]} arcs
205
+ * @param {number} z
206
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
207
+ * @param {boolean} mirrorY
208
+ * @returns {any | null}
209
+ */
210
+ static #buildArcMesh(THREE, arcs, z, normalizeBoardPoint, mirrorY) {
211
+ const positions = []
212
+
213
+ for (const arc of arcs) {
214
+ const center = PcbScene3dCopperFactory.#normalizePoint(
215
+ normalizeBoardPoint,
216
+ Number(arc?.x || 0),
217
+ Number(arc?.y || 0),
218
+ mirrorY
219
+ )
220
+ PcbScene3dCopperFactory.#appendArcTriangles(
221
+ positions,
222
+ center,
223
+ arc,
224
+ z,
225
+ mirrorY
226
+ )
227
+ }
228
+
229
+ return PcbScene3dCopperFactory.#buildStrokeMesh(THREE, positions)
230
+ }
231
+
232
+ /**
233
+ * Builds one copper stroke mesh from triangle positions.
234
+ * @param {any} THREE
235
+ * @param {number[]} positions
236
+ * @returns {any | null}
237
+ */
238
+ static #buildStrokeMesh(THREE, positions) {
239
+ if (!positions.length) {
240
+ return null
241
+ }
242
+
243
+ const geometry = new THREE.BufferGeometry()
244
+ geometry.setAttribute(
245
+ 'position',
246
+ new THREE.Float32BufferAttribute(positions, 3)
247
+ )
248
+ geometry.computeVertexNormals?.()
249
+
250
+ return new THREE.Mesh(
251
+ geometry,
252
+ PcbScene3dCopperFactory.#buildMaterial(THREE)
253
+ )
254
+ }
255
+
256
+ /**
257
+ * Builds the shared copper material.
258
+ * @param {any} THREE
259
+ * @returns {any}
260
+ */
261
+ static #buildMaterial(THREE) {
262
+ return new THREE.MeshStandardMaterial({
263
+ color: 0xd9a61d,
264
+ roughness: 0.38,
265
+ metalness: 0.55,
266
+ side: THREE.DoubleSide
267
+ })
268
+ }
269
+
270
+ /**
271
+ * Filters one track list to one outer copper face.
272
+ * @param {any[] | undefined} tracks
273
+ * @param {'top' | 'bottom'} side
274
+ * @returns {any[]}
275
+ */
276
+ static #filterTracks(tracks, side) {
277
+ return (tracks || []).filter((track) =>
278
+ PcbScene3dCopperFactory.#matchesCopperLayer(track, side)
279
+ )
280
+ }
281
+
282
+ /**
283
+ * Filters one arc list to one outer copper face.
284
+ * @param {any[] | undefined} arcs
285
+ * @param {'top' | 'bottom'} side
286
+ * @returns {any[]}
287
+ */
288
+ static #filterArcs(arcs, side) {
289
+ return (arcs || []).filter((arc) =>
290
+ PcbScene3dCopperFactory.#matchesCopperLayer(arc, side)
291
+ )
292
+ }
293
+
294
+ /**
295
+ * Returns true when one primitive belongs to the requested outer copper
296
+ * face.
297
+ * @param {{ layerId?: number, layerCode?: number }} primitive
298
+ * @param {'top' | 'bottom'} side
299
+ * @returns {boolean}
300
+ */
301
+ static #matchesCopperLayer(primitive, side) {
302
+ const layerId = Number(
303
+ primitive?.layerId ?? primitive?.layerCode ?? NaN
304
+ )
305
+
306
+ return side === 'bottom'
307
+ ? layerId === PcbScene3dCopperFactory.#BOTTOM_COPPER_LAYER_ID
308
+ : layerId === PcbScene3dCopperFactory.#TOP_COPPER_LAYER_ID
309
+ }
310
+
311
+ /**
312
+ * Appends one widened track quad as two triangles.
313
+ * @param {number[]} positions
314
+ * @param {{ x: number, y: number }} start
315
+ * @param {{ x: number, y: number }} end
316
+ * @param {number} width
317
+ * @param {number} z
318
+ * @returns {void}
319
+ */
320
+ static #appendTrackTriangles(positions, start, end, width, z) {
321
+ const dx = end.x - start.x
322
+ const dy = end.y - start.y
323
+ const length = Math.hypot(dx, dy)
324
+ const halfWidth = Math.max(Number(width || 0), 1) / 2
325
+
326
+ if (length <= 0.001) {
327
+ const minX = start.x - halfWidth
328
+ const maxX = start.x + halfWidth
329
+ const minY = start.y - halfWidth
330
+ const maxY = start.y + halfWidth
331
+
332
+ PcbScene3dCopperFactory.#appendDiscTriangles(
333
+ positions,
334
+ { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
335
+ halfWidth,
336
+ z
337
+ )
338
+ return
339
+ }
340
+
341
+ const normalX = (-dy / length) * halfWidth
342
+ const normalY = (dx / length) * halfWidth
343
+
344
+ PcbScene3dCopperFactory.#appendQuadTriangles(
345
+ positions,
346
+ { x: start.x + normalX, y: start.y + normalY },
347
+ { x: end.x + normalX, y: end.y + normalY },
348
+ { x: end.x - normalX, y: end.y - normalY },
349
+ { x: start.x - normalX, y: start.y - normalY },
350
+ z
351
+ )
352
+ PcbScene3dCopperFactory.#appendDiscTriangles(
353
+ positions,
354
+ start,
355
+ halfWidth,
356
+ z
357
+ )
358
+ PcbScene3dCopperFactory.#appendDiscTriangles(
359
+ positions,
360
+ end,
361
+ halfWidth,
362
+ z
363
+ )
364
+ }
365
+
366
+ /**
367
+ * Appends one widened arc band as triangles.
368
+ * @param {number[]} positions
369
+ * @param {{ x: number, y: number }} center
370
+ * @param {{ radius?: number, width?: number, startAngle?: number, endAngle?: number }} arc
371
+ * @param {number} z
372
+ * @param {boolean} mirrorY
373
+ * @returns {void}
374
+ */
375
+ static #appendArcTriangles(positions, center, arc, z, mirrorY) {
376
+ const strokeWidth = Math.max(Number(arc?.width || 0), 1)
377
+ const radius = Math.max(Number(arc?.radius || 0), strokeWidth / 2, 0.8)
378
+ const outerRadius = radius + strokeWidth / 2
379
+ const innerRadius = Math.max(radius - strokeWidth / 2, 0)
380
+ const startAngleRad = (Number(arc?.startAngle || 0) * Math.PI) / 180
381
+ const deltaAngleDeg = PcbScene3dArcUtils.resolveSweepDelta(
382
+ Number(arc?.startAngle || 0),
383
+ Number(arc?.endAngle || 0)
384
+ )
385
+ const isFullCircle =
386
+ Math.abs(deltaAngleDeg) <=
387
+ PcbScene3dCopperFactory.#FULL_CIRCLE_EPSILON ||
388
+ Math.abs(deltaAngleDeg) >=
389
+ 360 - PcbScene3dCopperFactory.#FULL_CIRCLE_EPSILON
390
+ const deltaAngleRad = isFullCircle
391
+ ? Math.PI * 2
392
+ : (deltaAngleDeg * Math.PI) / 180
393
+ const segments = Math.max(
394
+ isFullCircle ? 20 : 8,
395
+ Math.ceil((Math.abs(deltaAngleRad) / Math.PI) * 18)
396
+ )
397
+ const yDirection = mirrorY ? -1 : 1
398
+
399
+ for (let index = 0; index < segments; index += 1) {
400
+ const startAngle =
401
+ startAngleRad + (deltaAngleRad * index) / segments
402
+ const endAngle =
403
+ startAngleRad + (deltaAngleRad * (index + 1)) / segments
404
+ const outerStart = {
405
+ x: center.x + Math.cos(startAngle) * outerRadius,
406
+ y: center.y + Math.sin(startAngle) * outerRadius * yDirection
407
+ }
408
+ const outerEnd = {
409
+ x: center.x + Math.cos(endAngle) * outerRadius,
410
+ y: center.y + Math.sin(endAngle) * outerRadius * yDirection
411
+ }
412
+
413
+ if (innerRadius <= 0.001) {
414
+ PcbScene3dCopperFactory.#appendTriangle(
415
+ positions,
416
+ { x: center.x, y: center.y },
417
+ outerStart,
418
+ outerEnd,
419
+ z
420
+ )
421
+ continue
422
+ }
423
+
424
+ const innerStart = {
425
+ x: center.x + Math.cos(startAngle) * innerRadius,
426
+ y: center.y + Math.sin(startAngle) * innerRadius * yDirection
427
+ }
428
+ const innerEnd = {
429
+ x: center.x + Math.cos(endAngle) * innerRadius,
430
+ y: center.y + Math.sin(endAngle) * innerRadius * yDirection
431
+ }
432
+
433
+ PcbScene3dCopperFactory.#appendQuadTriangles(
434
+ positions,
435
+ outerStart,
436
+ outerEnd,
437
+ innerEnd,
438
+ innerStart,
439
+ z
440
+ )
441
+ }
442
+
443
+ if (!isFullCircle) {
444
+ PcbScene3dCopperFactory.#appendDiscTriangles(
445
+ positions,
446
+ {
447
+ x: center.x + Math.cos(startAngleRad) * radius,
448
+ y: center.y + Math.sin(startAngleRad) * radius * yDirection
449
+ },
450
+ strokeWidth / 2,
451
+ z
452
+ )
453
+ PcbScene3dCopperFactory.#appendDiscTriangles(
454
+ positions,
455
+ {
456
+ x:
457
+ center.x +
458
+ Math.cos(startAngleRad + deltaAngleRad) * radius,
459
+ y:
460
+ center.y +
461
+ Math.sin(startAngleRad + deltaAngleRad) *
462
+ radius *
463
+ yDirection
464
+ },
465
+ strokeWidth / 2,
466
+ z
467
+ )
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Appends one rectangle as two triangles.
473
+ * @param {number[]} positions
474
+ * @param {{ x: number, y: number }} a
475
+ * @param {{ x: number, y: number }} b
476
+ * @param {{ x: number, y: number }} c
477
+ * @param {{ x: number, y: number }} d
478
+ * @param {number} z
479
+ * @returns {void}
480
+ */
481
+ static #appendQuadTriangles(positions, a, b, c, d, z) {
482
+ PcbScene3dCopperFactory.#appendTriangle(positions, a, b, c, z)
483
+ PcbScene3dCopperFactory.#appendTriangle(positions, a, c, d, z)
484
+ }
485
+
486
+ /**
487
+ * Appends one filled circle fan.
488
+ * @param {number[]} positions
489
+ * @param {{ x: number, y: number }} center
490
+ * @param {number} radius
491
+ * @param {number} z
492
+ * @returns {void}
493
+ */
494
+ static #appendDiscTriangles(positions, center, radius, z) {
495
+ const safeRadius = Math.max(Number(radius || 0), 0)
496
+ if (safeRadius <= 0) {
497
+ return
498
+ }
499
+
500
+ for (
501
+ let index = 0;
502
+ index < PcbScene3dCopperFactory.#ROUND_CAP_SEGMENTS;
503
+ index += 1
504
+ ) {
505
+ const startAngle =
506
+ (Math.PI * 2 * index) /
507
+ PcbScene3dCopperFactory.#ROUND_CAP_SEGMENTS
508
+ const endAngle =
509
+ (Math.PI * 2 * (index + 1)) /
510
+ PcbScene3dCopperFactory.#ROUND_CAP_SEGMENTS
511
+
512
+ PcbScene3dCopperFactory.#appendTriangle(
513
+ positions,
514
+ center,
515
+ {
516
+ x: center.x + Math.cos(startAngle) * safeRadius,
517
+ y: center.y + Math.sin(startAngle) * safeRadius
518
+ },
519
+ {
520
+ x: center.x + Math.cos(endAngle) * safeRadius,
521
+ y: center.y + Math.sin(endAngle) * safeRadius
522
+ },
523
+ z
524
+ )
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Normalizes one board point and optionally mirrors it around the local
530
+ * X axis so underside copper keeps its world position after the face flip
531
+ * rotates it below the board.
532
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
533
+ * @param {number} x
534
+ * @param {number} y
535
+ * @param {boolean} mirrorY
536
+ * @returns {{ x: number, y: number }}
537
+ */
538
+ static #normalizePoint(normalizeBoardPoint, x, y, mirrorY) {
539
+ const point = normalizeBoardPoint(x, y)
540
+
541
+ return {
542
+ x: point.x,
543
+ y: mirrorY ? -point.y : point.y
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Appends one triangle into the position buffer.
549
+ * @param {number[]} positions
550
+ * @param {{ x: number, y: number }} a
551
+ * @param {{ x: number, y: number }} b
552
+ * @param {{ x: number, y: number }} c
553
+ * @param {number} z
554
+ * @returns {void}
555
+ */
556
+ static #appendTriangle(positions, a, b, c, z) {
557
+ positions.push(a.x, a.y, z, b.x, b.y, z, c.x, c.y, z)
558
+ }
559
+ }