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,671 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
const strokeFontScale = 1 / 21
|
|
5
|
+
const fontOffset = -8
|
|
6
|
+
const firstPrintableCodePoint = 32
|
|
7
|
+
const fallbackGlyphIndex = '?'.charCodeAt(0) - firstPrintableCodePoint
|
|
8
|
+
const tabWidth = 4
|
|
9
|
+
const overbarTrimRatio = 0.1
|
|
10
|
+
const overbarHeightRatio = 1.23
|
|
11
|
+
const superSubSizeMultiplier = 0.8
|
|
12
|
+
const superscriptHeightOffset = 0.35
|
|
13
|
+
const subscriptHeightOffset = 0.15
|
|
14
|
+
const textStyleSubscript = 1
|
|
15
|
+
const textStyleSuperscript = 2
|
|
16
|
+
|
|
17
|
+
// Printable ASCII subset of KiCad's newstroke_font.cpp glyph table.
|
|
18
|
+
const asciiNewstrokeFont = [
|
|
19
|
+
'JZ',
|
|
20
|
+
'MWRYSZR[QZRYR[ RRSQGRFSGRSRF',
|
|
21
|
+
'JZNFNJ RVFVJ',
|
|
22
|
+
'H]LM[M RRDL_ RYVJV RS_YD',
|
|
23
|
+
'H\\LZO[T[VZWYXWXUWSVRTQPPNOMNLLLJMHNGPFUFXG RRCR^',
|
|
24
|
+
'F^J[ZF RMFOGPIOKMLKKJIKGMF RYZZXYVWUUVTXUZW[YZ',
|
|
25
|
+
'E_[[Z[XZUWPQNNMKMINGPFQFSGTITJSLRMLQKRJTJWKYLZN[Q[SZTYWUXRXP',
|
|
26
|
+
'MWSFQJ',
|
|
27
|
+
'KYVcUbS_R]QZPUPQQLRISGUDVC',
|
|
28
|
+
'KYNcObQ_R]SZTUTQSLRIQGODNC',
|
|
29
|
+
'JZRFRK RMIRKWI ROORKUO',
|
|
30
|
+
'E_JSZS RR[RK',
|
|
31
|
+
'MWSZS[R]Q^',
|
|
32
|
+
'E_JSZS',
|
|
33
|
+
'MWRYSZR[QZRYR[',
|
|
34
|
+
'G][EI`',
|
|
35
|
+
'H\\QFSFUGVHWJXNXSWWVYUZS[Q[OZNYMWLSLNMJNHOGQF',
|
|
36
|
+
'H\\X[L[ RR[RFPINKLL',
|
|
37
|
+
'H\\LHMGOFTFVGWHXJXLWOK[X[',
|
|
38
|
+
'H\\KFXFQNTNVOWPXRXWWYVZT[N[LZKY',
|
|
39
|
+
'H\\VMV[ RQELTYT',
|
|
40
|
+
'H\\WFMFLPMOONTNVOWPXRXWWYVZT[O[MZLY',
|
|
41
|
+
'H\\VFRFPGOHMKLOLWMYNZP[T[VZWYXWXRWPVOTNPNNOMPLR',
|
|
42
|
+
'H\\KFYFP[',
|
|
43
|
+
'H\\PONNMMLKLJMHNGPFTFVGWHXJXKWMVNTOPONPMQLSLWMYNZP[T[VZWYXWXSWQVPTO',
|
|
44
|
+
'H\\N[R[TZUYWVXRXJWHVGTFPFNGMHLJLOMQNRPSTSVRWQXO',
|
|
45
|
+
'MWRYSZR[QZRYR[ RRNSORPQORNRP',
|
|
46
|
+
'MWSZS[R]Q^ RRNSORPQORNRP',
|
|
47
|
+
'E_ZMJSZY',
|
|
48
|
+
'E_JPZP RZVJV',
|
|
49
|
+
'E_JMZSJY',
|
|
50
|
+
'I[QYRZQ[PZQYQ[ RMGOFTFVGWIWKVMUNSORPQRQS',
|
|
51
|
+
'D_VQUPSOQOOPNQMSMUNWOXQYSYUXVW RVOVWWXXXZW[U[PYMVKRJNKKMIPHTIXK[N]R^V]Y[',
|
|
52
|
+
'I[MUWU RK[RFY[',
|
|
53
|
+
'G\\SPVQWRXTXWWYVZT[L[LFSFUGVHWJWLVNUOSPLP',
|
|
54
|
+
'F[WYVZS[Q[NZLXKVJRJOKKLINGQFSFVGWH',
|
|
55
|
+
'G\\L[LFQFTGVIWKXOXRWVVXTZQ[L[',
|
|
56
|
+
'H[MPTP RW[M[MFWF',
|
|
57
|
+
'HZTPMP RM[MFWF',
|
|
58
|
+
'F[VGTFQFNGLIKKJOJRKVLXNZQ[S[VZWYWRSR',
|
|
59
|
+
'G]L[LF RLPXP RX[XF',
|
|
60
|
+
'MWR[RF',
|
|
61
|
+
'JZUFUUTXRZO[M[',
|
|
62
|
+
'G\\L[LF RX[OO RXFLR',
|
|
63
|
+
'HYW[M[MF',
|
|
64
|
+
'F^K[KFRUYFY[',
|
|
65
|
+
'G]L[LFX[XF',
|
|
66
|
+
'G]PFTFVGXIYMYTXXVZT[P[NZLXKTKMLINGPF',
|
|
67
|
+
'G\\L[LFTFVGWHXJXMWOVPTQLQ',
|
|
68
|
+
'G]Z]X\\VZSWQVOV RP[NZLXKTKMLINGPFTFVGXIYMYTXXVZT[P[',
|
|
69
|
+
'G\\X[QQ RL[LFTFVGWHXJXMWOVPTQLQ',
|
|
70
|
+
'H\\LZO[T[VZWYXWXUWSVRTQPPNOMNLLLJMHNGPFUFXG',
|
|
71
|
+
'JZLFXF RR[RF',
|
|
72
|
+
'G]LFLWMYNZP[T[VZWYXWXF',
|
|
73
|
+
'I[KFR[YF',
|
|
74
|
+
'F^IFN[RLV[[F',
|
|
75
|
+
'H\\KFY[ RYFK[',
|
|
76
|
+
'I[RQR[ RKFRQYF',
|
|
77
|
+
'H\\KFYFK[Y[',
|
|
78
|
+
'KYVbQbQDVD',
|
|
79
|
+
'KYID[_',
|
|
80
|
+
'KYNbSbSDND',
|
|
81
|
+
'LXNHREVH',
|
|
82
|
+
'JZJ]Z]',
|
|
83
|
+
'NVPESH',
|
|
84
|
+
'I\\W[WPVNTMPMNN RWZU[P[NZMXMVNTPSUSWR',
|
|
85
|
+
'H[M[MF RMNOMSMUNVOWQWWVYUZS[O[MZ',
|
|
86
|
+
'HZVZT[P[NZMYLWLQMONNPMTMVN',
|
|
87
|
+
'I\\W[WF RWZU[Q[OZNYMWMQNOONQMUMWN',
|
|
88
|
+
'I[VZT[P[NZMXMPNNPMTMVNWPWRMT',
|
|
89
|
+
'MYOMWM RR[RISGUFWF',
|
|
90
|
+
'I\\WMW^V`UaSbPbNa RWZU[Q[OZNYMWMQNOONQMUMWN',
|
|
91
|
+
'H[M[MF RV[VPUNSMPMNNMO',
|
|
92
|
+
'MWR[RM RRFQGRHSGRFRH',
|
|
93
|
+
'MWRMR_QaObNb RRFQGRHSGRFRH',
|
|
94
|
+
'IZN[NF RPSV[ RVMNU',
|
|
95
|
+
'MXU[SZRXRF',
|
|
96
|
+
'D`I[IM RIOJNLMOMQNRPR[ RRPSNUMXMZN[P[[',
|
|
97
|
+
'I\\NMN[ RNOONQMTMVNWPW[',
|
|
98
|
+
'H[P[NZMYLWLQMONNPMSMUNVOWQWWVYUZS[P[',
|
|
99
|
+
'H[MMMb RMNOMSMUNVOWQWWVYUZS[O[MZ',
|
|
100
|
+
'I\\WMWb RWZU[Q[OZNYMWMQNOONQMUMWN',
|
|
101
|
+
'KXP[PM RPQQORNTMVM',
|
|
102
|
+
'J[NZP[T[VZWXWWVUTTQTOSNQNPONQMTMVN',
|
|
103
|
+
'MYOMWM RRFRXSZU[W[',
|
|
104
|
+
'H[VMV[ RMMMXNZP[S[UZVY',
|
|
105
|
+
'JZMMR[WM',
|
|
106
|
+
'G]JMN[RQV[ZM',
|
|
107
|
+
'IZL[WM RLMW[',
|
|
108
|
+
'JZMMR[ RWMR[P`OaMb',
|
|
109
|
+
'IZLMWML[W[',
|
|
110
|
+
'KYVcUcSbR`RVQTOSQRRPRFSDUCVC',
|
|
111
|
+
'H\\RbRD',
|
|
112
|
+
'KYNcOcQbR`RVSTUSSRRPRFQDOCNC',
|
|
113
|
+
'KZMSNRPQTSVRWQ'
|
|
114
|
+
]
|
|
115
|
+
// Latin-1 Supplement subset from KiCad's generated newstroke_font.cpp glyph table.
|
|
116
|
+
const latin1NewstrokeFont = new Map([
|
|
117
|
+
[0x00a0, 'JZ'],
|
|
118
|
+
[0x00a1, 'MWROQNRMSNRORM RRUSaRbQaRURb'],
|
|
119
|
+
[0x00a2, 'HZVZT[P[NZMYLWLQMONNPMTMVN RRJR^'],
|
|
120
|
+
[0x00a3, 'H[LMTM RL[W[ RO[OIPGRFUFWG'],
|
|
121
|
+
[0x00a4, 'H]LYOV RLLOO RVVYY RVOYL RVVTWQWOVNTNQOOQNTNVOWQWTVV'],
|
|
122
|
+
[0x00a5, 'F^JTZT RJMZM RRQR[ RKFRQYF'],
|
|
123
|
+
[0x00a6, 'MWRbRW RRFRQ'],
|
|
124
|
+
[0x00a7, 'I[N]P^S^U]V[UYOSNQNPONQM RVGTFQFOGNIOKUQVSVTUVSW'],
|
|
125
|
+
[0x00a8, 'LXNFOGNHMGNFNH RVFWGVHUGVFVH'],
|
|
126
|
+
[
|
|
127
|
+
0x00a9,
|
|
128
|
+
'@dVKTJPJNKLMKOKSLUNWPXTXVW RRCMDHGELDQEVH[M^R_W^\\[_V`Q_L\\GWDRC'
|
|
129
|
+
],
|
|
130
|
+
[0x00aa, 'KZOEQDSDUEVGVN RVMTNQNOMNKOIQHVH'],
|
|
131
|
+
[0x00ab, 'H\\RMLSRY RXWTSXO'],
|
|
132
|
+
[0x00ac, 'E_JQZQZV'],
|
|
133
|
+
[0x00ad, 'RR'],
|
|
134
|
+
[
|
|
135
|
+
0x00ae,
|
|
136
|
+
'@dWXRR RNXNJTJVKWMWOVQTRNR RRCMDHGELDQEVH[M^R_W^\\[_V`Q_L\\GWDRC'
|
|
137
|
+
],
|
|
138
|
+
[0x00af, 'LXMGWG'],
|
|
139
|
+
[0x00b0, 'JZRFPGOIPKRLTKUITGRF'],
|
|
140
|
+
[0x00b1, 'E_JOZO RRWRG RZ[J['],
|
|
141
|
+
[0x00b2, 'JZNAP@S@UAVCVEUGNNVN'],
|
|
142
|
+
[0x00b3, 'JZN@V@RESEUFVHVKUMSNPNNM'],
|
|
143
|
+
[0x00b4, 'NVTEQH'],
|
|
144
|
+
[0x00b5, 'H^MMMb RWXXZZ[ RMXNZP[T[VZWXWM'],
|
|
145
|
+
[0x00b6, 'F]VMV[ ROMOXNZL[ RZMMMKNJP'],
|
|
146
|
+
[0x00b7, 'JZRRQSRTSSRRRT'],
|
|
147
|
+
[0x00b8, 'MWR\\T]U_TaRbOb'],
|
|
148
|
+
[0x00b9, 'JZVNNN RNCPBR@RN'],
|
|
149
|
+
[0x00ba, 'KYQNOMNKNGOEQDSDUEVGVKUMSNQN'],
|
|
150
|
+
[0x00bb, 'H\\RMXSRY RLWPSLO'],
|
|
151
|
+
[0x00bc, 'G]KQYQ RVNNN RNCPBR@RN RUYUa RQSN]W]'],
|
|
152
|
+
[0x00bd, 'G]KQYQ RVNNN RNCPBR@RN RNTPSSSUTVVVXUZNaVa'],
|
|
153
|
+
[0x00be, 'G]KQYQ RN@V@RESEUFVHVKUMSNPNNM RUYUa RQSN]W]'],
|
|
154
|
+
[0x00bf, 'I[SORNSMTNSOSM RWaUbPbNaM_M]N[OZQYRXSVSU'],
|
|
155
|
+
[0x00c0, 'I[MUWU RK[RFY[ RP>SA'],
|
|
156
|
+
[0x00c1, 'I[MUWU RK[RFY[ RT>QA'],
|
|
157
|
+
[0x00c2, 'I[MUWU RK[RFY[ RNAR>VA'],
|
|
158
|
+
[0x00c3, 'I[MUWU RK[RFY[ RMAN@P?TAV@W?'],
|
|
159
|
+
[0x00c4, 'I[MUWU RK[RFY[ RN?O@NAM@N?NA RV?W@VAU@V?VA'],
|
|
160
|
+
[0x00c5, 'I[MUWU RK[RFY[ RRFPEOCPAR@TAUCTERF'],
|
|
161
|
+
[0x00c6, 'F`JURU RRPYP RH[OF\\F RRFR[\\['],
|
|
162
|
+
[0x00c7, 'F[WYVZS[Q[NZLXKVJRJOKKLINGQFSFVGWH RR\\T]U_TaRbOb'],
|
|
163
|
+
[0x00c8, 'H[MPTP RW[M[MFWF RP>SA'],
|
|
164
|
+
[0x00c9, 'H[MPTP RW[M[MFWF RT>QA'],
|
|
165
|
+
[0x00ca, 'H[MPTP RW[M[MFWF RNAR>VA'],
|
|
166
|
+
[0x00cb, 'H[MPTP RW[M[MFWF RN?O@NAM@N?NA RV?W@VAU@V?VA'],
|
|
167
|
+
[0x00cc, 'MWR[RF RP>SA'],
|
|
168
|
+
[0x00cd, 'MWR[RF RT>QA'],
|
|
169
|
+
[0x00ce, 'MWR[RF RNAR>VA'],
|
|
170
|
+
[0x00cf, 'MWR[RF RN?O@NAM@N?NA RV?W@VAU@V?VA'],
|
|
171
|
+
[0x00d0, 'G\\L[LFQFTGVIWKXOXRWVVXTZQ[L[ RIPQP'],
|
|
172
|
+
[0x00d1, 'G]L[LFX[XF RMAN@P?TAV@W?'],
|
|
173
|
+
[0x00d2, 'G]PFTFVGXIYMYTXXVZT[P[NZLXKTKMLINGPF RP>SA'],
|
|
174
|
+
[0x00d3, 'G]PFTFVGXIYMYTXXVZT[P[NZLXKTKMLINGPF RT>QA'],
|
|
175
|
+
[0x00d4, 'G]PFTFVGXIYMYTXXVZT[P[NZLXKTKMLINGPF RNAR>VA'],
|
|
176
|
+
[0x00d5, 'G]PFTFVGXIYMYTXXVZT[P[NZLXKTKMLINGPF RMAN@P?TAV@W?'],
|
|
177
|
+
[
|
|
178
|
+
0x00d6,
|
|
179
|
+
'G]PFTFVGXIYMYTXXVZT[P[NZLXKTKMLINGPF RN?O@NAM@N?NA RV?W@VAU@V?VA'
|
|
180
|
+
],
|
|
181
|
+
[0x00d7, 'E_LMXY RXMLY'],
|
|
182
|
+
[0x00d8, 'G]ZFJ[ RP[NZLXKTKMLINGPFTFVGXIYMYTXXVZT[P['],
|
|
183
|
+
[0x00d9, 'G]LFLWMYNZP[T[VZWYXWXF RP>SA'],
|
|
184
|
+
[0x00da, 'G]LFLWMYNZP[T[VZWYXWXF RT>QA'],
|
|
185
|
+
[0x00db, 'G]LFLWMYNZP[T[VZWYXWXF RNAR>VA'],
|
|
186
|
+
[0x00dc, 'G]LFLWMYNZP[T[VZWYXWXF RN?O@NAM@N?NA RV?W@VAU@V?VA'],
|
|
187
|
+
[0x00dd, 'I[RQR[ RKFRQYF RT>QA'],
|
|
188
|
+
[0x00de, 'G\\LFL[ RLKTKVLWMXOXRWTVUTVLV'],
|
|
189
|
+
[0x00df, 'F]K[KJLHMGOFRFTGUHVJVMSMQNPPPQQSSTVTXUYWYXXZV[R[PZ'],
|
|
190
|
+
[0x00e0, 'I\\W[WPVNTMPMNN RWZU[P[NZMXMVNTPSUSWR RPESH'],
|
|
191
|
+
[0x00e1, 'I\\W[WPVNTMPMNN RWZU[P[NZMXMVNTPSUSWR RTEQH'],
|
|
192
|
+
[0x00e2, 'I\\W[WPVNTMPMNN RWZU[P[NZMXMVNTPSUSWR RNHREVH'],
|
|
193
|
+
[0x00e3, 'I\\W[WPVNTMPMNN RWZU[P[NZMXMVNTPSUSWR RMHNGPFTHVGWF'],
|
|
194
|
+
[
|
|
195
|
+
0x00e4,
|
|
196
|
+
'I\\W[WPVNTMPMNN RWZU[P[NZMXMVNTPSUSWR RNFOGNHMGNFNH RVFWGVHUGVFVH'
|
|
197
|
+
],
|
|
198
|
+
[0x00e5, 'I\\W[WPVNTMPMNN RWZU[P[NZMXMVNTPSUSWR RRHPGOEPCRBTCUETGRH'],
|
|
199
|
+
[0x00e6, 'D`INKMOMQNRP R[ZY[U[SZRXRPSNUMYM[N\\P\\RRSKSITHVHXIZK[O[QZRX'],
|
|
200
|
+
[0x00e7, 'HZVZT[P[NZMYLWLQMONNPMTMVN RR\\T]U_TaRbOb'],
|
|
201
|
+
[0x00e8, 'I[VZT[P[NZMXMPNNPMTMVNWPWRMT RPESH'],
|
|
202
|
+
[0x00e9, 'I[VZT[P[NZMXMPNNPMTMVNWPWRMT RTEQH'],
|
|
203
|
+
[0x00ea, 'I[VZT[P[NZMXMPNNPMTMVNWPWRMT RNHREVH'],
|
|
204
|
+
[0x00eb, 'I[VZT[P[NZMXMPNNPMTMVNWPWRMT RNFOGNHMGNFNH RVFWGVHUGVFVH'],
|
|
205
|
+
[0x00ec, 'MWR[RM RPESH'],
|
|
206
|
+
[0x00ed, 'MWR[RM RTEQH'],
|
|
207
|
+
[0x00ee, 'LXNHREVH RR[RM'],
|
|
208
|
+
[0x00ef, 'LXNFOGNHMGNFNH RVFWGVHUGVFVH RR[RM'],
|
|
209
|
+
[0x00f0, 'I\\SCQI RWNUMQMONNOMQMXNZP[T[VZWXWLVITGRFNE'],
|
|
210
|
+
[0x00f1, 'I\\NMN[ RNOONQMTMVNWPW[ RMHNGPFTHVGWF'],
|
|
211
|
+
[0x00f2, 'H[P[NZMYLWLQMONNPMSMUNVOWQWWVYUZS[P[ RPESH'],
|
|
212
|
+
[0x00f3, 'H[P[NZMYLWLQMONNPMSMUNVOWQWWVYUZS[P[ RTEQH'],
|
|
213
|
+
[0x00f4, 'H[P[NZMYLWLQMONNPMSMUNVOWQWWVYUZS[P[ RNHREVH'],
|
|
214
|
+
[0x00f5, 'H[P[NZMYLWLQMONNPMSMUNVOWQWWVYUZS[P[ RMHNGPFTHVGWF'],
|
|
215
|
+
[
|
|
216
|
+
0x00f6,
|
|
217
|
+
'H[P[NZMYLWLQMONNPMSMUNVOWQWWVYUZS[P[ RNFOGNHMGNFNH RVFWGVHUGVFVH'
|
|
218
|
+
],
|
|
219
|
+
[0x00f7, 'E_ZSJS RRXSYRZQYRXRZ RRLSMRNQMRLRN'],
|
|
220
|
+
[0x00f8, 'H[XMK[ RP[NZMYLWLQMONNPMSMUNVOWQWWVYUZS[P['],
|
|
221
|
+
[0x00f9, 'H[VMV[ RMMMXNZP[S[UZVY RPESH'],
|
|
222
|
+
[0x00fa, 'H[VMV[ RMMMXNZP[S[UZVY RTEQH'],
|
|
223
|
+
[0x00fb, 'H[VMV[ RMMMXNZP[S[UZVY RNHREVH'],
|
|
224
|
+
[0x00fc, 'H[VMV[ RMMMXNZP[S[UZVY RNFOGNHMGNFNH RVFWGVHUGVFVH'],
|
|
225
|
+
[0x00fd, 'JZMMR[ RWMR[P`OaMb RTEQH'],
|
|
226
|
+
[0x00fe, 'H[MFMb RMNOMSMUNVOWQWWVYUZS[O[MZ'],
|
|
227
|
+
[0x00ff, 'JZMMR[ RWMR[P`OaMb RNFOGNHMGNFNH RVFWGVHUGVFVH']
|
|
228
|
+
])
|
|
229
|
+
const glyphs = asciiNewstrokeFont.map(parseGlyph)
|
|
230
|
+
const latin1Glyphs = new Map(
|
|
231
|
+
[...latin1NewstrokeFont].map(([codePoint, glyph]) => [
|
|
232
|
+
codePoint,
|
|
233
|
+
parseGlyph(glyph)
|
|
234
|
+
])
|
|
235
|
+
)
|
|
236
|
+
const spaceWidth = glyphs[0].bounds.maxX
|
|
237
|
+
const lineLayoutCacheLimit = 2048
|
|
238
|
+
const lineLayoutCache = new Map()
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* KiCad NewStroke glyph renderer for PCB text.
|
|
242
|
+
*/
|
|
243
|
+
export class PcbScene3dStrokeFont {
|
|
244
|
+
/**
|
|
245
|
+
* Measures one text line using KiCad's full stroke glyph cursor advance.
|
|
246
|
+
* @param {string} value
|
|
247
|
+
* @param {number} sizeX
|
|
248
|
+
* @returns {number}
|
|
249
|
+
*/
|
|
250
|
+
static measureLine(value, sizeX) {
|
|
251
|
+
return lineAdvance(value, sizeX)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Converts one text line into stroke point lists and its advance width.
|
|
256
|
+
* @param {string} value
|
|
257
|
+
* @param {{ x: number, y: number, sizeX: number, sizeY: number }} attrs
|
|
258
|
+
* @returns {{ width: number, strokes: { x: number, y: number }[][] }}
|
|
259
|
+
*/
|
|
260
|
+
static layoutLine(value, attrs) {
|
|
261
|
+
return layoutLine(value, attrs)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Converts one text line into KiCad-scaled stroke point lists.
|
|
266
|
+
* @param {string} value
|
|
267
|
+
* @param {{ x: number, y: number, sizeX: number, sizeY: number }} attrs
|
|
268
|
+
* @returns {{ x: number, y: number }[][]}
|
|
269
|
+
*/
|
|
270
|
+
static strokeLine(value, attrs) {
|
|
271
|
+
return layoutLine(value, attrs).strokes
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Measures one KiCad markup-aware text line.
|
|
277
|
+
* @param {string} value Text line.
|
|
278
|
+
* @param {number} sizeX Glyph width.
|
|
279
|
+
* @returns {number}
|
|
280
|
+
*/
|
|
281
|
+
function lineAdvance(value, sizeX) {
|
|
282
|
+
return cachedLineLayout(value, sizeX, sizeX).width
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Lays out one line at the requested origin.
|
|
287
|
+
* @param {string} value Text line.
|
|
288
|
+
* @param {{ x: number, y: number, sizeX: number, sizeY: number }} attrs Text attributes.
|
|
289
|
+
* @returns {{ width: number, strokes: { x: number, y: number }[][] }}
|
|
290
|
+
*/
|
|
291
|
+
function layoutLine(value, attrs) {
|
|
292
|
+
const state = textRunState(attrs)
|
|
293
|
+
const layout = cachedLineLayout(value, state.sizeX, state.sizeY)
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
width: layout.width,
|
|
297
|
+
strokes: offsetStrokes(layout.strokes, state.x, state.y)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Resolves a reusable zero-origin line layout.
|
|
303
|
+
* @param {string} value Text line.
|
|
304
|
+
* @param {number} sizeX Glyph width.
|
|
305
|
+
* @param {number} sizeY Glyph height.
|
|
306
|
+
* @returns {{ width: number, strokes: { x: number, y: number }[][] }}
|
|
307
|
+
*/
|
|
308
|
+
function cachedLineLayout(value, sizeX, sizeY) {
|
|
309
|
+
const normalizedValue = String(value || '')
|
|
310
|
+
const normalizedSizeX = Number(sizeX || 1)
|
|
311
|
+
const normalizedSizeY = Number(sizeY || normalizedSizeX || 1)
|
|
312
|
+
const cacheKey = `${normalizedValue}\u0000${normalizedSizeX}\u0000${normalizedSizeY}`
|
|
313
|
+
const cached = lineLayoutCache.get(cacheKey)
|
|
314
|
+
|
|
315
|
+
if (cached) {
|
|
316
|
+
lineLayoutCache.delete(cacheKey)
|
|
317
|
+
lineLayoutCache.set(cacheKey, cached)
|
|
318
|
+
return cached
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const strokes = []
|
|
322
|
+
const width = Math.max(
|
|
323
|
+
strokeMarkupNodes(
|
|
324
|
+
parseMarkup(normalizedValue),
|
|
325
|
+
{ x: 0, y: 0, sizeX: normalizedSizeX, sizeY: normalizedSizeY },
|
|
326
|
+
0,
|
|
327
|
+
strokes
|
|
328
|
+
),
|
|
329
|
+
0
|
|
330
|
+
)
|
|
331
|
+
const layout = { width, strokes }
|
|
332
|
+
lineLayoutCache.set(cacheKey, layout)
|
|
333
|
+
|
|
334
|
+
if (lineLayoutCache.size > lineLayoutCacheLimit) {
|
|
335
|
+
lineLayoutCache.delete(lineLayoutCache.keys().next().value)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return layout
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Copies cached zero-origin strokes to a requested line origin.
|
|
343
|
+
* @param {{ x: number, y: number }[][]} strokes Cached strokes.
|
|
344
|
+
* @param {number} offsetX X offset.
|
|
345
|
+
* @param {number} offsetY Y offset.
|
|
346
|
+
* @returns {{ x: number, y: number }[][]}
|
|
347
|
+
*/
|
|
348
|
+
function offsetStrokes(strokes, offsetX, offsetY) {
|
|
349
|
+
return strokes.map((stroke) =>
|
|
350
|
+
stroke.map((point) => ({
|
|
351
|
+
x: point.x + offsetX,
|
|
352
|
+
y: point.y + offsetY
|
|
353
|
+
}))
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Resolves a printable glyph, falling back like KiCad's stroke font.
|
|
359
|
+
* @param {string} char Character.
|
|
360
|
+
* @returns {object}
|
|
361
|
+
*/
|
|
362
|
+
function glyphForCharacter(char) {
|
|
363
|
+
const codePoint = char.codePointAt(0)
|
|
364
|
+
const index = codePoint - firstPrintableCodePoint
|
|
365
|
+
|
|
366
|
+
if (index >= 0 && index < glyphs.length && glyphs[index]) {
|
|
367
|
+
return glyphs[index]
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return latin1Glyphs.get(codePoint) || glyphs[fallbackGlyphIndex]
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Parses KiCad text markup commands.
|
|
375
|
+
* @param {string} value Text value.
|
|
376
|
+
* @returns {object[]}
|
|
377
|
+
*/
|
|
378
|
+
function parseMarkup(value) {
|
|
379
|
+
return parseMarkupUntil(String(value || ''), 0, '').nodes
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Parses markup nodes until an optional terminator.
|
|
384
|
+
* @param {string} value Text value.
|
|
385
|
+
* @param {number} start Start index.
|
|
386
|
+
* @param {string} terminator Terminator character.
|
|
387
|
+
* @returns {{ nodes: object[], index: number, closed: boolean }}
|
|
388
|
+
*/
|
|
389
|
+
function parseMarkupUntil(value, start, terminator) {
|
|
390
|
+
const nodes = []
|
|
391
|
+
let text = ''
|
|
392
|
+
let index = start
|
|
393
|
+
|
|
394
|
+
while (index < value.length) {
|
|
395
|
+
const char = value[index]
|
|
396
|
+
|
|
397
|
+
if (terminator && char === terminator) {
|
|
398
|
+
pushTextNode(nodes, text)
|
|
399
|
+
return { nodes, index, closed: true }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (isMarkupCommand(value, index)) {
|
|
403
|
+
const parsed = parseMarkupUntil(value, index + 2, '}')
|
|
404
|
+
|
|
405
|
+
if (parsed.closed) {
|
|
406
|
+
pushTextNode(nodes, text)
|
|
407
|
+
text = ''
|
|
408
|
+
nodes.push({
|
|
409
|
+
type: markupCommandType(char),
|
|
410
|
+
children: parsed.nodes
|
|
411
|
+
})
|
|
412
|
+
index = parsed.index + 1
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
text += char
|
|
418
|
+
index += 1
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
pushTextNode(nodes, text)
|
|
422
|
+
return { nodes, index, closed: !terminator }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Appends a text node when the text is non-empty.
|
|
427
|
+
* @param {object[]} nodes Node list.
|
|
428
|
+
* @param {string} text Text content.
|
|
429
|
+
*/
|
|
430
|
+
function pushTextNode(nodes, text) {
|
|
431
|
+
if (text) nodes.push({ type: 'text', value: text })
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Checks for a KiCad markup command prefix.
|
|
436
|
+
* @param {string} value Text value.
|
|
437
|
+
* @param {number} index Candidate index.
|
|
438
|
+
* @returns {boolean}
|
|
439
|
+
*/
|
|
440
|
+
function isMarkupCommand(value, index) {
|
|
441
|
+
return ['~', '_', '^'].includes(value[index]) && value[index + 1] === '{'
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Maps KiCad markup command characters to node types.
|
|
446
|
+
* @param {string} char Command character.
|
|
447
|
+
* @returns {string}
|
|
448
|
+
*/
|
|
449
|
+
function markupCommandType(char) {
|
|
450
|
+
if (char === '~') return 'overbar'
|
|
451
|
+
if (char === '_') return 'subscript'
|
|
452
|
+
return 'superscript'
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Creates a text-run state from stroke-font attributes.
|
|
457
|
+
* @param {{ x: number, y: number, sizeX: number, sizeY: number }} attrs Text attributes.
|
|
458
|
+
* @returns {{ x: number, y: number, sizeX: number, sizeY: number }}
|
|
459
|
+
*/
|
|
460
|
+
function textRunState(attrs) {
|
|
461
|
+
return {
|
|
462
|
+
x: Number(attrs.x || 0),
|
|
463
|
+
y: Number(attrs.y || 0),
|
|
464
|
+
sizeX: Number(attrs.sizeX || 1),
|
|
465
|
+
sizeY: Number(attrs.sizeY || attrs.sizeX || 1)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Converts markup nodes into stroke paths and returns the next cursor x.
|
|
471
|
+
* @param {object[]} nodes Markup nodes.
|
|
472
|
+
* @param {object} state Text run state.
|
|
473
|
+
* @param {number} style Text style flags.
|
|
474
|
+
* @param {Array[] | undefined} strokes Stroke output.
|
|
475
|
+
* @returns {number}
|
|
476
|
+
*/
|
|
477
|
+
function strokeMarkupNodes(nodes, state, style, strokes) {
|
|
478
|
+
let cursorX = state.x
|
|
479
|
+
|
|
480
|
+
for (const node of nodes) {
|
|
481
|
+
const nextState = { ...state, x: cursorX }
|
|
482
|
+
cursorX = strokeMarkupNode(node, nextState, style, strokes)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return cursorX
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Converts one markup node into stroke paths.
|
|
490
|
+
* @param {object} node Markup node.
|
|
491
|
+
* @param {object} state Text run state.
|
|
492
|
+
* @param {number} style Text style flags.
|
|
493
|
+
* @param {Array[] | undefined} strokes Stroke output.
|
|
494
|
+
* @returns {number}
|
|
495
|
+
*/
|
|
496
|
+
function strokeMarkupNode(node, state, style, strokes) {
|
|
497
|
+
if (node.type === 'text') {
|
|
498
|
+
return strokeTextRun(node.value, state, style, strokes)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
let nodeStyle = style
|
|
502
|
+
if (node.type === 'subscript') nodeStyle |= textStyleSubscript
|
|
503
|
+
if (node.type === 'superscript') nodeStyle |= textStyleSuperscript
|
|
504
|
+
|
|
505
|
+
const startX = state.x
|
|
506
|
+
const endX = strokeMarkupNodes(
|
|
507
|
+
node.children || [],
|
|
508
|
+
state,
|
|
509
|
+
nodeStyle,
|
|
510
|
+
strokes
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
if (node.type === 'overbar' && strokes) {
|
|
514
|
+
strokes.push(overbarStroke(startX, endX, state))
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return endX
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Converts one plain-text run into stroke paths.
|
|
522
|
+
* @param {string} value Text run.
|
|
523
|
+
* @param {object} state Text run state.
|
|
524
|
+
* @param {number} style Text style flags.
|
|
525
|
+
* @param {Array[] | undefined} strokes Stroke output.
|
|
526
|
+
* @returns {number}
|
|
527
|
+
*/
|
|
528
|
+
function strokeTextRun(value, state, style, strokes) {
|
|
529
|
+
const metrics = styledRunMetrics(state, style)
|
|
530
|
+
let cursorX = state.x
|
|
531
|
+
let charCount = 0
|
|
532
|
+
|
|
533
|
+
for (const char of String(value || '')) {
|
|
534
|
+
if (char === '\t') {
|
|
535
|
+
charCount = Math.floor(charCount / tabWidth + 1) * tabWidth - 1
|
|
536
|
+
let newCursor =
|
|
537
|
+
state.x + metrics.sizeX * charCount + metrics.sizeX * spaceWidth
|
|
538
|
+
|
|
539
|
+
while (newCursor <= cursorX) {
|
|
540
|
+
charCount += tabWidth
|
|
541
|
+
newCursor += metrics.sizeX * tabWidth
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
cursorX = newCursor
|
|
545
|
+
} else if (char === ' ') {
|
|
546
|
+
cursorX += metrics.sizeX * spaceWidth
|
|
547
|
+
} else {
|
|
548
|
+
const glyph = glyphForCharacter(char)
|
|
549
|
+
if (strokes) {
|
|
550
|
+
glyph.strokes.forEach((stroke) => {
|
|
551
|
+
strokes.push(
|
|
552
|
+
stroke.map((point) => ({
|
|
553
|
+
x: cursorX + point.x * metrics.sizeX,
|
|
554
|
+
y: metrics.y + point.y * metrics.sizeY
|
|
555
|
+
}))
|
|
556
|
+
)
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
cursorX += glyph.bounds.maxX * metrics.sizeX
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
charCount += 1
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return cursorX
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Resolves glyph size and baseline for KiCad subscript/superscript flags.
|
|
570
|
+
* @param {object} state Text run state.
|
|
571
|
+
* @param {number} style Text style flags.
|
|
572
|
+
* @returns {{ sizeX: number, sizeY: number, y: number }}
|
|
573
|
+
*/
|
|
574
|
+
function styledRunMetrics(state, style) {
|
|
575
|
+
let sizeX = state.sizeX
|
|
576
|
+
let sizeY = state.sizeY
|
|
577
|
+
let y = state.y
|
|
578
|
+
|
|
579
|
+
if (style & (textStyleSubscript | textStyleSuperscript)) {
|
|
580
|
+
sizeX *= superSubSizeMultiplier
|
|
581
|
+
sizeY *= superSubSizeMultiplier
|
|
582
|
+
|
|
583
|
+
if (style & textStyleSubscript) {
|
|
584
|
+
y += sizeY * subscriptHeightOffset
|
|
585
|
+
} else {
|
|
586
|
+
y -= sizeY * superscriptHeightOffset
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return { sizeX, sizeY, y }
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Builds KiCad's overbar stroke for a marked text range.
|
|
595
|
+
* @param {number} startX Range start x.
|
|
596
|
+
* @param {number} endX Range end x.
|
|
597
|
+
* @param {object} state Text run state.
|
|
598
|
+
* @returns {{ x: number, y: number }[]}
|
|
599
|
+
*/
|
|
600
|
+
function overbarStroke(startX, endX, state) {
|
|
601
|
+
const trim = state.sizeX * overbarTrimRatio
|
|
602
|
+
const y = state.y - state.sizeY * overbarHeightRatio
|
|
603
|
+
return [
|
|
604
|
+
{ x: startX + trim, y },
|
|
605
|
+
{ x: endX - trim, y }
|
|
606
|
+
]
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Parses one Hershey-style glyph entry.
|
|
611
|
+
* @param {string} data Glyph source.
|
|
612
|
+
* @returns {object}
|
|
613
|
+
*/
|
|
614
|
+
function parseGlyph(data) {
|
|
615
|
+
const glyphStartX = coordinateValue(data[0]) * strokeFontScale
|
|
616
|
+
const glyphEndX = coordinateValue(data[1]) * strokeFontScale
|
|
617
|
+
const strokes = []
|
|
618
|
+
let stroke = []
|
|
619
|
+
|
|
620
|
+
for (let index = 2; index < data.length; index += 2) {
|
|
621
|
+
const xValue = data[index]
|
|
622
|
+
const yValue = data[index + 1]
|
|
623
|
+
|
|
624
|
+
if (xValue === ' ' && yValue === 'R') {
|
|
625
|
+
stroke = []
|
|
626
|
+
continue
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (stroke.length === 0) {
|
|
630
|
+
strokes.push(stroke)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
stroke.push({
|
|
634
|
+
x: coordinateValue(xValue) * strokeFontScale - glyphStartX,
|
|
635
|
+
y: (coordinateValue(yValue) + fontOffset) * strokeFontScale
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
strokes,
|
|
641
|
+
bounds: glyphBounds(strokes, glyphEndX - glyphStartX)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Calculates glyph bounds.
|
|
647
|
+
* @param {Array[]} strokes Glyph strokes.
|
|
648
|
+
* @param {number} width Advance width.
|
|
649
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number }}
|
|
650
|
+
*/
|
|
651
|
+
function glyphBounds(strokes, width) {
|
|
652
|
+
const bounds = { minX: 0, minY: 0, maxX: width, maxY: 0 }
|
|
653
|
+
|
|
654
|
+
strokes.flat().forEach((point) => {
|
|
655
|
+
bounds.minX = Math.min(bounds.minX, point.x)
|
|
656
|
+
bounds.minY = Math.min(bounds.minY, point.y)
|
|
657
|
+
bounds.maxX = Math.max(bounds.maxX, point.x)
|
|
658
|
+
bounds.maxY = Math.max(bounds.maxY, point.y)
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
return bounds
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Converts KiCad encoded glyph coordinates to numbers.
|
|
666
|
+
* @param {string} value Encoded coordinate.
|
|
667
|
+
* @returns {number}
|
|
668
|
+
*/
|
|
669
|
+
function coordinateValue(value) {
|
|
670
|
+
return value.charCodeAt(0) - 'R'.charCodeAt(0)
|
|
671
|
+
}
|