altium-toolkit 1.1.32 → 1.1.35
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/docs/model-format.md +2 -3
- package/package.json +1 -1
- package/src/core/altium/AltiumGeneratedLibraryRecordBuilder.mjs +551 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +286 -2
- package/src/core/altium/AltiumPcbLibExporter.mjs +9 -2
- package/src/core/altium/AltiumSchLibExporter.mjs +1 -1
- package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +120 -15
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +52 -4
- package/src/core/altium/PcbShapeBasedBodyGeometryParser.mjs +85 -11
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +31 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +30 -2
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +27 -2
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +44 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +171 -6
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +782 -0
- package/src/ui/AltiumScene3dTwoRowFootprintDetector.mjs +90 -0
- package/src/ui/PcbScene3dBuilder.mjs +595 -28
- package/src/ui/PcbScene3dCopperRegionDetailBuilder.mjs +280 -0
- package/src/ui/PcbScene3dModelRegistry.mjs +16 -0
- package/src/ui/PcbScene3dPackageDimensionResolver.mjs +107 -0
- package/src/ui/PcbScene3dPackages.mjs +15 -3
- package/src/ui/PcbScene3dPadYawResolver.mjs +200 -0
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +56 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +638 -16
- package/src/ui/PcbScene3dStaticBodyRecovery.mjs +891 -0
- package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +358 -0
- package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +876 -0
|
@@ -121,10 +121,11 @@ export class PcbShapeBasedBodyGeometryParser {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
const textBytes = bytes.subarray(offset + 22, offset + 22 + textLength)
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
const payload =
|
|
125
|
+
PcbShapeBasedBodyGeometryParser.#splitInlineVertexPayload(textBytes)
|
|
126
|
+
const text = PrintableTextDecoder.decodeBytes(
|
|
127
|
+
payload.fieldBytes
|
|
128
|
+
).replaceAll('\u0000', '')
|
|
128
129
|
|
|
129
130
|
if (
|
|
130
131
|
!text.includes('MODEL.') ||
|
|
@@ -133,11 +134,17 @@ export class PcbShapeBasedBodyGeometryParser {
|
|
|
133
134
|
return null
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
const fields =
|
|
137
|
-
|
|
137
|
+
const fields = PcbShapeBasedBodyGeometryParser.#parseFieldRecordBytes(
|
|
138
|
+
payload.fieldBytes
|
|
139
|
+
)
|
|
140
|
+
const textEndOffset = offset + 22 + textLength
|
|
141
|
+
const vertexBlockOffset =
|
|
142
|
+
payload.inlineVertexBlockOffset === null
|
|
143
|
+
? textEndOffset
|
|
144
|
+
: offset + 22 + payload.inlineVertexBlockOffset
|
|
138
145
|
const vertexBlock = PcbShapeBasedBodyGeometryParser.#parseVertexBlock(
|
|
139
146
|
bytes,
|
|
140
|
-
|
|
147
|
+
vertexBlockOffset
|
|
141
148
|
)
|
|
142
149
|
|
|
143
150
|
return {
|
|
@@ -146,13 +153,80 @@ export class PcbShapeBasedBodyGeometryParser {
|
|
|
146
153
|
fields,
|
|
147
154
|
vertexBlock.verticesMil
|
|
148
155
|
),
|
|
149
|
-
nextOffset:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
nextOffset: Math.max(vertexBlock.nextOffset, textEndOffset)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Splits printable fields from Altium records whose text length also
|
|
162
|
+
* covers an inline binary vertex block after a NUL terminator.
|
|
163
|
+
* @param {Uint8Array} textBytes Declared text payload bytes.
|
|
164
|
+
* @returns {{ fieldBytes: Uint8Array, inlineVertexBlockOffset: number | null }}
|
|
165
|
+
*/
|
|
166
|
+
static #splitInlineVertexPayload(textBytes) {
|
|
167
|
+
const inlineVertexBlockOffset =
|
|
168
|
+
PcbShapeBasedBodyGeometryParser.#inlineVertexBlockOffset(textBytes)
|
|
169
|
+
if (inlineVertexBlockOffset === null) {
|
|
170
|
+
return { fieldBytes: textBytes, inlineVertexBlockOffset: null }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
fieldBytes: textBytes.subarray(0, inlineVertexBlockOffset - 1),
|
|
175
|
+
inlineVertexBlockOffset
|
|
153
176
|
}
|
|
154
177
|
}
|
|
155
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Finds the inline vertex-count offset inside a declared text payload.
|
|
181
|
+
* @param {Uint8Array} textBytes Declared text payload bytes.
|
|
182
|
+
* @returns {number | null}
|
|
183
|
+
*/
|
|
184
|
+
static #inlineVertexBlockOffset(textBytes) {
|
|
185
|
+
for (let index = 0; index < textBytes.byteLength; index += 1) {
|
|
186
|
+
if (textBytes[index] !== 0) {
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const vertexBlockOffset = index + 1
|
|
191
|
+
if (
|
|
192
|
+
PcbShapeBasedBodyGeometryParser.#hasPlausibleVertexBlock(
|
|
193
|
+
textBytes,
|
|
194
|
+
vertexBlockOffset
|
|
195
|
+
)
|
|
196
|
+
) {
|
|
197
|
+
return vertexBlockOffset
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Checks whether one byte offset points to a complete vertex block.
|
|
206
|
+
* @param {Uint8Array} bytes Candidate payload bytes.
|
|
207
|
+
* @param {number} offset Candidate vertex-count offset.
|
|
208
|
+
* @returns {boolean}
|
|
209
|
+
*/
|
|
210
|
+
static #hasPlausibleVertexBlock(bytes, offset) {
|
|
211
|
+
if (offset + 4 > bytes.byteLength) {
|
|
212
|
+
return false
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const view = new DataView(
|
|
216
|
+
bytes.buffer,
|
|
217
|
+
bytes.byteOffset,
|
|
218
|
+
bytes.byteLength
|
|
219
|
+
)
|
|
220
|
+
const vertexCount = view.getInt32(offset, true)
|
|
221
|
+
const dataOffset = offset + 4
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
vertexCount >= 3 &&
|
|
225
|
+
vertexCount <= 10000 &&
|
|
226
|
+
dataOffset + vertexCount * 37 <= bytes.byteLength
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
156
230
|
/**
|
|
157
231
|
* Parses the optional coordinate block after a shape-body field record.
|
|
158
232
|
* @param {Uint8Array} bytes Stream bytes.
|
|
@@ -137,10 +137,25 @@ export class SourceComponentBundleNormalizer {
|
|
|
137
137
|
primitives: SourceComponentBundleNormalizer.#array(
|
|
138
138
|
footprint.primitives || footprint.shapes
|
|
139
139
|
),
|
|
140
|
+
component:
|
|
141
|
+
SourceComponentBundleNormalizer.#normalizeFootprintComponent(
|
|
142
|
+
footprint.component || source.component
|
|
143
|
+
),
|
|
140
144
|
raw: footprint
|
|
141
145
|
}
|
|
142
146
|
}
|
|
143
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Preserves source footprint component placement metadata for exporters.
|
|
150
|
+
* @param {object | undefined} component Source component placement.
|
|
151
|
+
* @returns {object}
|
|
152
|
+
*/
|
|
153
|
+
static #normalizeFootprintComponent(component) {
|
|
154
|
+
return component && typeof component === 'object'
|
|
155
|
+
? { ...component }
|
|
156
|
+
: {}
|
|
157
|
+
}
|
|
158
|
+
|
|
144
159
|
/**
|
|
145
160
|
* Normalizes model asset descriptors.
|
|
146
161
|
* @param {object} source Unwrapped response.
|
|
@@ -177,6 +192,8 @@ export class SourceComponentBundleNormalizer {
|
|
|
177
192
|
)
|
|
178
193
|
const bytes =
|
|
179
194
|
SourceComponentBundleNormalizer.#normalizeModelBytes(model)
|
|
195
|
+
const transform =
|
|
196
|
+
SourceComponentBundleNormalizer.#normalizeModelTransform(model)
|
|
180
197
|
|
|
181
198
|
return {
|
|
182
199
|
id: SourceComponentBundleNormalizer.#firstString(
|
|
@@ -199,10 +216,24 @@ export class SourceComponentBundleNormalizer {
|
|
|
199
216
|
model.downloadUrl,
|
|
200
217
|
model.sourceUrl
|
|
201
218
|
),
|
|
219
|
+
...(transform ? { transform } : {}),
|
|
220
|
+
...(model.generated === true ? { generated: true } : {}),
|
|
202
221
|
raw: model
|
|
203
222
|
}
|
|
204
223
|
}
|
|
205
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Preserves optional model placement transforms.
|
|
227
|
+
* @param {object} model Raw model asset.
|
|
228
|
+
* @returns {object | null}
|
|
229
|
+
*/
|
|
230
|
+
static #normalizeModelTransform(model) {
|
|
231
|
+
const transform = model.transform || model.modelTransform || null
|
|
232
|
+
return transform && typeof transform === 'object'
|
|
233
|
+
? { ...transform }
|
|
234
|
+
: null
|
|
235
|
+
}
|
|
236
|
+
|
|
206
237
|
/**
|
|
207
238
|
* Normalizes one model payload into bytes.
|
|
208
239
|
* @param {object} model Raw model asset.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Defines the Circuit JSON contract emitted by public parser roots.
|
|
7
7
|
*/
|
|
8
8
|
export class CircuitJsonModelSchema {
|
|
9
|
-
static CURRENT_SCHEMA_ID = '
|
|
9
|
+
static CURRENT_SCHEMA_ID = 'urn:altium-toolkit:circuit-json:0.0.433'
|
|
10
10
|
|
|
11
11
|
static CURRENT_SCHEMA_VERSION = '0.0.433'
|
|
12
12
|
|
|
@@ -7,7 +7,10 @@ export class AltiumScene3dAuthoredBodyAnchorAdapter {
|
|
|
7
7
|
static #BODY_ANCHOR_TOLERANCE_MIL = 5
|
|
8
8
|
static #AUTHORED_SOURCE = 'authored-body-anchor'
|
|
9
9
|
static #AUTHORED_ANCHOR_IDENTITY_PATTERN =
|
|
10
|
-
/(?:^|[^a-z0-9])(?:antenna|coax|connector|edge|header|jack|mechanical|module|mount|shield|sma|socket
|
|
10
|
+
/(?:^|[^a-z0-9])(?:antenna|coax|connector|edge|header|jack|mechanical|module|mount|shield|sma|socket)(?:$|[^a-z0-9])/i
|
|
11
|
+
static #USB_ANCHOR_IDENTITY_PATTERN = /(?:^|[^a-z0-9])usb(?:$|[^a-z0-9])/i
|
|
12
|
+
static #INTEGRATED_CIRCUIT_PACKAGE_PATTERN =
|
|
13
|
+
/(?:^|[^a-z0-9])(?:u?qfn|v?qfn|dfn|qfp|lqfp|tqfp|bga|lga|sop|soic|ssop|tssop|msop|so[-_ ]?\d+)(?:[-_ ]?\d+)?(?:$|[^a-z0-9])/i
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Marks off-anchor explicit Altium body placements so the runtime does not
|
|
@@ -149,11 +152,36 @@ export class AltiumScene3dAuthoredBodyAnchorAdapter {
|
|
|
149
152
|
* @returns {boolean}
|
|
150
153
|
*/
|
|
151
154
|
static #hasAuthoredAnchorIdentity(placement, component) {
|
|
152
|
-
|
|
155
|
+
const identityText =
|
|
153
156
|
AltiumScene3dAuthoredBodyAnchorAdapter.#identityText(
|
|
154
157
|
placement,
|
|
155
158
|
component
|
|
156
159
|
)
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#AUTHORED_ANCHOR_IDENTITY_PATTERN.test(
|
|
163
|
+
identityText
|
|
164
|
+
) ||
|
|
165
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#hasUsbHardwareAnchorIdentity(
|
|
166
|
+
identityText
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Allows USB connector-like anchors without treating USB interface ICs as
|
|
173
|
+
* authored mechanical anchors.
|
|
174
|
+
* @param {string} identityText Searchable package identity.
|
|
175
|
+
* @returns {boolean}
|
|
176
|
+
*/
|
|
177
|
+
static #hasUsbHardwareAnchorIdentity(identityText) {
|
|
178
|
+
return (
|
|
179
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#USB_ANCHOR_IDENTITY_PATTERN.test(
|
|
180
|
+
identityText
|
|
181
|
+
) &&
|
|
182
|
+
!AltiumScene3dAuthoredBodyAnchorAdapter.#INTEGRATED_CIRCUIT_PACKAGE_PATTERN.test(
|
|
183
|
+
identityText
|
|
184
|
+
)
|
|
157
185
|
)
|
|
158
186
|
}
|
|
159
187
|
|
|
@@ -89,6 +89,14 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
89
89
|
if (!placement?.bodyPositionMil || !placement?.positionMil) {
|
|
90
90
|
return placement
|
|
91
91
|
}
|
|
92
|
+
if (
|
|
93
|
+
AltiumScene3dExternalPlacementAdapter.#isAuthoredShapeStackPlacement(
|
|
94
|
+
placement
|
|
95
|
+
)
|
|
96
|
+
) {
|
|
97
|
+
return placement
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
const componentBody =
|
|
93
101
|
AltiumScene3dExternalPlacementAdapter.#resolveComponentBody(
|
|
94
102
|
placement,
|
|
@@ -282,6 +290,19 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
282
290
|
}
|
|
283
291
|
}
|
|
284
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Checks whether an earlier adapter already resolved an authored carrier
|
|
295
|
+
* stack placement.
|
|
296
|
+
* @param {object} placement External model placement.
|
|
297
|
+
* @returns {boolean}
|
|
298
|
+
*/
|
|
299
|
+
static #isAuthoredShapeStackPlacement(placement) {
|
|
300
|
+
return (
|
|
301
|
+
String(placement?.projection?.source || '').toLowerCase() ===
|
|
302
|
+
'authored-shape-stack'
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
285
306
|
/**
|
|
286
307
|
* Resolves board-local SMT pad centers for mixed connector footprints.
|
|
287
308
|
* @param {object} placement External model placement.
|
|
@@ -731,8 +752,12 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
731
752
|
180
|
|
732
753
|
const cos = Math.cos(rotationRad)
|
|
733
754
|
const sin = Math.sin(rotationRad)
|
|
734
|
-
const
|
|
735
|
-
|
|
755
|
+
const sourceY =
|
|
756
|
+
AltiumScene3dExternalPlacementAdapter.#isBottomPlacement(placement)
|
|
757
|
+
? -Number(offset?.y || 0)
|
|
758
|
+
: Number(offset?.y || 0)
|
|
759
|
+
const x = Number(offset?.x || 0) * cos - sourceY * sin
|
|
760
|
+
const y = Number(offset?.x || 0) * sin + sourceY * cos
|
|
736
761
|
|
|
737
762
|
return {
|
|
738
763
|
x: Math.abs(x) < Number.EPSILON ? 0 : Number(x.toFixed(10)),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { AltiumScene3dTwoRowFootprintDetector } from './AltiumScene3dTwoRowFootprintDetector.mjs'
|
|
2
|
+
|
|
1
3
|
const PASSIVE_BODY_PATTERN =
|
|
2
4
|
/(?:^|[^a-z0-9])(?:cap|capacitor|res|resistor|ind|inductor|ferrite|bead|crystal|xtal|lqw|lqg)(?:$|[^a-z0-9])/i
|
|
3
5
|
const CHIP_PASSIVE_BODY_PATTERN =
|
|
@@ -6,6 +8,8 @@ const LOCAL_Y_CHIP_PASSIVE_MODEL_PATTERN =
|
|
|
6
8
|
/(?:^|[^a-z0-9])local[-_ ]?y(?:$|[^a-z0-9])/i
|
|
7
9
|
const PIN_ONE_CORNER_PACKAGE_PATTERN =
|
|
8
10
|
/(?:^|[^a-z0-9])(?:[avw]?qfn|vfqfn|wqfn|v?qfp|lqfp|tqfp|pqfp|mqfp)(?:[0-9]+)?(?:$|[^a-z0-9])/i
|
|
11
|
+
const TWO_ROW_PIN_ONE_PACKAGE_PATTERN =
|
|
12
|
+
/(?:^|[^a-z0-9])(?:msop|qsop|soic|sop|ssop|tssop|tsop|vsop)(?:[-_ ]?\d+)?(?:$|[^a-z0-9])/i
|
|
9
13
|
const FIVE_LEAD_SOT_PATTERN =
|
|
10
14
|
/(?:^|[^a-z0-9])sot[-_ ]?(?:23[-_ ]?5|25|5)(?:$|[^a-z0-9])/i
|
|
11
15
|
const THREE_LEAD_SOT23_PATTERN =
|
|
@@ -39,6 +43,9 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
39
43
|
AltiumScene3dPlacementRotationPolicy.#needsSquarePinOneCorrection(
|
|
40
44
|
context
|
|
41
45
|
) ||
|
|
46
|
+
AltiumScene3dPlacementRotationPolicy.#needsTwoRowPinOneCorrection(
|
|
47
|
+
context
|
|
48
|
+
) ||
|
|
42
49
|
AltiumScene3dPlacementRotationPolicy.#needsFiveLeadSotCorrection(
|
|
43
50
|
context
|
|
44
51
|
) ||
|
|
@@ -211,6 +218,43 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
211
218
|
)
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Detects two-row IC packages whose embedded source frame puts the
|
|
223
|
+
* package pin-one corner opposite the footprint convention.
|
|
224
|
+
* @param {{ placement?: object, component?: object | null, componentBody?: object | null, pads?: object[] }} context Rotation context.
|
|
225
|
+
* @returns {boolean}
|
|
226
|
+
*/
|
|
227
|
+
static #needsTwoRowPinOneCorrection(context) {
|
|
228
|
+
const { placement, component, componentBody, pads } = context || {}
|
|
229
|
+
if (
|
|
230
|
+
!AltiumScene3dPlacementRotationPolicy.#isPadFallback(placement) ||
|
|
231
|
+
!component ||
|
|
232
|
+
String(placement?.mountSide || '').toLowerCase() !== 'top' ||
|
|
233
|
+
AltiumScene3dPlacementRotationPolicy.#isGenericPassiveBody(
|
|
234
|
+
componentBody
|
|
235
|
+
) ||
|
|
236
|
+
!AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
|
|
237
|
+
componentBody
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const identityText =
|
|
244
|
+
AltiumScene3dPlacementRotationPolicy.#packageIdentityText(
|
|
245
|
+
component,
|
|
246
|
+
componentBody
|
|
247
|
+
)
|
|
248
|
+
if (!TWO_ROW_PIN_ONE_PACKAGE_PATTERN.test(identityText)) {
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return AltiumScene3dTwoRowFootprintDetector.hasTwoRowSurfaceFootprint(
|
|
253
|
+
component,
|
|
254
|
+
pads
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
214
258
|
/**
|
|
215
259
|
* Checks whether a source package yaw already matches the footprint and
|
|
216
260
|
* does not need another pin-one half-turn.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PcbScene3dPackageDimensionResolver } from './PcbScene3dPackageDimensionResolver.mjs'
|
|
2
|
+
|
|
1
3
|
const CONNECTOR_TOKENS = new Set([
|
|
2
4
|
'antenna',
|
|
3
5
|
'coax',
|
|
@@ -14,9 +16,14 @@ const TIMING_PACKAGE_PATTERN =
|
|
|
14
16
|
/(?:^|[^a-z0-9])(?:clock|crystal|osc|oscillator|resonator|tcxo|txco|xtal)(?:$|[^a-z0-9])/i
|
|
15
17
|
const TIMING_DESIGNATOR_PATTERN = /^(?:y|xo)\d+[a-z]?$/i
|
|
16
18
|
const AUTHORED_ANCHOR_IDENTITY_PATTERN =
|
|
17
|
-
/(?:^|[^a-z0-9])(?:antenna|coax|connector|edge|header|jack|mechanical|module|mount|shield|sma|socket
|
|
19
|
+
/(?:^|[^a-z0-9])(?:antenna|coax|connector|edge|header|jack|mechanical|module|mount|shield|sma|socket)(?:$|[^a-z0-9])/i
|
|
18
20
|
const PAD_FALLBACK_AUTHORED_ANCHOR_PATTERN =
|
|
19
|
-
/(?:^|[^a-z0-9])(?:antenna|coax|conn|connector|edge|flex|fpc|frame|hardware|header|jack|mechanical|module|shield|sma|socket
|
|
21
|
+
/(?:^|[^a-z0-9])(?:antenna|coax|conn|connector|edge|flex|fpc|frame|hardware|header|jack|mechanical|module|shield|sma|socket)(?:$|[^a-z0-9])/i
|
|
22
|
+
const USB_ANCHOR_IDENTITY_PATTERN = /(?:^|[^a-z0-9])usb(?:$|[^a-z0-9])/i
|
|
23
|
+
const INTEGRATED_CIRCUIT_PACKAGE_PATTERN =
|
|
24
|
+
/(?:^|[^a-z0-9])(?:u?qfn|v?qfn|dfn|qfp|lqfp|tqfp|bga|lga|sop|soic|ssop|tssop|msop|so[-_ ]?\d+)(?:[-_ ]?\d+)?(?:$|[^a-z0-9])/i
|
|
25
|
+
const SHIELD_COVER_TOKEN_PATTERN =
|
|
26
|
+
/(?:^|[^a-z0-9])(?:rf|emi|rfi|shield|cover)(?:$|[^a-z0-9])/gi
|
|
20
27
|
|
|
21
28
|
/**
|
|
22
29
|
* Repairs repeated Altium model-anchor bodies by matching their shared source
|
|
@@ -228,8 +235,20 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
228
235
|
String(placement?.projection?.source || '').toLowerCase() ===
|
|
229
236
|
'pad-fallback'
|
|
230
237
|
) {
|
|
238
|
+
if (
|
|
239
|
+
AltiumScene3dRepeatedModelOwnerRepair.#hasShieldCoverSourceOriginOffset(
|
|
240
|
+
placement,
|
|
241
|
+
component,
|
|
242
|
+
identityText
|
|
243
|
+
)
|
|
244
|
+
) {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
|
|
231
248
|
return (
|
|
232
|
-
|
|
249
|
+
AltiumScene3dRepeatedModelOwnerRepair.#hasPadFallbackAuthoredAnchorIdentity(
|
|
250
|
+
identityText
|
|
251
|
+
) ||
|
|
233
252
|
AltiumScene3dRepeatedModelOwnerRepair.#hasCompactIcSourceOriginOffset(
|
|
234
253
|
placement,
|
|
235
254
|
component,
|
|
@@ -238,7 +257,149 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
238
257
|
)
|
|
239
258
|
}
|
|
240
259
|
|
|
241
|
-
return
|
|
260
|
+
return AltiumScene3dRepeatedModelOwnerRepair.#hasAuthoredAnchorIdentity(
|
|
261
|
+
identityText
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Checks whether pad-fallback identity describes an authored hardware
|
|
267
|
+
* anchor instead of a source-origin-biased IC body.
|
|
268
|
+
* @param {string} identityText Searchable package identity.
|
|
269
|
+
* @returns {boolean}
|
|
270
|
+
*/
|
|
271
|
+
static #hasPadFallbackAuthoredAnchorIdentity(identityText) {
|
|
272
|
+
return (
|
|
273
|
+
PAD_FALLBACK_AUTHORED_ANCHOR_PATTERN.test(identityText) ||
|
|
274
|
+
AltiumScene3dRepeatedModelOwnerRepair.#hasUsbHardwareAnchorIdentity(
|
|
275
|
+
identityText
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Checks whether non-pad-fallback identity describes authored hardware.
|
|
282
|
+
* @param {string} identityText Searchable package identity.
|
|
283
|
+
* @returns {boolean}
|
|
284
|
+
*/
|
|
285
|
+
static #hasAuthoredAnchorIdentity(identityText) {
|
|
286
|
+
return (
|
|
287
|
+
AUTHORED_ANCHOR_IDENTITY_PATTERN.test(identityText) ||
|
|
288
|
+
AltiumScene3dRepeatedModelOwnerRepair.#hasUsbHardwareAnchorIdentity(
|
|
289
|
+
identityText
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Allows USB connector-like anchors without treating USB interface ICs as
|
|
296
|
+
* authored mechanical anchors.
|
|
297
|
+
* @param {string} identityText Searchable package identity.
|
|
298
|
+
* @returns {boolean}
|
|
299
|
+
*/
|
|
300
|
+
static #hasUsbHardwareAnchorIdentity(identityText) {
|
|
301
|
+
return (
|
|
302
|
+
USB_ANCHOR_IDENTITY_PATTERN.test(identityText) &&
|
|
303
|
+
!INTEGRATED_CIRCUIT_PACKAGE_PATTERN.test(identityText)
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Checks whether a shield cover body origin is a package corner.
|
|
309
|
+
* @param {object} placement Scene placement.
|
|
310
|
+
* @param {object} component PCB component.
|
|
311
|
+
* @param {string} identityText Searchable package identity.
|
|
312
|
+
* @returns {boolean}
|
|
313
|
+
*/
|
|
314
|
+
static #hasShieldCoverSourceOriginOffset(
|
|
315
|
+
placement,
|
|
316
|
+
component,
|
|
317
|
+
identityText
|
|
318
|
+
) {
|
|
319
|
+
const tokens = new Set(
|
|
320
|
+
[...String(identityText || '').matchAll(SHIELD_COVER_TOKEN_PATTERN)]
|
|
321
|
+
.map((match) => String(match[0] || '').toLowerCase())
|
|
322
|
+
.map((token) => token.replace(/[^a-z0-9]+/giu, ''))
|
|
323
|
+
)
|
|
324
|
+
if (
|
|
325
|
+
!tokens.has('cover') ||
|
|
326
|
+
!['rf', 'emi', 'rfi', 'shield'].some((token) => tokens.has(token))
|
|
327
|
+
) {
|
|
328
|
+
return false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const size =
|
|
332
|
+
PcbScene3dPackageDimensionResolver.resolvePlanarSize(component)
|
|
333
|
+
if (!size) {
|
|
334
|
+
return false
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const offset = {
|
|
338
|
+
x:
|
|
339
|
+
Number(placement?.bodyPositionMil?.x || 0) -
|
|
340
|
+
Number(component?.x || 0),
|
|
341
|
+
y:
|
|
342
|
+
Number(placement?.bodyPositionMil?.y || 0) -
|
|
343
|
+
Number(component?.y || 0)
|
|
344
|
+
}
|
|
345
|
+
if (
|
|
346
|
+
!Number.isFinite(offset.x) ||
|
|
347
|
+
!Number.isFinite(offset.y) ||
|
|
348
|
+
Math.hypot(offset.x, offset.y) <=
|
|
349
|
+
AltiumScene3dRepeatedModelOwnerRepair
|
|
350
|
+
.#MIN_PAD_FALLBACK_OWNER_OFFSET_MIL
|
|
351
|
+
) {
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return AltiumScene3dRepeatedModelOwnerRepair.#matchesCornerOriginSize(
|
|
356
|
+
Math.abs(offset.x) * 2,
|
|
357
|
+
Math.abs(offset.y) * 2,
|
|
358
|
+
size
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Checks whether doubled owner offset matches package dimensions.
|
|
364
|
+
* @param {number} offsetWidth Doubled X offset.
|
|
365
|
+
* @param {number} offsetDepth Doubled Y offset.
|
|
366
|
+
* @param {{ width: number, depth: number }} size Explicit package size.
|
|
367
|
+
* @returns {boolean}
|
|
368
|
+
*/
|
|
369
|
+
static #matchesCornerOriginSize(offsetWidth, offsetDepth, size) {
|
|
370
|
+
return (
|
|
371
|
+
(AltiumScene3dRepeatedModelOwnerRepair.#matchesDimension(
|
|
372
|
+
offsetWidth,
|
|
373
|
+
size.width
|
|
374
|
+
) &&
|
|
375
|
+
AltiumScene3dRepeatedModelOwnerRepair.#matchesDimension(
|
|
376
|
+
offsetDepth,
|
|
377
|
+
size.depth
|
|
378
|
+
)) ||
|
|
379
|
+
(AltiumScene3dRepeatedModelOwnerRepair.#matchesDimension(
|
|
380
|
+
offsetWidth,
|
|
381
|
+
size.depth
|
|
382
|
+
) &&
|
|
383
|
+
AltiumScene3dRepeatedModelOwnerRepair.#matchesDimension(
|
|
384
|
+
offsetDepth,
|
|
385
|
+
size.width
|
|
386
|
+
))
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Checks whether two dimensions are close enough for source-origin repair.
|
|
392
|
+
* @param {number} actual Actual dimension.
|
|
393
|
+
* @param {number} expected Expected dimension.
|
|
394
|
+
* @returns {boolean}
|
|
395
|
+
*/
|
|
396
|
+
static #matchesDimension(actual, expected) {
|
|
397
|
+
if (!Number.isFinite(actual) || !Number.isFinite(expected)) {
|
|
398
|
+
return false
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const tolerance = Math.max(35, Math.abs(expected) * 0.08)
|
|
402
|
+
return Math.abs(actual - expected) <= tolerance
|
|
242
403
|
}
|
|
243
404
|
|
|
244
405
|
/**
|
|
@@ -557,8 +718,12 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
557
718
|
180
|
|
558
719
|
const cos = Math.cos(rotationRad)
|
|
559
720
|
const sin = Math.sin(rotationRad)
|
|
560
|
-
const
|
|
561
|
-
|
|
721
|
+
const sourceY =
|
|
722
|
+
AltiumScene3dRepeatedModelOwnerRepair.#isBottomPlacement(placement)
|
|
723
|
+
? -Number(offset?.y || 0)
|
|
724
|
+
: Number(offset?.y || 0)
|
|
725
|
+
const x = Number(offset?.x || 0) * cos - sourceY * sin
|
|
726
|
+
const y = Number(offset?.x || 0) * sin + sourceY * cos
|
|
562
727
|
const z = Number(
|
|
563
728
|
modelTransform?.offsetMil?.z ?? modelTransform?.dzMil ?? 0
|
|
564
729
|
)
|