pcb-scene3d-viewer 1.1.49 → 1.2.0
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.
- package/README.md +46 -21
- package/docs/api.md +109 -8
- package/docs/circuitjson.md +143 -29
- package/docs/model-format.md +50 -7
- package/docs/release-notes-v1.2.0.md +122 -0
- package/docs/testing.md +30 -1
- package/package.json +6 -3
- package/spec/library-scope.md +10 -2
- package/src/CircuitJsonCadModelAssetResolver.mjs +697 -83
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +43 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +40 -18
- package/src/PcbAssemblyModelMeshLoader.mjs +101 -150
- package/src/PcbAssemblyPadMeshBuilder.mjs +22 -0
- package/src/PcbModelArchiveExporter.mjs +28 -112
- package/src/PcbModelArchiveSourceBundle.mjs +326 -0
- package/src/PcbScene3dAabbIndex.mjs +464 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -1
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +9 -5
- package/src/PcbScene3dBoardMaterialPalette.mjs +29 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +11 -118
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +65 -39
- package/src/PcbScene3dCircuitJsonAdapter.mjs +151 -48
- package/src/PcbScene3dCircuitJsonDrillDetail.mjs +31 -0
- package/src/PcbScene3dCircuitJsonGeometry.mjs +184 -33
- package/src/PcbScene3dCircuitJsonInput.mjs +132 -0
- package/src/PcbScene3dCircuitJsonModelAsset.mjs +40 -0
- package/src/PcbScene3dController.mjs +40 -34
- package/src/PcbScene3dCopperFactory.mjs +36 -22
- package/src/PcbScene3dCopperFillAreaClipper.mjs +133 -235
- package/src/PcbScene3dCopperFillCoverageContext.mjs +204 -0
- package/src/PcbScene3dCopperFillLoopSetResolver.mjs +192 -0
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +77 -295
- package/src/PcbScene3dCopperTextFactory.mjs +12 -4
- package/src/PcbScene3dCutoutCircleDetector.mjs +34 -17
- package/src/PcbScene3dCutoutGeometryFilter.mjs +104 -269
- package/src/PcbScene3dCutoutGridIndex.mjs +184 -0
- package/src/PcbScene3dDeferredModelFinalizer.mjs +52 -0
- package/src/PcbScene3dDescriptorSafeRecord.mjs +38 -0
- package/src/PcbScene3dDrillCutoutFilter.mjs +149 -143
- package/src/PcbScene3dDrillPathFactory.mjs +86 -16
- package/src/PcbScene3dDrillVoidFactory.mjs +35 -10
- package/src/PcbScene3dExternalModelGroupLoader.mjs +472 -31
- package/src/PcbScene3dExternalModels.mjs +23 -24
- package/src/PcbScene3dFacetedModelGroupBuilder.mjs +217 -0
- package/src/PcbScene3dGeometryZCompressor.mjs +4 -2
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +18 -4
- package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +75 -10
- package/src/PcbScene3dModelContent.mjs +236 -0
- package/src/PcbScene3dModelFetchPolicy.mjs +304 -0
- package/src/PcbScene3dModelIdentity.mjs +106 -0
- package/src/PcbScene3dPlatedDrillSpecResolver.mjs +141 -0
- package/src/PcbScene3dPreparedPolygon.mjs +709 -0
- package/src/PcbScene3dPreparedPolygonSet.mjs +70 -0
- package/src/PcbScene3dRuntime.mjs +45 -45
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +85 -1
- package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +4 -2
- package/src/PcbScene3dShellRenderer.mjs +75 -6
- package/src/PcbScene3dSilkscreenCutoutContext.mjs +255 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +87 -127
- package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +11 -5
- package/src/PcbScene3dStepLoader.mjs +11 -10
- package/src/PcbScene3dText.mjs +1 -1
- package/src/PcbScene3dTriangleVertexQueryBounds.mjs +302 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { PcbScene3dAabbIndex } from './PcbScene3dAabbIndex.mjs'
|
|
2
|
+
import { PcbScene3dPreparedPolygon } from './PcbScene3dPreparedPolygon.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Immutable request-scoped acceleration for normalized copper fill loop sets.
|
|
6
|
+
*/
|
|
7
|
+
export class PcbScene3dCopperFillCoverageContext {
|
|
8
|
+
static #GEOMETRY_EPSILON = 0.001
|
|
9
|
+
|
|
10
|
+
/** @type {{ outer: PcbScene3dPreparedPolygon, holes: PcbScene3dPreparedPolygon[], bounds: object, sourceIndex: number }[]} */
|
|
11
|
+
#areas
|
|
12
|
+
|
|
13
|
+
/** @type {PcbScene3dAabbIndex} */
|
|
14
|
+
#index
|
|
15
|
+
|
|
16
|
+
/** @type {object[]} */
|
|
17
|
+
#loopSets
|
|
18
|
+
|
|
19
|
+
/** @type {object[] | null} */
|
|
20
|
+
#sourceLoopSets
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates one context from already-normalized ordered loop sets.
|
|
24
|
+
* @param {{ outer?: number[][], holes?: number[][][] }[]} loopSets Ordered fill islands.
|
|
25
|
+
* @returns {PcbScene3dCopperFillCoverageContext}
|
|
26
|
+
*/
|
|
27
|
+
static fromLoopSets(loopSets) {
|
|
28
|
+
return new PcbScene3dCopperFillCoverageContext(loopSets)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prepares every loop coordinate once and builds one top-level area index.
|
|
33
|
+
* @param {{ outer?: number[][], holes?: number[][][] }[]} loopSets Ordered fill islands.
|
|
34
|
+
*/
|
|
35
|
+
constructor(loopSets) {
|
|
36
|
+
this.#sourceLoopSets = Array.isArray(loopSets) ? loopSets : null
|
|
37
|
+
this.#loopSets = Object.freeze(Array.from(loopSets || []))
|
|
38
|
+
this.#areas = Object.freeze(
|
|
39
|
+
Array.from(this.#loopSets, (loopSet, sourceIndex) =>
|
|
40
|
+
PcbScene3dCopperFillCoverageContext.#prepareArea(
|
|
41
|
+
loopSet,
|
|
42
|
+
sourceIndex
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
this.#index = new PcbScene3dAabbIndex(this.#areas, {
|
|
47
|
+
resolveBounds: PcbScene3dCopperFillCoverageContext.#resolveBounds,
|
|
48
|
+
resolveSourceIndex:
|
|
49
|
+
PcbScene3dCopperFillCoverageContext.#resolveSourceIndex
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Returns the immutable number of prepared fill areas.
|
|
55
|
+
* @returns {number}
|
|
56
|
+
*/
|
|
57
|
+
get areaCount() {
|
|
58
|
+
return this.#areas.length
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns whether the request array and loop-set order match preparation.
|
|
63
|
+
* @param {object[]} loopSets Active normalized fill islands.
|
|
64
|
+
* @returns {boolean}
|
|
65
|
+
*/
|
|
66
|
+
matchesLoopSets(loopSets) {
|
|
67
|
+
if (
|
|
68
|
+
!Array.isArray(loopSets) ||
|
|
69
|
+
loopSets !== this.#sourceLoopSets ||
|
|
70
|
+
loopSets.length !== this.#loopSets.length
|
|
71
|
+
) {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (let index = 0; index < loopSets.length; index += 1) {
|
|
76
|
+
if (loopSets[index] !== this.#loopSets[index]) {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Appends stable broad-phase area candidates into a caller-owned target.
|
|
86
|
+
* @param {{ minX: number, maxX: number, minY: number, maxY: number }} triangleBounds Triangle bounds.
|
|
87
|
+
* @param {object[]} [target] Candidate accumulator.
|
|
88
|
+
* @param {{ beforeSourceIndex?: number, allowedSourceIndexes?: Set<number> | number[] | null }} [options] Source-order filters.
|
|
89
|
+
* @returns {object[]}
|
|
90
|
+
*/
|
|
91
|
+
queryAreas(triangleBounds, target = [], options = {}) {
|
|
92
|
+
const candidates = this.#index.query(triangleBounds, {
|
|
93
|
+
epsilon: PcbScene3dCopperFillCoverageContext.#GEOMETRY_EPSILON,
|
|
94
|
+
stable: true
|
|
95
|
+
})
|
|
96
|
+
const beforeSourceIndex = options.beforeSourceIndex ?? Infinity
|
|
97
|
+
const allowedSourceIndexes = options.allowedSourceIndexes ?? null
|
|
98
|
+
|
|
99
|
+
for (const area of candidates) {
|
|
100
|
+
if (
|
|
101
|
+
area.sourceIndex < beforeSourceIndex &&
|
|
102
|
+
PcbScene3dCopperFillCoverageContext.#isAllowedSourceIndex(
|
|
103
|
+
area.sourceIndex,
|
|
104
|
+
allowedSourceIndexes
|
|
105
|
+
)
|
|
106
|
+
) {
|
|
107
|
+
target.push(area)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return target
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Prepares one outer loop and its authored holes.
|
|
116
|
+
* @param {{ outer?: number[][], holes?: number[][][] }} loopSet Source loop set.
|
|
117
|
+
* @param {number} sourceIndex Flattened source position.
|
|
118
|
+
* @returns {{ outer: PcbScene3dPreparedPolygon, holes: PcbScene3dPreparedPolygon[], bounds: object, sourceIndex: number }}
|
|
119
|
+
*/
|
|
120
|
+
static #prepareArea(loopSet, sourceIndex) {
|
|
121
|
+
const outer = PcbScene3dCopperFillCoverageContext.#prepareLoop(
|
|
122
|
+
loopSet?.outer,
|
|
123
|
+
sourceIndex
|
|
124
|
+
)
|
|
125
|
+
const holes = Object.freeze(
|
|
126
|
+
Array.from(loopSet?.holes || [], (hole) =>
|
|
127
|
+
PcbScene3dCopperFillCoverageContext.#prepareLoop(
|
|
128
|
+
hole,
|
|
129
|
+
sourceIndex
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return Object.freeze({
|
|
135
|
+
outer,
|
|
136
|
+
holes,
|
|
137
|
+
bounds: outer.bounds,
|
|
138
|
+
sourceIndex
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Converts one normalized pair loop to numeric point objects once.
|
|
144
|
+
* @param {number[][] | undefined} loop Source loop.
|
|
145
|
+
* @param {number} sourceIndex Flattened source position.
|
|
146
|
+
* @returns {PcbScene3dPreparedPolygon}
|
|
147
|
+
*/
|
|
148
|
+
static #prepareLoop(loop, sourceIndex) {
|
|
149
|
+
const source = Array.isArray(loop) ? loop : []
|
|
150
|
+
const points = Object.freeze(
|
|
151
|
+
source.map((point) => {
|
|
152
|
+
const x = Number(point?.[0])
|
|
153
|
+
const y = Number(point?.[1])
|
|
154
|
+
return Object.freeze({ x, y })
|
|
155
|
+
})
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return new PcbScene3dPreparedPolygon(points, {
|
|
159
|
+
source,
|
|
160
|
+
sourceIndex,
|
|
161
|
+
epsilon: PcbScene3dCopperFillCoverageContext.#GEOMETRY_EPSILON,
|
|
162
|
+
pointRepresentation: 'numeric'
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Returns whether an optional source-index collection permits one area.
|
|
168
|
+
* @param {number} sourceIndex Candidate source position.
|
|
169
|
+
* @param {Set<number> | number[] | null} allowedSourceIndexes Optional allow set.
|
|
170
|
+
* @returns {boolean}
|
|
171
|
+
*/
|
|
172
|
+
static #isAllowedSourceIndex(sourceIndex, allowedSourceIndexes) {
|
|
173
|
+
if (allowedSourceIndexes === null) {
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (typeof allowedSourceIndexes?.has === 'function') {
|
|
178
|
+
return allowedSourceIndexes.has(sourceIndex)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
Array.isArray(allowedSourceIndexes) &&
|
|
183
|
+
allowedSourceIndexes.includes(sourceIndex)
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolves area bounds for the AABB index.
|
|
189
|
+
* @param {{ bounds: object }} area Prepared area.
|
|
190
|
+
* @returns {object}
|
|
191
|
+
*/
|
|
192
|
+
static #resolveBounds(area) {
|
|
193
|
+
return area.bounds
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolves stable flattened fill-island order for the AABB index.
|
|
198
|
+
* @param {{ sourceIndex: number }} area Prepared area.
|
|
199
|
+
* @returns {number}
|
|
200
|
+
*/
|
|
201
|
+
static #resolveSourceIndex(area) {
|
|
202
|
+
return area.sourceIndex
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { PcbAssemblyFillGeometryResolver } from './PcbAssemblyFillGeometryResolver.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves copper fills into canonical side-local polygon loop sets.
|
|
5
|
+
*/
|
|
6
|
+
export class PcbScene3dCopperFillLoopSetResolver {
|
|
7
|
+
static #AREA_EPSILON = 0.001
|
|
8
|
+
static #GEOMETRY_EPSILON = 0.001
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolves every valid fill island in source order.
|
|
12
|
+
* @param {object[]} fills Filled copper primitives.
|
|
13
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
14
|
+
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
15
|
+
* @returns {{ outer: number[][], holes: number[][][], bounds: { minX: number, minY: number, maxX: number, maxY: number } }[]}
|
|
16
|
+
*/
|
|
17
|
+
static resolve(fills, normalizeBoardPoint, mirrorY) {
|
|
18
|
+
const loopSets = []
|
|
19
|
+
|
|
20
|
+
for (const fill of fills || []) {
|
|
21
|
+
for (const loops of PcbAssemblyFillGeometryResolver.resolveAll(
|
|
22
|
+
fill
|
|
23
|
+
)) {
|
|
24
|
+
const loopSet =
|
|
25
|
+
PcbScene3dCopperFillLoopSetResolver.#normalizeLoopSet(
|
|
26
|
+
loops,
|
|
27
|
+
normalizeBoardPoint,
|
|
28
|
+
mirrorY
|
|
29
|
+
)
|
|
30
|
+
if (loopSet) {
|
|
31
|
+
loopSets.push(loopSet)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return loopSets
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalizes one fill island and discards degenerate outer geometry.
|
|
41
|
+
* @param {{ outer?: any[], holes?: any[][] }} loops Source loops.
|
|
42
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
43
|
+
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
44
|
+
* @returns {{ outer: number[][], holes: number[][][], bounds: { minX: number, minY: number, maxX: number, maxY: number } } | null}
|
|
45
|
+
*/
|
|
46
|
+
static #normalizeLoopSet(loops, normalizeBoardPoint, mirrorY) {
|
|
47
|
+
const outer = PcbScene3dCopperFillLoopSetResolver.#normalizeLoop(
|
|
48
|
+
loops?.outer,
|
|
49
|
+
normalizeBoardPoint,
|
|
50
|
+
mirrorY
|
|
51
|
+
)
|
|
52
|
+
if (!PcbScene3dCopperFillLoopSetResolver.#isValidLoop(outer)) {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const holes = []
|
|
57
|
+
for (const sourceHole of loops?.holes || []) {
|
|
58
|
+
const hole = PcbScene3dCopperFillLoopSetResolver.#normalizeLoop(
|
|
59
|
+
sourceHole,
|
|
60
|
+
normalizeBoardPoint,
|
|
61
|
+
mirrorY
|
|
62
|
+
)
|
|
63
|
+
if (PcbScene3dCopperFillLoopSetResolver.#isValidLoop(hole)) {
|
|
64
|
+
holes.push(hole)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
outer,
|
|
70
|
+
holes,
|
|
71
|
+
bounds: PcbScene3dCopperFillLoopSetResolver.#resolveBounds(outer)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Converts one loop into clean finite side-local coordinate pairs.
|
|
77
|
+
* @param {any[]} loop Source points.
|
|
78
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint Board normalizer.
|
|
79
|
+
* @param {boolean} mirrorY Whether to mirror underside Y coordinates.
|
|
80
|
+
* @returns {number[][]}
|
|
81
|
+
*/
|
|
82
|
+
static #normalizeLoop(loop, normalizeBoardPoint, mirrorY) {
|
|
83
|
+
const points = []
|
|
84
|
+
|
|
85
|
+
for (const point of loop || []) {
|
|
86
|
+
const normalized = normalizeBoardPoint(
|
|
87
|
+
Number(point?.x ?? point?.[0]),
|
|
88
|
+
Number(point?.y ?? point?.[1])
|
|
89
|
+
)
|
|
90
|
+
const x = Number(normalized?.x)
|
|
91
|
+
const y = mirrorY ? -Number(normalized?.y) : Number(normalized?.y)
|
|
92
|
+
|
|
93
|
+
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
94
|
+
points.push([x, y])
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return PcbScene3dCopperFillLoopSetResolver.#cleanLoop(points)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Removes consecutive duplicate and explicit closing points.
|
|
103
|
+
* @param {number[][]} points Candidate points.
|
|
104
|
+
* @returns {number[][]}
|
|
105
|
+
*/
|
|
106
|
+
static #cleanLoop(points) {
|
|
107
|
+
const loop = []
|
|
108
|
+
|
|
109
|
+
for (const point of points || []) {
|
|
110
|
+
const previous = loop[loop.length - 1]
|
|
111
|
+
if (
|
|
112
|
+
previous &&
|
|
113
|
+
Math.abs(previous[0] - point[0]) <
|
|
114
|
+
PcbScene3dCopperFillLoopSetResolver.#GEOMETRY_EPSILON &&
|
|
115
|
+
Math.abs(previous[1] - point[1]) <
|
|
116
|
+
PcbScene3dCopperFillLoopSetResolver.#GEOMETRY_EPSILON
|
|
117
|
+
) {
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
loop.push(point)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const first = loop[0]
|
|
124
|
+
const last = loop[loop.length - 1]
|
|
125
|
+
if (
|
|
126
|
+
first &&
|
|
127
|
+
last &&
|
|
128
|
+
Math.abs(first[0] - last[0]) <
|
|
129
|
+
PcbScene3dCopperFillLoopSetResolver.#GEOMETRY_EPSILON &&
|
|
130
|
+
Math.abs(first[1] - last[1]) <
|
|
131
|
+
PcbScene3dCopperFillLoopSetResolver.#GEOMETRY_EPSILON
|
|
132
|
+
) {
|
|
133
|
+
loop.pop()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return loop
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Returns true when one loop has sufficient non-collinear area.
|
|
141
|
+
* @param {number[][]} loop Candidate loop.
|
|
142
|
+
* @returns {boolean}
|
|
143
|
+
*/
|
|
144
|
+
static #isValidLoop(loop) {
|
|
145
|
+
return (
|
|
146
|
+
Array.isArray(loop) &&
|
|
147
|
+
loop.length >= 3 &&
|
|
148
|
+
Math.abs(PcbScene3dCopperFillLoopSetResolver.#signedArea(loop)) >
|
|
149
|
+
PcbScene3dCopperFillLoopSetResolver.#AREA_EPSILON
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Computes one loop's signed shoelace area.
|
|
155
|
+
* @param {number[][]} loop Candidate loop.
|
|
156
|
+
* @returns {number}
|
|
157
|
+
*/
|
|
158
|
+
static #signedArea(loop) {
|
|
159
|
+
let area = 0
|
|
160
|
+
|
|
161
|
+
for (let index = 0; index < loop.length; index += 1) {
|
|
162
|
+
const current = loop[index]
|
|
163
|
+
const next = loop[(index + 1) % loop.length]
|
|
164
|
+
area += current[0] * next[1] - next[0] * current[1]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return area / 2
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Resolves finite axis-aligned bounds for a valid loop.
|
|
172
|
+
* @param {number[][]} loop Candidate loop.
|
|
173
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number }}
|
|
174
|
+
*/
|
|
175
|
+
static #resolveBounds(loop) {
|
|
176
|
+
const bounds = {
|
|
177
|
+
minX: Infinity,
|
|
178
|
+
minY: Infinity,
|
|
179
|
+
maxX: -Infinity,
|
|
180
|
+
maxY: -Infinity
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const point of loop) {
|
|
184
|
+
bounds.minX = Math.min(bounds.minX, point[0])
|
|
185
|
+
bounds.minY = Math.min(bounds.minY, point[1])
|
|
186
|
+
bounds.maxX = Math.max(bounds.maxX, point[0])
|
|
187
|
+
bounds.maxY = Math.max(bounds.maxY, point[1])
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return bounds
|
|
191
|
+
}
|
|
192
|
+
}
|