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.
- package/AGENTS.md +67 -0
- package/COMMERCIAL-LICENSE.md +15 -0
- package/CONTRIBUTING.md +14 -0
- package/LICENSE +19 -0
- package/LICENSES/AGPL-3.0-or-later.txt +235 -0
- package/LICENSES/CC-BY-SA-4.0.txt +170 -0
- package/LICENSES/LGPL-2.1-or-later.txt +176 -0
- package/LICENSES/LicenseRef-PolyForm-Noncommercial-1.0.0.txt +131 -0
- package/NOTICE.md +36 -0
- package/README.md +128 -0
- package/REUSE.toml +16 -0
- package/docs/api.md +148 -0
- package/docs/circuitjson.md +190 -0
- package/docs/model-format.md +117 -0
- package/docs/testing.md +23 -0
- package/package.json +65 -0
- package/spec/library-scope.md +36 -0
- package/src/PcbModelArchiveExporter.mjs +320 -0
- package/src/PcbScene3dArcUtils.mjs +27 -0
- package/src/PcbScene3dBoardAssemblyPlacement.mjs +36 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +859 -0
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +537 -0
- package/src/PcbScene3dBoardMaterialPalette.mjs +40 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +895 -0
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +613 -0
- package/src/PcbScene3dCameraRig.mjs +168 -0
- package/src/PcbScene3dCircuitJsonAdapter.mjs +545 -0
- package/src/PcbScene3dController.mjs +956 -0
- package/src/PcbScene3dCopperDetailFilter.mjs +490 -0
- package/src/PcbScene3dCopperFactory.mjs +559 -0
- package/src/PcbScene3dCopperTextFactory.mjs +534 -0
- package/src/PcbScene3dCutoutGeometryFilter.mjs +873 -0
- package/src/PcbScene3dDetailCoordinateNormalizer.mjs +65 -0
- package/src/PcbScene3dDrillCutoutFilter.mjs +224 -0
- package/src/PcbScene3dDrillPathFactory.mjs +362 -0
- package/src/PcbScene3dDrillVoidFactory.mjs +268 -0
- package/src/PcbScene3dExternalModelLoadOrder.mjs +54 -0
- package/src/PcbScene3dExternalModels.mjs +968 -0
- package/src/PcbScene3dFallbackVisibility.mjs +82 -0
- package/src/PcbScene3dInteractionHints.mjs +56 -0
- package/src/PcbScene3dMountRig.mjs +53 -0
- package/src/PcbScene3dOutlineBuilder.mjs +210 -0
- package/src/PcbScene3dPadFactory.mjs +553 -0
- package/src/PcbScene3dPresetState.mjs +48 -0
- package/src/PcbScene3dRenderGroupVisibility.mjs +134 -0
- package/src/PcbScene3dRuntime.mjs +996 -0
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +99 -0
- package/src/PcbScene3dSelectionStyler.mjs +252 -0
- package/src/PcbScene3dShapePathFactory.mjs +220 -0
- package/src/PcbScene3dShellRenderer.mjs +131 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +854 -0
- package/src/PcbScene3dSilkscreenStrokeWidthResolver.mjs +81 -0
- package/src/PcbScene3dStepLoader.mjs +611 -0
- package/src/PcbScene3dStrokeFont.mjs +671 -0
- package/src/PcbScene3dStrokeGeometryBuilder.mjs +322 -0
- package/src/PcbScene3dText.mjs +99 -0
- package/src/PcbScene3dTrueTypeTextFactory.mjs +885 -0
- package/src/PcbScene3dViaFactory.mjs +176 -0
- package/src/PcbScene3dViewCompensation.mjs +109 -0
- package/src/PcbScene3dViewScale.mjs +24 -0
- package/src/PcbScene3dViewportResize.mjs +35 -0
- package/src/PcbScene3dWorkerClient.mjs +123 -0
- package/src/index.mjs +1 -0
- package/src/scene3d.mjs +44 -0
- package/src/styles/scene3d.css +295 -0
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds browser-font-backed text planes for Altium TrueType silkscreen labels.
|
|
3
|
+
*/
|
|
4
|
+
export class PcbScene3dTrueTypeTextFactory {
|
|
5
|
+
static #CANVAS_SCALE = 2
|
|
6
|
+
static #DEFAULT_FONT_FAMILY = 'Arial'
|
|
7
|
+
static #DEFAULT_MATERIAL_COLOR = 0xf8f6ef
|
|
8
|
+
static #LINE_HEIGHT_RATIO = 1.16
|
|
9
|
+
static #DEFAULT_TRUETYPE_EM_SCALE = 0.895
|
|
10
|
+
static #MIN_CANVAS_PADDING = 2
|
|
11
|
+
static #TEXTURE_PADDING_RATIO = 0.14
|
|
12
|
+
static #loadedFontKeys = new Set()
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Loads embedded Altium font payloads into the browser font set.
|
|
16
|
+
* @param {{ name?: string, style?: string, format?: string, mimeType?: string, payloadBase64?: string, metrics?: { weightClass?: number } }[]} embeddedFonts
|
|
17
|
+
* @returns {Promise<void>}
|
|
18
|
+
*/
|
|
19
|
+
static async prepareEmbeddedFonts(embeddedFonts) {
|
|
20
|
+
if (
|
|
21
|
+
typeof globalThis.FontFace !== 'function' ||
|
|
22
|
+
!globalThis.document?.fonts?.add
|
|
23
|
+
) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const pendingLoads = (Array.isArray(embeddedFonts) ? embeddedFonts : [])
|
|
28
|
+
.filter((font) => font?.name && font?.payloadBase64)
|
|
29
|
+
.filter((font) =>
|
|
30
|
+
PcbScene3dTrueTypeTextFactory.#markFontForLoading(font)
|
|
31
|
+
)
|
|
32
|
+
.map((font) =>
|
|
33
|
+
PcbScene3dTrueTypeTextFactory.#loadEmbeddedFont(font)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
await Promise.all(pendingLoads.map((load) => load.catch(() => null)))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Checks whether a normalized text primitive requests TrueType rendering.
|
|
41
|
+
* @param {object} text
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
static isTrueTypeText(text) {
|
|
45
|
+
const fontTypeName = String(text?.fontTypeName || '').toUpperCase()
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
text?.isTrueType === true ||
|
|
49
|
+
Number(text?.fontType) === 1 ||
|
|
50
|
+
fontTypeName.includes('TRUETYPE')
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Builds one group containing texture-backed text meshes.
|
|
56
|
+
* @param {any} THREE
|
|
57
|
+
* @param {object[]} texts
|
|
58
|
+
* @param {number} z
|
|
59
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
60
|
+
* @param {{ materialColor?: number, invertedMaterialColor?: number, mirrorY?: boolean }} [options]
|
|
61
|
+
* @returns {any}
|
|
62
|
+
*/
|
|
63
|
+
static buildGroup(THREE, texts, z, normalizeBoardPoint, options = {}) {
|
|
64
|
+
const group = new THREE.Group()
|
|
65
|
+
group.name = 'true-type-texts'
|
|
66
|
+
;(Array.isArray(texts) ? texts : [])
|
|
67
|
+
.filter((text) =>
|
|
68
|
+
PcbScene3dTrueTypeTextFactory.isTrueTypeText(text)
|
|
69
|
+
)
|
|
70
|
+
.map((text) =>
|
|
71
|
+
PcbScene3dTrueTypeTextFactory.#buildTextMesh(
|
|
72
|
+
THREE,
|
|
73
|
+
text,
|
|
74
|
+
z,
|
|
75
|
+
normalizeBoardPoint,
|
|
76
|
+
options
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.forEach((mesh) => group.add(mesh))
|
|
81
|
+
|
|
82
|
+
return group
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Builds one texture-backed text mesh.
|
|
87
|
+
* @param {any} THREE
|
|
88
|
+
* @param {object} text
|
|
89
|
+
* @param {number} z
|
|
90
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
91
|
+
* @param {{ materialColor?: number, invertedMaterialColor?: number, mirrorY?: boolean }} options
|
|
92
|
+
* @returns {any | null}
|
|
93
|
+
*/
|
|
94
|
+
static #buildTextMesh(THREE, text, z, normalizeBoardPoint, options) {
|
|
95
|
+
if (
|
|
96
|
+
!THREE?.CanvasTexture ||
|
|
97
|
+
!THREE?.Mesh ||
|
|
98
|
+
!THREE?.MeshBasicMaterial ||
|
|
99
|
+
!THREE?.PlaneGeometry
|
|
100
|
+
) {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const mirrorY = Boolean(options?.mirrorY)
|
|
105
|
+
const textureInfo = PcbScene3dTrueTypeTextFactory.#buildTextureInfo(
|
|
106
|
+
THREE,
|
|
107
|
+
text,
|
|
108
|
+
PcbScene3dTrueTypeTextFactory.#resolveTextPaint(text, options)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if (!textureInfo) {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const geometry = PcbScene3dTrueTypeTextFactory.#buildPlaneGeometry(
|
|
116
|
+
THREE,
|
|
117
|
+
textureInfo.width,
|
|
118
|
+
textureInfo.height,
|
|
119
|
+
textureInfo.anchorX,
|
|
120
|
+
textureInfo.anchorY,
|
|
121
|
+
Boolean(text?.mirrored)
|
|
122
|
+
)
|
|
123
|
+
const mesh = new THREE.Mesh(
|
|
124
|
+
geometry,
|
|
125
|
+
PcbScene3dTrueTypeTextFactory.#buildMaterial(THREE, textureInfo)
|
|
126
|
+
)
|
|
127
|
+
const position = PcbScene3dTrueTypeTextFactory.#normalizePoint(
|
|
128
|
+
normalizeBoardPoint,
|
|
129
|
+
Number(text?.x || 0),
|
|
130
|
+
Number(text?.y || 0),
|
|
131
|
+
mirrorY
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
mesh.name = 'true-type-text'
|
|
135
|
+
mesh.userData = mesh.userData || {}
|
|
136
|
+
mesh.userData.scene3dViewCompensation = true
|
|
137
|
+
mesh.userData.scene3dViewCompensationAxes = {
|
|
138
|
+
x: false,
|
|
139
|
+
y: !mirrorY,
|
|
140
|
+
z: false
|
|
141
|
+
}
|
|
142
|
+
if (!mirrorY) {
|
|
143
|
+
mesh.userData.scene3dViewCompensationAxisSources = {
|
|
144
|
+
y: 'board-mirror'
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
mesh.position.set(position.x, position.y, z)
|
|
148
|
+
mesh.rotation.z = (Number(text?.rotation || 0) * Math.PI) / 180
|
|
149
|
+
|
|
150
|
+
return mesh
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Builds plane geometry anchored at the text insertion point.
|
|
155
|
+
* @param {any} THREE
|
|
156
|
+
* @param {number} width
|
|
157
|
+
* @param {number} height
|
|
158
|
+
* @param {number} anchorX
|
|
159
|
+
* @param {number} anchorY
|
|
160
|
+
* @param {boolean} mirrored
|
|
161
|
+
* @returns {any}
|
|
162
|
+
*/
|
|
163
|
+
static #buildPlaneGeometry(
|
|
164
|
+
THREE,
|
|
165
|
+
width,
|
|
166
|
+
height,
|
|
167
|
+
anchorX,
|
|
168
|
+
anchorY,
|
|
169
|
+
mirrored
|
|
170
|
+
) {
|
|
171
|
+
const geometry = new THREE.PlaneGeometry(width, height)
|
|
172
|
+
|
|
173
|
+
geometry.translate?.(width / 2 - anchorX, anchorY - height / 2, 0)
|
|
174
|
+
if (mirrored) {
|
|
175
|
+
geometry.scale?.(-1, 1, 1)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return geometry
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Builds a transparent canvas texture and its board-space dimensions.
|
|
183
|
+
* @param {any} THREE
|
|
184
|
+
* @param {object} text
|
|
185
|
+
* @param {{ color: number, textColor: number, knockout: boolean }} paint
|
|
186
|
+
* @returns {{ texture: any, width: number, height: number, baselineX: number, baselineY: number, anchorX: number, anchorY: number } | null}
|
|
187
|
+
*/
|
|
188
|
+
static #buildTextureInfo(THREE, text, paint) {
|
|
189
|
+
const canvas = PcbScene3dTrueTypeTextFactory.#createCanvas()
|
|
190
|
+
const context = canvas?.getContext?.('2d')
|
|
191
|
+
|
|
192
|
+
if (!canvas || !context) {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const lines = PcbScene3dTrueTypeTextFactory.#textLines(text)
|
|
197
|
+
const fontSize = PcbScene3dTrueTypeTextFactory.#textHeight(text)
|
|
198
|
+
const font = PcbScene3dTrueTypeTextFactory.#buildCanvasFont(
|
|
199
|
+
text,
|
|
200
|
+
fontSize
|
|
201
|
+
)
|
|
202
|
+
const metrics = PcbScene3dTrueTypeTextFactory.#measureLines(
|
|
203
|
+
context,
|
|
204
|
+
lines,
|
|
205
|
+
font,
|
|
206
|
+
fontSize
|
|
207
|
+
)
|
|
208
|
+
const padding = PcbScene3dTrueTypeTextFactory.#resolveTexturePadding(
|
|
209
|
+
text,
|
|
210
|
+
fontSize,
|
|
211
|
+
paint.knockout
|
|
212
|
+
)
|
|
213
|
+
const layout = PcbScene3dTrueTypeTextFactory.#resolveTextureLayout(
|
|
214
|
+
text,
|
|
215
|
+
metrics,
|
|
216
|
+
padding,
|
|
217
|
+
paint.knockout
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
PcbScene3dTrueTypeTextFactory.#sizeCanvas(
|
|
221
|
+
canvas,
|
|
222
|
+
layout.width,
|
|
223
|
+
layout.height
|
|
224
|
+
)
|
|
225
|
+
PcbScene3dTrueTypeTextFactory.#drawText(
|
|
226
|
+
canvas,
|
|
227
|
+
lines,
|
|
228
|
+
font,
|
|
229
|
+
paint,
|
|
230
|
+
metrics,
|
|
231
|
+
layout.baselineX,
|
|
232
|
+
layout.baselineY,
|
|
233
|
+
layout.width,
|
|
234
|
+
layout.height
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
const texture = new THREE.CanvasTexture(canvas)
|
|
238
|
+
texture.needsUpdate = true
|
|
239
|
+
if ('colorSpace' in texture && THREE.SRGBColorSpace) {
|
|
240
|
+
texture.colorSpace = THREE.SRGBColorSpace
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
texture,
|
|
245
|
+
width: layout.width,
|
|
246
|
+
height: layout.height,
|
|
247
|
+
baselineX: layout.baselineX,
|
|
248
|
+
baselineY: layout.baselineY,
|
|
249
|
+
anchorX: layout.anchorX,
|
|
250
|
+
anchorY: layout.anchorY
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Creates a canvas when the runtime has a browser document.
|
|
256
|
+
* @returns {HTMLCanvasElement | null}
|
|
257
|
+
*/
|
|
258
|
+
static #createCanvas() {
|
|
259
|
+
return globalThis.document?.createElement?.('canvas') || null
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Splits one text primitive into drawable lines.
|
|
264
|
+
* @param {object} text
|
|
265
|
+
* @returns {string[]}
|
|
266
|
+
*/
|
|
267
|
+
static #textLines(text) {
|
|
268
|
+
const value = String(text?.value ?? text?.text ?? '')
|
|
269
|
+
const lines = value.split(/\r?\n/u)
|
|
270
|
+
|
|
271
|
+
return lines.length ? lines : ['']
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Resolves the requested text height in mil.
|
|
276
|
+
* @param {object} text
|
|
277
|
+
* @returns {number}
|
|
278
|
+
*/
|
|
279
|
+
static #textHeight(text) {
|
|
280
|
+
const height = Math.max(
|
|
281
|
+
Number(text?.sizeX) || Number(text?.height) || Number(text?.sizeY),
|
|
282
|
+
1
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return height * PcbScene3dTrueTypeTextFactory.#fontScale(text)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Resolves the imported TrueType scale used for browser outline fonts.
|
|
290
|
+
* @param {object} text
|
|
291
|
+
* @returns {number}
|
|
292
|
+
*/
|
|
293
|
+
static #fontScale(text) {
|
|
294
|
+
if (Number.isFinite(Number(text?.trueTypeFontScale))) {
|
|
295
|
+
return Math.max(Number(text.trueTypeFontScale), 0.01)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (Number.isFinite(Number(text?.fontMetrics?.emScaleFromPcbHeight))) {
|
|
299
|
+
return Math.max(Number(text.fontMetrics.emScaleFromPcbHeight), 0.01)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return PcbScene3dTrueTypeTextFactory.#DEFAULT_TRUETYPE_EM_SCALE
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Marks a font as loaded or pending so repeated renders do not re-register it.
|
|
307
|
+
* @param {{ name?: string, style?: string, payloadBase64?: string, metrics?: { weightClass?: number } }} font
|
|
308
|
+
* @returns {boolean}
|
|
309
|
+
*/
|
|
310
|
+
static #markFontForLoading(font) {
|
|
311
|
+
const key = [
|
|
312
|
+
PcbScene3dTrueTypeTextFactory.#cleanFontFamily(font?.name),
|
|
313
|
+
PcbScene3dTrueTypeTextFactory.#fontStyleForFont(font),
|
|
314
|
+
PcbScene3dTrueTypeTextFactory.#fontWeightForFont(font),
|
|
315
|
+
String(font?.payloadBase64 || '').length
|
|
316
|
+
].join('\0')
|
|
317
|
+
|
|
318
|
+
if (PcbScene3dTrueTypeTextFactory.#loadedFontKeys.has(key)) {
|
|
319
|
+
return false
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
PcbScene3dTrueTypeTextFactory.#loadedFontKeys.add(key)
|
|
323
|
+
return true
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Loads one embedded font face and registers it with document.fonts.
|
|
328
|
+
* @param {{ name?: string, style?: string, format?: string, mimeType?: string, payloadBase64?: string, metrics?: { weightClass?: number } }} font
|
|
329
|
+
* @returns {Promise<void>}
|
|
330
|
+
*/
|
|
331
|
+
static async #loadEmbeddedFont(font) {
|
|
332
|
+
const face = new globalThis.FontFace(
|
|
333
|
+
PcbScene3dTrueTypeTextFactory.#cleanFontFamily(font?.name),
|
|
334
|
+
PcbScene3dTrueTypeTextFactory.#fontFaceSource(font),
|
|
335
|
+
{
|
|
336
|
+
style: PcbScene3dTrueTypeTextFactory.#fontStyleForFont(font),
|
|
337
|
+
weight: String(
|
|
338
|
+
PcbScene3dTrueTypeTextFactory.#fontWeightForFont(font)
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
const loadedFace = await face.load()
|
|
343
|
+
|
|
344
|
+
globalThis.document.fonts.add(loadedFace)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Builds the CSS source string for one embedded font payload.
|
|
349
|
+
* @param {{ format?: string, mimeType?: string, payloadBase64?: string }} font
|
|
350
|
+
* @returns {string}
|
|
351
|
+
*/
|
|
352
|
+
static #fontFaceSource(font) {
|
|
353
|
+
return (
|
|
354
|
+
'url(data:' +
|
|
355
|
+
PcbScene3dTrueTypeTextFactory.#fontMimeType(font) +
|
|
356
|
+
';base64,' +
|
|
357
|
+
PcbScene3dTrueTypeTextFactory.#sanitizeBase64(font?.payloadBase64) +
|
|
358
|
+
") format('" +
|
|
359
|
+
PcbScene3dTrueTypeTextFactory.#fontFormat(font) +
|
|
360
|
+
"')"
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Resolves a CSS font-style value from embedded font metadata.
|
|
366
|
+
* @param {{ style?: string }} font
|
|
367
|
+
* @returns {'normal' | 'italic'}
|
|
368
|
+
*/
|
|
369
|
+
static #fontStyleForFont(font) {
|
|
370
|
+
return /italic|oblique/iu.test(String(font?.style || ''))
|
|
371
|
+
? 'italic'
|
|
372
|
+
: 'normal'
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Resolves a CSS font-weight value from embedded font metadata.
|
|
377
|
+
* @param {{ style?: string, metrics?: { weightClass?: number } }} font
|
|
378
|
+
* @returns {number}
|
|
379
|
+
*/
|
|
380
|
+
static #fontWeightForFont(font) {
|
|
381
|
+
if (Number(font?.metrics?.weightClass) >= 100) {
|
|
382
|
+
return Number(font.metrics.weightClass)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return /bold/iu.test(String(font?.style || '')) ? 700 : 400
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Resolves a browser font MIME type.
|
|
390
|
+
* @param {{ mimeType?: string, format?: string }} font
|
|
391
|
+
* @returns {string}
|
|
392
|
+
*/
|
|
393
|
+
static #fontMimeType(font) {
|
|
394
|
+
if (font?.mimeType) {
|
|
395
|
+
return PcbScene3dTrueTypeTextFactory.#escapeCssUrlToken(
|
|
396
|
+
font.mimeType
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return font?.format === 'opentype' ? 'font/otf' : 'font/ttf'
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Resolves a CSS font format label.
|
|
405
|
+
* @param {{ format?: string }} font
|
|
406
|
+
* @returns {'opentype' | 'truetype'}
|
|
407
|
+
*/
|
|
408
|
+
static #fontFormat(font) {
|
|
409
|
+
return font?.format === 'opentype' ? 'opentype' : 'truetype'
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Keeps a base64 font payload constrained to data-URI-safe characters.
|
|
414
|
+
* @param {string | undefined} value
|
|
415
|
+
* @returns {string}
|
|
416
|
+
*/
|
|
417
|
+
static #sanitizeBase64(value) {
|
|
418
|
+
return String(value || '').replace(/[^A-Za-z0-9+/=]/gu, '')
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Escapes a short CSS URL token.
|
|
423
|
+
* @param {string | undefined} value
|
|
424
|
+
* @returns {string}
|
|
425
|
+
*/
|
|
426
|
+
static #escapeCssUrlToken(value) {
|
|
427
|
+
return String(value || '').replace(/[^A-Za-z0-9./+-]/gu, '')
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Builds the CSS font string used for canvas rendering.
|
|
432
|
+
* @param {object} text
|
|
433
|
+
* @param {number} fontSize
|
|
434
|
+
* @returns {string}
|
|
435
|
+
*/
|
|
436
|
+
static #buildCanvasFont(text, fontSize) {
|
|
437
|
+
const weight =
|
|
438
|
+
text?.isBold || Number(text?.fontWeight) >= 600 ? '700' : '400'
|
|
439
|
+
const style = text?.isItalic ? 'italic' : 'normal'
|
|
440
|
+
const family = PcbScene3dTrueTypeTextFactory.#buildCanvasFontFamily(
|
|
441
|
+
text?.fontFamily || text?.fontName
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
return `${style} ${weight} ${fontSize}px ${family}`
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Resolves transparent texture padding around one text run.
|
|
449
|
+
* @param {object} text
|
|
450
|
+
* @param {number} fontSize
|
|
451
|
+
* @param {boolean} knockout
|
|
452
|
+
* @returns {number}
|
|
453
|
+
*/
|
|
454
|
+
static #resolveTexturePadding(text, fontSize, knockout) {
|
|
455
|
+
const basePadding = Math.max(
|
|
456
|
+
fontSize * PcbScene3dTrueTypeTextFactory.#TEXTURE_PADDING_RATIO,
|
|
457
|
+
PcbScene3dTrueTypeTextFactory.#MIN_CANVAS_PADDING
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if (!knockout) {
|
|
461
|
+
return basePadding
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const marginBorderWidth = Number(text?.marginBorderWidth)
|
|
465
|
+
|
|
466
|
+
return Number.isFinite(marginBorderWidth) && marginBorderWidth >= 0
|
|
467
|
+
? marginBorderWidth
|
|
468
|
+
: basePadding
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Builds a CSS family stack for one Altium TrueType font name.
|
|
473
|
+
* @param {unknown} family
|
|
474
|
+
* @returns {string}
|
|
475
|
+
*/
|
|
476
|
+
static #buildCanvasFontFamily(family) {
|
|
477
|
+
const cleaned = PcbScene3dTrueTypeTextFactory.#cleanFontFamily(family)
|
|
478
|
+
const quoted = PcbScene3dTrueTypeTextFactory.#quoteFontFamily(cleaned)
|
|
479
|
+
|
|
480
|
+
if (PcbScene3dTrueTypeTextFactory.#isMonospaceFamily(cleaned)) {
|
|
481
|
+
return [
|
|
482
|
+
quoted,
|
|
483
|
+
'"Menlo"',
|
|
484
|
+
'"Monaco"',
|
|
485
|
+
'"Liberation Mono"',
|
|
486
|
+
'"Courier New"',
|
|
487
|
+
'monospace'
|
|
488
|
+
].join(', ')
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (PcbScene3dTrueTypeTextFactory.#isArialFamily(cleaned)) {
|
|
492
|
+
return [
|
|
493
|
+
quoted,
|
|
494
|
+
'"Helvetica Neue"',
|
|
495
|
+
'Helvetica',
|
|
496
|
+
'Arial',
|
|
497
|
+
'sans-serif'
|
|
498
|
+
].join(', ')
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return [quoted, 'Arial', 'sans-serif'].join(', ')
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Removes Altium fixed-field padding from one font family name.
|
|
506
|
+
* @param {unknown} family
|
|
507
|
+
* @returns {string}
|
|
508
|
+
*/
|
|
509
|
+
static #cleanFontFamily(family) {
|
|
510
|
+
const raw = String(
|
|
511
|
+
family || PcbScene3dTrueTypeTextFactory.#DEFAULT_FONT_FAMILY
|
|
512
|
+
)
|
|
513
|
+
const cleaned = raw.split('\0')[0]?.trim() || ''
|
|
514
|
+
|
|
515
|
+
return cleaned || PcbScene3dTrueTypeTextFactory.#DEFAULT_FONT_FAMILY
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Quotes one CSS font family safely.
|
|
520
|
+
* @param {string} family
|
|
521
|
+
* @returns {string}
|
|
522
|
+
*/
|
|
523
|
+
static #quoteFontFamily(family) {
|
|
524
|
+
return `"${family.replace(/["\\]/gu, '\\$&')}"`
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Checks whether an imported family is Arial-like.
|
|
529
|
+
* @param {unknown} family
|
|
530
|
+
* @returns {boolean}
|
|
531
|
+
*/
|
|
532
|
+
static #isArialFamily(family) {
|
|
533
|
+
return /^arial(?:\b|$)/iu.test(
|
|
534
|
+
PcbScene3dTrueTypeTextFactory.#cleanFontFamily(family)
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Checks whether an imported family is a common PCB monospace font.
|
|
540
|
+
* @param {unknown} family
|
|
541
|
+
* @returns {boolean}
|
|
542
|
+
*/
|
|
543
|
+
static #isMonospaceFamily(family) {
|
|
544
|
+
return /^(consolas|courier|courier new|menlo|monaco|liberation mono)$/iu.test(
|
|
545
|
+
PcbScene3dTrueTypeTextFactory.#cleanFontFamily(family)
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Measures text lines with browser font metrics and stable fallbacks.
|
|
551
|
+
* @param {CanvasRenderingContext2D} context
|
|
552
|
+
* @param {string[]} lines
|
|
553
|
+
* @param {string} font
|
|
554
|
+
* @param {number} fontSize
|
|
555
|
+
* @returns {{ width: number, height: number, ascent: number, descent: number, lineHeight: number }}
|
|
556
|
+
*/
|
|
557
|
+
static #measureLines(context, lines, font, fontSize) {
|
|
558
|
+
context.font = font
|
|
559
|
+
|
|
560
|
+
const measured = lines.map((line) => context.measureText(line || ' '))
|
|
561
|
+
const ascent = PcbScene3dTrueTypeTextFactory.#resolveMeasuredExtent(
|
|
562
|
+
measured,
|
|
563
|
+
'actualBoundingBoxAscent',
|
|
564
|
+
fontSize * 0.82
|
|
565
|
+
)
|
|
566
|
+
const descent = PcbScene3dTrueTypeTextFactory.#resolveMeasuredExtent(
|
|
567
|
+
measured,
|
|
568
|
+
'actualBoundingBoxDescent',
|
|
569
|
+
fontSize * 0.18
|
|
570
|
+
)
|
|
571
|
+
const glyphHeight = Math.max(ascent + descent, 1)
|
|
572
|
+
const lineHeight = Math.max(
|
|
573
|
+
glyphHeight * PcbScene3dTrueTypeTextFactory.#LINE_HEIGHT_RATIO,
|
|
574
|
+
glyphHeight
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
width: Math.max(...measured.map((metric) => Number(metric.width))),
|
|
579
|
+
height: glyphHeight + lineHeight * (lines.length - 1),
|
|
580
|
+
ascent,
|
|
581
|
+
descent,
|
|
582
|
+
lineHeight
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** @param {TextMetrics[]} measured @param {'actualBoundingBoxAscent' | 'actualBoundingBoxDescent'} field @param {number} fallback @returns {number} */
|
|
587
|
+
static #resolveMeasuredExtent(measured, field, fallback) {
|
|
588
|
+
const values = measured
|
|
589
|
+
.map((metric) => Number(metric?.[field]))
|
|
590
|
+
.filter((value) => Number.isFinite(value) && value > 0)
|
|
591
|
+
|
|
592
|
+
return values.length ? Math.max(...values) : fallback
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** @param {object} text @param {{ width: number, height: number, ascent: number }} metrics @param {number} padding @param {boolean} knockout @returns {{ width: number, height: number, baselineX: number, baselineY: number, anchorX: number, anchorY: number }} */
|
|
596
|
+
static #resolveTextureLayout(text, metrics, padding, knockout) {
|
|
597
|
+
const authoredRectangle =
|
|
598
|
+
PcbScene3dTrueTypeTextFactory.#resolveAuthoredRectangle(
|
|
599
|
+
text,
|
|
600
|
+
knockout
|
|
601
|
+
)
|
|
602
|
+
const width =
|
|
603
|
+
authoredRectangle?.width || Math.max(metrics.width, 1) + padding * 2
|
|
604
|
+
const height =
|
|
605
|
+
authoredRectangle?.height ||
|
|
606
|
+
Math.max(metrics.height, 1) + padding * 2
|
|
607
|
+
|
|
608
|
+
if (!authoredRectangle) {
|
|
609
|
+
return {
|
|
610
|
+
width,
|
|
611
|
+
height,
|
|
612
|
+
baselineX: padding,
|
|
613
|
+
baselineY: padding + Number(metrics.ascent || 0),
|
|
614
|
+
anchorX: padding,
|
|
615
|
+
anchorY: padding + Number(metrics.ascent || 0)
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const baselineX = PcbScene3dTrueTypeTextFactory.#authoredBaselineX(
|
|
619
|
+
text,
|
|
620
|
+
metrics,
|
|
621
|
+
authoredRectangle.width,
|
|
622
|
+
padding
|
|
623
|
+
)
|
|
624
|
+
const baselineY = PcbScene3dTrueTypeTextFactory.#authoredBaselineY(
|
|
625
|
+
text,
|
|
626
|
+
metrics,
|
|
627
|
+
authoredRectangle.height,
|
|
628
|
+
padding
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
width,
|
|
633
|
+
height,
|
|
634
|
+
baselineX,
|
|
635
|
+
baselineY,
|
|
636
|
+
anchorX: baselineX,
|
|
637
|
+
anchorY: baselineY
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Resolves authored inverted rectangle dimensions from source metadata.
|
|
643
|
+
* @param {object} text
|
|
644
|
+
* @param {boolean} knockout
|
|
645
|
+
* @returns {{ width: number, height: number } | null}
|
|
646
|
+
*/
|
|
647
|
+
static #resolveAuthoredRectangle(text, knockout) {
|
|
648
|
+
const width = Number(text?.textboxRectWidth)
|
|
649
|
+
const height = Number(text?.textboxRectHeight)
|
|
650
|
+
|
|
651
|
+
if (
|
|
652
|
+
!knockout ||
|
|
653
|
+
!Boolean(text?.useInvertedRectangle) ||
|
|
654
|
+
!Number.isFinite(width) ||
|
|
655
|
+
!Number.isFinite(height) ||
|
|
656
|
+
width <= 0 ||
|
|
657
|
+
height <= 0
|
|
658
|
+
) {
|
|
659
|
+
return null
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return { width, height }
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Resolves horizontal baseline inside an authored rectangle.
|
|
667
|
+
* @param {object} text
|
|
668
|
+
* @param {{ width: number }} metrics
|
|
669
|
+
* @param {number} width
|
|
670
|
+
* @param {number} padding
|
|
671
|
+
* @returns {number}
|
|
672
|
+
*/
|
|
673
|
+
static #authoredBaselineX(text, metrics, width, padding) {
|
|
674
|
+
const column = PcbScene3dTrueTypeTextFactory.#justificationColumn(text)
|
|
675
|
+
const remainingWidth = Math.max(width - Number(metrics.width || 0), 0)
|
|
676
|
+
|
|
677
|
+
if (column === 1) {
|
|
678
|
+
return remainingWidth / 2
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (column === 2) {
|
|
682
|
+
return remainingWidth
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return Math.min(padding, remainingWidth)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Resolves vertical baseline inside an authored rectangle.
|
|
690
|
+
* @param {object} text
|
|
691
|
+
* @param {{ height: number, ascent: number }} metrics
|
|
692
|
+
* @param {number} height
|
|
693
|
+
* @param {number} padding
|
|
694
|
+
* @returns {number}
|
|
695
|
+
*/
|
|
696
|
+
static #authoredBaselineY(text, metrics, height, padding) {
|
|
697
|
+
const row = PcbScene3dTrueTypeTextFactory.#justificationRow(text)
|
|
698
|
+
const remainingHeight = Math.max(
|
|
699
|
+
height - Number(metrics.height || 0),
|
|
700
|
+
0
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
if (row === 1) {
|
|
704
|
+
return remainingHeight / 2 + Number(metrics.ascent || 0)
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (row === 2) {
|
|
708
|
+
return remainingHeight + Number(metrics.ascent || 0)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return Math.min(padding, remainingHeight) + Number(metrics.ascent || 0)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** @param {object} text @returns {0 | 1 | 2 | null} */
|
|
715
|
+
static #justificationColumn(text) {
|
|
716
|
+
const justification = Number(text?.textboxRectJustification)
|
|
717
|
+
|
|
718
|
+
return Number.isInteger(justification) && justification > 0
|
|
719
|
+
? Math.max(0, Math.min(2, Math.floor((justification - 1) / 3)))
|
|
720
|
+
: null
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** @param {object} text @returns {0 | 1 | 2 | null} */
|
|
724
|
+
static #justificationRow(text) {
|
|
725
|
+
const justification = Number(text?.textboxRectJustification)
|
|
726
|
+
|
|
727
|
+
return Number.isInteger(justification) && justification > 0
|
|
728
|
+
? (justification - 1) % 3
|
|
729
|
+
: null
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Resizes one canvas for high-DPI text rasterization.
|
|
734
|
+
* @param {HTMLCanvasElement} canvas
|
|
735
|
+
* @param {number} width
|
|
736
|
+
* @param {number} height
|
|
737
|
+
* @returns {void}
|
|
738
|
+
*/
|
|
739
|
+
static #sizeCanvas(canvas, width, height) {
|
|
740
|
+
const scale = PcbScene3dTrueTypeTextFactory.#CANVAS_SCALE
|
|
741
|
+
|
|
742
|
+
canvas.width = Math.ceil(width * scale)
|
|
743
|
+
canvas.height = Math.ceil(height * scale)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Draws text into one transparent canvas texture.
|
|
748
|
+
* @param {HTMLCanvasElement} canvas
|
|
749
|
+
* @param {string[]} lines
|
|
750
|
+
* @param {string} font
|
|
751
|
+
* @param {{ color: number, textColor: number, knockout: boolean }} paint
|
|
752
|
+
* @param {{ ascent: number, lineHeight: number }} metrics
|
|
753
|
+
* @param {number} baselineX
|
|
754
|
+
* @param {number} baselineY
|
|
755
|
+
* @param {number} width
|
|
756
|
+
* @param {number} height
|
|
757
|
+
* @returns {void}
|
|
758
|
+
*/
|
|
759
|
+
static #drawText(
|
|
760
|
+
canvas,
|
|
761
|
+
lines,
|
|
762
|
+
font,
|
|
763
|
+
paint,
|
|
764
|
+
metrics,
|
|
765
|
+
baselineX,
|
|
766
|
+
baselineY,
|
|
767
|
+
width,
|
|
768
|
+
height
|
|
769
|
+
) {
|
|
770
|
+
const context = canvas.getContext('2d')
|
|
771
|
+
const scale = PcbScene3dTrueTypeTextFactory.#CANVAS_SCALE
|
|
772
|
+
|
|
773
|
+
context.clearRect(0, 0, canvas.width, canvas.height)
|
|
774
|
+
context.scale?.(scale, scale)
|
|
775
|
+
context.font = font
|
|
776
|
+
context.textAlign = 'left'
|
|
777
|
+
context.textBaseline = 'alphabetic'
|
|
778
|
+
|
|
779
|
+
if (paint.knockout) {
|
|
780
|
+
context.fillStyle = PcbScene3dTrueTypeTextFactory.#colorToCss(
|
|
781
|
+
paint.color
|
|
782
|
+
)
|
|
783
|
+
context.fillRect?.(0, 0, width, height)
|
|
784
|
+
context.globalCompositeOperation = 'destination-out'
|
|
785
|
+
context.fillStyle = '#000000'
|
|
786
|
+
} else {
|
|
787
|
+
context.fillStyle = PcbScene3dTrueTypeTextFactory.#colorToCss(
|
|
788
|
+
paint.textColor
|
|
789
|
+
)
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
lines.forEach((line, index) => {
|
|
793
|
+
context.fillText(
|
|
794
|
+
line,
|
|
795
|
+
baselineX,
|
|
796
|
+
baselineY + metrics.lineHeight * index
|
|
797
|
+
)
|
|
798
|
+
})
|
|
799
|
+
context.globalCompositeOperation = 'source-over'
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Builds a transparent unlit material for one text texture.
|
|
804
|
+
* @param {any} THREE
|
|
805
|
+
* @param {{ texture: any }} textureInfo
|
|
806
|
+
* @returns {any}
|
|
807
|
+
*/
|
|
808
|
+
static #buildMaterial(THREE, textureInfo) {
|
|
809
|
+
return new THREE.MeshBasicMaterial({
|
|
810
|
+
map: textureInfo.texture,
|
|
811
|
+
transparent: true,
|
|
812
|
+
opacity: 0.96,
|
|
813
|
+
alphaTest: 0.02,
|
|
814
|
+
depthWrite: false,
|
|
815
|
+
toneMapped: false,
|
|
816
|
+
fog: false,
|
|
817
|
+
side: THREE.DoubleSide
|
|
818
|
+
})
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Normalizes one board point and optionally mirrors it for underside use.
|
|
823
|
+
* @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
|
|
824
|
+
* @param {number} x
|
|
825
|
+
* @param {number} y
|
|
826
|
+
* @param {boolean} mirrorY
|
|
827
|
+
* @returns {{ x: number, y: number }}
|
|
828
|
+
*/
|
|
829
|
+
static #normalizePoint(normalizeBoardPoint, x, y, mirrorY) {
|
|
830
|
+
const point = normalizeBoardPoint(x, y)
|
|
831
|
+
|
|
832
|
+
return {
|
|
833
|
+
x: point.x,
|
|
834
|
+
y: mirrorY ? -point.y : point.y
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Converts a numeric RGB color into CSS hex syntax.
|
|
840
|
+
* @param {number} color
|
|
841
|
+
* @returns {string}
|
|
842
|
+
*/
|
|
843
|
+
static #colorToCss(color) {
|
|
844
|
+
return `#${Number(color).toString(16).padStart(6, '0')}`
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Resolves a safe RGB material color.
|
|
849
|
+
* @param {unknown} color
|
|
850
|
+
* @returns {number}
|
|
851
|
+
*/
|
|
852
|
+
static #resolveMaterialColor(color) {
|
|
853
|
+
const numericColor = Number(color)
|
|
854
|
+
|
|
855
|
+
return Number.isInteger(numericColor) &&
|
|
856
|
+
numericColor >= 0 &&
|
|
857
|
+
numericColor <= 0xffffff
|
|
858
|
+
? numericColor
|
|
859
|
+
: PcbScene3dTrueTypeTextFactory.#DEFAULT_MATERIAL_COLOR
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Resolves how one TrueType text primitive should be painted.
|
|
864
|
+
* @param {object} text
|
|
865
|
+
* @param {{ materialColor?: number, invertedMaterialColor?: number }} options
|
|
866
|
+
* @returns {{ color: number, textColor: number, knockout: boolean }}
|
|
867
|
+
*/
|
|
868
|
+
static #resolveTextPaint(text, options) {
|
|
869
|
+
const materialColor =
|
|
870
|
+
PcbScene3dTrueTypeTextFactory.#resolveMaterialColor(
|
|
871
|
+
options?.materialColor
|
|
872
|
+
)
|
|
873
|
+
const invertedMaterialColor =
|
|
874
|
+
PcbScene3dTrueTypeTextFactory.#resolveMaterialColor(
|
|
875
|
+
options?.invertedMaterialColor
|
|
876
|
+
)
|
|
877
|
+
const usesInvertedText = Boolean(text?.isInverted)
|
|
878
|
+
|
|
879
|
+
return {
|
|
880
|
+
color: materialColor,
|
|
881
|
+
textColor: usesInvertedText ? invertedMaterialColor : materialColor,
|
|
882
|
+
knockout: usesInvertedText
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|