altium-toolkit 0.1.23 → 1.0.2
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 +4 -3
- package/docs/api.md +47 -8
- package/docs/model-format.md +30 -6
- package/package.json +2 -1
- package/spec/library-scope.md +2 -1
- package/src/core/altium/AltiumParser.mjs +17 -4
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +826 -0
- package/src/core/circuit-json/CircuitJsonModelAdapterPrimitives.mjs +354 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +83 -0
- package/src/core/netlist-query/CircuitTraversal.mjs +325 -0
- package/src/core/netlist-query/ComponentGrouping.mjs +355 -0
- package/src/core/netlist-query/LoadedDesignNetlistService.mjs +732 -0
- package/src/core/netlist-query/QueryNetlistBuilder.mjs +180 -0
- package/src/core/netlist-query/RegexPattern.mjs +89 -0
- package/src/netlist-query.mjs +12 -0
- package/src/parser.mjs +2 -0
- package/src/styles/altium-renderers.css +12 -1
- package/src/ui/PcbNativeTextKnockoutDetector.mjs +130 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +402 -0
- package/src/ui/PcbScene3dBuilder.mjs +16 -5
- package/src/ui/PcbScene3dDrillCutoutBuilder.mjs +26 -15
- package/src/ui/PcbSvgRenderer.mjs +83 -10
- package/src/ui/PcbTextPrimitiveRenderer.mjs +648 -19
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { ComponentGrouping } from './ComponentGrouping.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds compact query netlists from normalized toolkit document models.
|
|
9
|
+
*/
|
|
10
|
+
export class QueryNetlistBuilder {
|
|
11
|
+
/**
|
|
12
|
+
* Builds the query netlist for one document model.
|
|
13
|
+
* @param {object} documentModel Document model.
|
|
14
|
+
* @returns {{ nets: object, components: object }}
|
|
15
|
+
*/
|
|
16
|
+
static build(documentModel) {
|
|
17
|
+
const components =
|
|
18
|
+
QueryNetlistBuilder.#buildComponentDetails(documentModel)
|
|
19
|
+
const nets = {}
|
|
20
|
+
|
|
21
|
+
for (const net of documentModel?.schematic?.nets || []) {
|
|
22
|
+
const netName = String(net?.name || '').trim()
|
|
23
|
+
if (!netName) continue
|
|
24
|
+
|
|
25
|
+
for (const pin of net.pins || []) {
|
|
26
|
+
const refdes = QueryNetlistBuilder.#resolvePinRefdes(
|
|
27
|
+
pin,
|
|
28
|
+
components
|
|
29
|
+
)
|
|
30
|
+
const pinNumber = QueryNetlistBuilder.#resolvePinNumber(pin)
|
|
31
|
+
if (!refdes || !pinNumber) continue
|
|
32
|
+
|
|
33
|
+
nets[netName] ||= {}
|
|
34
|
+
const existing = nets[netName][refdes]
|
|
35
|
+
nets[netName][refdes] = QueryNetlistBuilder.#appendPin(
|
|
36
|
+
existing,
|
|
37
|
+
pinNumber
|
|
38
|
+
)
|
|
39
|
+
components[refdes] ||= { pins: {} }
|
|
40
|
+
components[refdes].pins[pinNumber] =
|
|
41
|
+
QueryNetlistBuilder.#pinEntry(pinNumber, pin?.name, netName)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { nets, components }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Builds component metadata from schematic, PCB, and BOM records.
|
|
50
|
+
* @param {object} documentModel Document model.
|
|
51
|
+
* @returns {object}
|
|
52
|
+
*/
|
|
53
|
+
static #buildComponentDetails(documentModel) {
|
|
54
|
+
const components = {}
|
|
55
|
+
|
|
56
|
+
for (const component of documentModel?.schematic?.components || []) {
|
|
57
|
+
const refdes = String(component?.designator || '').trim()
|
|
58
|
+
if (!refdes) continue
|
|
59
|
+
components[refdes] = {
|
|
60
|
+
...components[refdes],
|
|
61
|
+
value: component.value || components[refdes]?.value,
|
|
62
|
+
description:
|
|
63
|
+
component.description || components[refdes]?.description,
|
|
64
|
+
comment: component.comment || components[refdes]?.comment,
|
|
65
|
+
ownerIndex:
|
|
66
|
+
component.ownerIndex || components[refdes]?.ownerIndex,
|
|
67
|
+
dns:
|
|
68
|
+
component.dns ||
|
|
69
|
+
component.excludeFromBom ||
|
|
70
|
+
components[refdes]?.dns,
|
|
71
|
+
excludeFromBom:
|
|
72
|
+
component.excludeFromBom ||
|
|
73
|
+
components[refdes]?.excludeFromBom,
|
|
74
|
+
pins: components[refdes]?.pins || {}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const component of documentModel?.pcb?.components || []) {
|
|
79
|
+
const refdes = String(component?.designator || '').trim()
|
|
80
|
+
if (!refdes) continue
|
|
81
|
+
components[refdes] = {
|
|
82
|
+
...components[refdes],
|
|
83
|
+
description:
|
|
84
|
+
component.description ||
|
|
85
|
+
component.pattern ||
|
|
86
|
+
components[refdes]?.description,
|
|
87
|
+
value: component.value || components[refdes]?.value,
|
|
88
|
+
comment: component.comment || components[refdes]?.comment,
|
|
89
|
+
pins: components[refdes]?.pins || {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const row of documentModel?.bom || []) {
|
|
94
|
+
for (const refdes of row.designators || []) {
|
|
95
|
+
const normalizedRefdes = String(refdes || '').trim()
|
|
96
|
+
if (!normalizedRefdes) continue
|
|
97
|
+
components[normalizedRefdes] = {
|
|
98
|
+
...components[normalizedRefdes],
|
|
99
|
+
mpn: row.pattern || components[normalizedRefdes]?.mpn,
|
|
100
|
+
description:
|
|
101
|
+
row.source || components[normalizedRefdes]?.description,
|
|
102
|
+
value: row.value || components[normalizedRefdes]?.value,
|
|
103
|
+
pins: components[normalizedRefdes]?.pins || {}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return components
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolves a pin's owning reference designator.
|
|
113
|
+
* @param {object} pin Net pin.
|
|
114
|
+
* @param {object} components Component details.
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
static #resolvePinRefdes(pin, components) {
|
|
118
|
+
const direct = String(
|
|
119
|
+
pin?.refdes ||
|
|
120
|
+
pin?.componentRefdes ||
|
|
121
|
+
pin?.componentDesignator ||
|
|
122
|
+
pin?.ownerDesignator ||
|
|
123
|
+
''
|
|
124
|
+
).trim()
|
|
125
|
+
if (direct) return direct
|
|
126
|
+
|
|
127
|
+
const ownerIndex = String(pin?.ownerIndex || '').trim()
|
|
128
|
+
if (!ownerIndex) return ''
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
Object.entries(components).find(([, component]) => {
|
|
132
|
+
return String(component.ownerIndex || '') === ownerIndex
|
|
133
|
+
})?.[0] || ''
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolves a pin number.
|
|
139
|
+
* @param {object} pin Net pin.
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
static #resolvePinNumber(pin) {
|
|
143
|
+
return String(
|
|
144
|
+
pin?.pinNumber || pin?.number || pin?.designator || ''
|
|
145
|
+
).trim()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Appends a pin to a compact net connection value.
|
|
150
|
+
* @param {string | string[] | undefined} existing Existing pins.
|
|
151
|
+
* @param {string} pinNumber Pin number.
|
|
152
|
+
* @returns {string | string[]}
|
|
153
|
+
*/
|
|
154
|
+
static #appendPin(existing, pinNumber) {
|
|
155
|
+
if (!existing) return pinNumber
|
|
156
|
+
const pins = Array.isArray(existing) ? existing : [existing]
|
|
157
|
+
if (!pins.includes(pinNumber)) {
|
|
158
|
+
pins.push(pinNumber)
|
|
159
|
+
}
|
|
160
|
+
return ComponentGrouping.compactArray(
|
|
161
|
+
pins.sort(ComponentGrouping.naturalSort)
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Builds a compact pin entry.
|
|
167
|
+
* @param {string} pinNumber Pin number.
|
|
168
|
+
* @param {string | undefined} pinName Pin name.
|
|
169
|
+
* @param {string} netName Net name.
|
|
170
|
+
* @returns {string | { name: string, net: string }}
|
|
171
|
+
*/
|
|
172
|
+
static #pinEntry(pinNumber, pinName, netName) {
|
|
173
|
+
const normalizedName = String(pinName || '').trim()
|
|
174
|
+
if (normalizedName && normalizedName !== pinNumber) {
|
|
175
|
+
return { name: normalizedName, net: netName }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return netName
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regex helpers for netlist query tools.
|
|
7
|
+
*/
|
|
8
|
+
export class RegexPattern {
|
|
9
|
+
/**
|
|
10
|
+
* Parses a user regex pattern into a JavaScript RegExp.
|
|
11
|
+
* @param {string} pattern User-provided pattern.
|
|
12
|
+
* @param {string} [flags] RegExp flags.
|
|
13
|
+
* @returns {{ regex: RegExp } | { error: string }}
|
|
14
|
+
*/
|
|
15
|
+
static parse(pattern, flags = 'i') {
|
|
16
|
+
const normalizedPattern = RegexPattern.#normalizeInlineFlags(pattern)
|
|
17
|
+
const normalizedFlags = RegexPattern.#normalizeFlags(
|
|
18
|
+
flags,
|
|
19
|
+
normalizedPattern.forceIgnoreCase
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return {
|
|
24
|
+
regex: new RegExp(normalizedPattern.pattern, normalizedFlags)
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return {
|
|
28
|
+
error:
|
|
29
|
+
'Invalid regex pattern ' +
|
|
30
|
+
JSON.stringify(String(pattern || '')) +
|
|
31
|
+
': ' +
|
|
32
|
+
(error instanceof Error ? error.message : String(error))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns true when a pattern matches every candidate value.
|
|
39
|
+
* @param {string} pattern User-provided pattern.
|
|
40
|
+
* @param {string[]} candidates Candidate values.
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
static rejectsBroadMatch(pattern, candidates) {
|
|
44
|
+
const parsed = RegexPattern.parse(pattern)
|
|
45
|
+
if (parsed.error || !Array.isArray(candidates) || !candidates.length) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return candidates.every((candidate) => {
|
|
50
|
+
parsed.regex.lastIndex = 0
|
|
51
|
+
return parsed.regex.test(String(candidate || ''))
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalizes a leading `(?i)` flag into JavaScript RegExp flags.
|
|
57
|
+
* @param {string} pattern User-provided pattern.
|
|
58
|
+
* @returns {{ pattern: string, forceIgnoreCase: boolean }}
|
|
59
|
+
*/
|
|
60
|
+
static #normalizeInlineFlags(pattern) {
|
|
61
|
+
const source = String(pattern || '')
|
|
62
|
+
if (source.startsWith('(?i)')) {
|
|
63
|
+
return {
|
|
64
|
+
pattern: source.slice(4),
|
|
65
|
+
forceIgnoreCase: true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
pattern: source,
|
|
71
|
+
forceIgnoreCase: false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Adds the ignore-case flag when requested.
|
|
77
|
+
* @param {string} flags RegExp flags.
|
|
78
|
+
* @param {boolean} forceIgnoreCase Whether to force `i`.
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
static #normalizeFlags(flags, forceIgnoreCase) {
|
|
82
|
+
const uniqueFlags = new Set(String(flags || '').split(''))
|
|
83
|
+
if (forceIgnoreCase) {
|
|
84
|
+
uniqueFlags.add('i')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [...uniqueFlags].join('')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
export { RegexPattern } from './core/netlist-query/RegexPattern.mjs'
|
|
6
|
+
export {
|
|
7
|
+
ComponentGrouping,
|
|
8
|
+
MPN_MISSING_NOTE
|
|
9
|
+
} from './core/netlist-query/ComponentGrouping.mjs'
|
|
10
|
+
export { CircuitTraversal } from './core/netlist-query/CircuitTraversal.mjs'
|
|
11
|
+
export { QueryNetlistBuilder } from './core/netlist-query/QueryNetlistBuilder.mjs'
|
|
12
|
+
export { LoadedDesignNetlistService } from './core/netlist-query/LoadedDesignNetlistService.mjs'
|
package/src/parser.mjs
CHANGED
|
@@ -11,6 +11,8 @@ export { AltiumLayoutParser } from './core/altium/AltiumLayoutParser.mjs'
|
|
|
11
11
|
export { AsciiRecordParser } from './core/altium/AsciiRecordParser.mjs'
|
|
12
12
|
export { ParserUtils } from './core/altium/ParserUtils.mjs'
|
|
13
13
|
export { NormalizedModelSchema } from './core/altium/NormalizedModelSchema.mjs'
|
|
14
|
+
export { CircuitJsonModelSchema } from './core/circuit-json/CircuitJsonModelSchema.mjs'
|
|
15
|
+
export { CircuitJsonModelAdapter } from './core/circuit-json/CircuitJsonModelAdapter.mjs'
|
|
14
16
|
export { PcbBinaryPrimitiveParser } from './core/altium/PcbBinaryPrimitiveParser.mjs'
|
|
15
17
|
export { PcbBoardRegionSemanticsParser } from './core/altium/PcbBoardRegionSemanticsParser.mjs'
|
|
16
18
|
export { PcbComponentPrimitiveIndexer } from './core/altium/PcbComponentPrimitiveIndexer.mjs'
|
|
@@ -141,6 +141,7 @@
|
|
|
141
141
|
--pcb-via-hole-fill: #0f746c;
|
|
142
142
|
--pcb-footprint-fill: rgba(247, 230, 117, 0.14);
|
|
143
143
|
--pcb-footprint-track-color: rgba(237, 172, 36, 1);
|
|
144
|
+
--pcb-text-knockout-fill: rgba(248, 246, 239, 0.96);
|
|
144
145
|
--pcb-component-top-fill: rgba(244, 219, 198, 0.92);
|
|
145
146
|
--pcb-component-bottom-fill: rgba(15, 116, 108, 0.84);
|
|
146
147
|
--pcb-component-stroke: rgba(110, 64, 38, 0.28);
|
|
@@ -178,6 +179,8 @@
|
|
|
178
179
|
fill: var(--pcb-board-fill);
|
|
179
180
|
stroke: var(--pcb-board-stroke);
|
|
180
181
|
stroke-width: 18;
|
|
182
|
+
stroke-linecap: round;
|
|
183
|
+
stroke-linejoin: round;
|
|
181
184
|
}
|
|
182
185
|
|
|
183
186
|
.board-outline--stroke {
|
|
@@ -252,7 +255,7 @@
|
|
|
252
255
|
}
|
|
253
256
|
|
|
254
257
|
.pcb-footprint-region {
|
|
255
|
-
fill: var(--pcb-footprint-track-color);
|
|
258
|
+
fill: var(--pcb-footprint-region-fill, var(--pcb-footprint-track-color));
|
|
256
259
|
}
|
|
257
260
|
|
|
258
261
|
.pcb-footprint-track,
|
|
@@ -278,6 +281,14 @@
|
|
|
278
281
|
pointer-events: none;
|
|
279
282
|
}
|
|
280
283
|
|
|
284
|
+
.pcb-text__knockout-fill {
|
|
285
|
+
fill: var(--pcb-text-knockout-fill);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.pcb-text__knockout-glyphs {
|
|
289
|
+
fill: #000;
|
|
290
|
+
}
|
|
291
|
+
|
|
281
292
|
.bom-panel {
|
|
282
293
|
display: grid;
|
|
283
294
|
gap: 1rem;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects native overlay artwork that already contains text knockout holes.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbNativeTextKnockoutDetector {
|
|
9
|
+
static #DENSE_OVERLAY_MIN_REGION_AREA_RATIO = 0.2
|
|
10
|
+
static #DENSE_OVERLAY_MIN_TRACK_COUNT = 250
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns true when side-resolved overlay primitives carry native text
|
|
14
|
+
* knockouts, so source inverted TrueType labels would duplicate artwork.
|
|
15
|
+
* @param {{ fills?: object[], regions?: object[], tracks?: object[], arcs?: object[] }} primitives Side-resolved overlay primitives.
|
|
16
|
+
* @param {{ widthMil?: number, heightMil?: number }} board Board bounds.
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
static hasNativeTextKnockouts(primitives, board) {
|
|
20
|
+
const fills = [
|
|
21
|
+
...(Array.isArray(primitives?.fills) ? primitives.fills : []),
|
|
22
|
+
...(Array.isArray(primitives?.regions) ? primitives.regions : [])
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
PcbNativeTextKnockoutDetector.#isDenseOverlayArtwork(
|
|
27
|
+
{
|
|
28
|
+
fills,
|
|
29
|
+
tracks: primitives?.tracks,
|
|
30
|
+
arcs: primitives?.arcs
|
|
31
|
+
},
|
|
32
|
+
board
|
|
33
|
+
) &&
|
|
34
|
+
fills.some(
|
|
35
|
+
(fill) => Array.isArray(fill?.holes) && fill.holes.length > 0
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Detects dense overlay artwork from structural density.
|
|
42
|
+
* @param {{ fills?: object[], tracks?: object[], arcs?: object[] }} side Side primitives.
|
|
43
|
+
* @param {{ widthMil?: number, heightMil?: number }} board Board bounds.
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
static #isDenseOverlayArtwork(side, board) {
|
|
47
|
+
const strokeCount =
|
|
48
|
+
(Array.isArray(side?.tracks) ? side.tracks.length : 0) +
|
|
49
|
+
(Array.isArray(side?.arcs) ? side.arcs.length : 0)
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
strokeCount >=
|
|
53
|
+
PcbNativeTextKnockoutDetector.#DENSE_OVERLAY_MIN_TRACK_COUNT &&
|
|
54
|
+
PcbNativeTextKnockoutDetector.#maxFillAreaRatio(
|
|
55
|
+
side?.fills,
|
|
56
|
+
board
|
|
57
|
+
) >=
|
|
58
|
+
PcbNativeTextKnockoutDetector
|
|
59
|
+
.#DENSE_OVERLAY_MIN_REGION_AREA_RATIO
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolves the largest fill-to-board bounding-box area ratio.
|
|
65
|
+
* @param {object[] | undefined} fills Fill primitives.
|
|
66
|
+
* @param {{ widthMil?: number, heightMil?: number }} board Board bounds.
|
|
67
|
+
* @returns {number}
|
|
68
|
+
*/
|
|
69
|
+
static #maxFillAreaRatio(fills, board) {
|
|
70
|
+
const boardArea =
|
|
71
|
+
Math.max(Number(board?.widthMil || 0), 0) *
|
|
72
|
+
Math.max(Number(board?.heightMil || 0), 0)
|
|
73
|
+
if (!boardArea) {
|
|
74
|
+
return 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (Array.isArray(fills) ? fills : []).reduce((maxRatio, fill) => {
|
|
78
|
+
const bounds =
|
|
79
|
+
PcbNativeTextKnockoutDetector.#resolveFillBounds(fill)
|
|
80
|
+
if (!bounds) {
|
|
81
|
+
return maxRatio
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const fillArea =
|
|
85
|
+
Math.max(bounds.maxX - bounds.minX, 0) *
|
|
86
|
+
Math.max(bounds.maxY - bounds.minY, 0)
|
|
87
|
+
|
|
88
|
+
return Math.max(maxRatio, fillArea / boardArea)
|
|
89
|
+
}, 0)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolves rough authored bounds for one rectangular or polygon fill.
|
|
94
|
+
* @param {{ x1?: number, y1?: number, x2?: number, y2?: number, points?: { x?: number, y?: number }[] }} fill Fill primitive.
|
|
95
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
96
|
+
*/
|
|
97
|
+
static #resolveFillBounds(fill) {
|
|
98
|
+
const points = Array.isArray(fill?.points)
|
|
99
|
+
? fill.points
|
|
100
|
+
.map((point) => ({
|
|
101
|
+
x: Number(point?.x),
|
|
102
|
+
y: Number(point?.y)
|
|
103
|
+
}))
|
|
104
|
+
.filter(
|
|
105
|
+
(point) =>
|
|
106
|
+
Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
107
|
+
)
|
|
108
|
+
: [
|
|
109
|
+
{ x: Number(fill?.x1), y: Number(fill?.y1) },
|
|
110
|
+
{ x: Number(fill?.x2), y: Number(fill?.y2) }
|
|
111
|
+
].filter(
|
|
112
|
+
(point) =>
|
|
113
|
+
Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if (points.length < 2) {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const xs = points.map((point) => point.x)
|
|
121
|
+
const ys = points.map((point) => point.y)
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
minX: Math.min(...xs),
|
|
125
|
+
minY: Math.min(...ys),
|
|
126
|
+
maxX: Math.max(...xs),
|
|
127
|
+
maxY: Math.max(...ys)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|