altium-toolkit 1.1.2 → 1.1.22
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/api.md +37 -0
- package/docs/model-format.md +18 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
- package/docs/testing.md +5 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/altium/AltiumLayoutParser.mjs +275 -13
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +245 -10
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AsciiRecordParser.mjs +43 -11
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PrintableTextDecoder.mjs +133 -13
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicImageParser.mjs +291 -6
- package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicPinParser.mjs +262 -24
- package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
- package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +263 -0
- package/src/ui/SchematicContentLayout.mjs +58 -1
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicImageRenderer.mjs +125 -10
- package/src/ui/SchematicJunctionRenderer.mjs +1 -1
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
- package/src/ui/SchematicNoteRenderer.mjs +87 -7
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
- package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
- package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
- package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +109 -24
- package/src/ui/SchematicSvgRenderer.mjs +1210 -71
|
@@ -12,12 +12,15 @@ import { SchematicDirectiveRenderer } from './SchematicDirectiveRenderer.mjs'
|
|
|
12
12
|
import { SchematicShapeRenderer } from './SchematicShapeRenderer.mjs'
|
|
13
13
|
import { SchematicPinSvgRenderer } from './SchematicPinSvgRenderer.mjs'
|
|
14
14
|
import { SchematicColorResolver } from './SchematicColorResolver.mjs'
|
|
15
|
+
import { SchematicLineColorResolver } from './SchematicLineColorResolver.mjs'
|
|
15
16
|
import { SchematicSheetChromeRenderer } from './SchematicSheetChromeRenderer.mjs'
|
|
16
17
|
import { SchematicContentLayout } from './SchematicContentLayout.mjs'
|
|
17
18
|
import { SchematicOwnerPinLabelLayout } from './SchematicOwnerPinLabelLayout.mjs'
|
|
18
19
|
import { SchematicRegionRenderer } from './SchematicRegionRenderer.mjs'
|
|
19
20
|
import { SchematicSheetSymbolRenderer } from './SchematicSheetSymbolRenderer.mjs'
|
|
20
21
|
import { SchematicImageRenderer } from './SchematicImageRenderer.mjs'
|
|
22
|
+
import { SchematicNativeFooterPartitioner } from './SchematicNativeFooterPartitioner.mjs'
|
|
23
|
+
import { SchematicOwnerPinMarkerLineThemer } from './SchematicOwnerPinMarkerLineThemer.mjs'
|
|
21
24
|
import { TextGeometrySidecarBuilder } from './TextGeometrySidecarBuilder.mjs'
|
|
22
25
|
import { SchematicRenderOpsSidecarBuilder } from './SchematicRenderOpsSidecarBuilder.mjs'
|
|
23
26
|
import { SchematicProjectParameterResolver } from '../core/altium/SchematicProjectParameterResolver.mjs'
|
|
@@ -28,6 +31,11 @@ const SECTION_HEADING_MIN_FONT_SIZE = 18
|
|
|
28
31
|
const SECTION_HEADING_BASELINE_LIFT_RATIO = 0.36
|
|
29
32
|
const SECTION_HEADING_LINE_Y_TOLERANCE = 0.75
|
|
30
33
|
const SECTION_HEADING_LINE_X_PADDING = 15
|
|
34
|
+
const BODY_TEXT_ASCENT_RATIO = 0.72
|
|
35
|
+
const BODY_TEXT_DESCENT_RATIO = 0.14
|
|
36
|
+
const BODY_TEXT_FLAT_DESCENT_RATIO = 0
|
|
37
|
+
const BODY_TEXT_MIN_SEPARATOR_SPAN_RATIO = 0.3
|
|
38
|
+
const BODY_TEXT_DESCENDER_PATTERN = /[gjpqyQ_,;]/
|
|
31
39
|
|
|
32
40
|
/**
|
|
33
41
|
* Renders normalized schematic models into presentational SVG.
|
|
@@ -38,7 +46,7 @@ export class SchematicSvgRenderer {
|
|
|
38
46
|
/**
|
|
39
47
|
* Renders a normalized schematic model into SVG markup.
|
|
40
48
|
* @param {{ fileName?: string, summary: { title?: string }, schematic?: { sheet: { width: number, height: number, sourceWidth?: number, sourceHeight?: number, paperSize?: string, borderOn?: boolean, titleBlockOn?: boolean, marginWidth?: number, xZones?: number, yZones?: number, titleBlock?: { title?: string, revision?: string, documentNumber?: string, sheetNumber?: string, sheetTotal?: string, date?: string, drawnBy?: string } }, lines: { x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle?: number, isBus?: boolean, ownerIndex?: string, renderOrder?: number, recordType?: string }[], polygons?: { points: { x: number, y: number }[], color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], rectangles?: { x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], regions?: { x: number, y: number, width: number, height: number, color: string, fill: string, renderOrder?: number }[], ellipses?: { x: number, y: number, radiusX: number, radiusY: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], arcs?: { x: number, y: number, radius: number, startAngle: number, endAngle: number, color: string, width: number, ownerIndex?: string, renderOrder?: number }[], directives?: { x: number, y: number, color: string, name: string, orientation?: number }[], texts: { x: number, y: number, text: string, color: string, recordType?: string, style?: number, fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, rotation?: number, sourceOrientation?: number, isMirrored?: boolean, anchor?: 'start' | 'middle' | 'end', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }[], components: { x: number, y: number, designator: string }[], pins?: { x: number, y: number, length: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, color: string, labelColor?: string, labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', ownerIndex?: string }[], ports?: { x: number, y: number, width: number, height: number, name: string, fill: string, color: string, direction?: 'left' | 'right' | 'up' | 'down', shape?: 'single' | 'double' | 'plain' }[], crosses?: { x: number, y: number, size: number, color: string }[] } }} documentModel
|
|
41
|
-
* @param {{ projectParameters?: Record<string, string | number | boolean | null | undefined
|
|
49
|
+
* @param {{ projectParameters?: Record<string, string | number | boolean | null | undefined>, colorizeImages?: boolean, colorize_images?: boolean }} options Render options.
|
|
42
50
|
* @returns {string}
|
|
43
51
|
*/
|
|
44
52
|
static render(documentModel, options = {}) {
|
|
@@ -66,7 +74,9 @@ export class SchematicSvgRenderer {
|
|
|
66
74
|
renderedSheet.contentSheet === schematic.sheet
|
|
67
75
|
? schematic
|
|
68
76
|
: { ...schematic, sheet: renderedSheet.contentSheet }
|
|
69
|
-
const allTexts =
|
|
77
|
+
const allTexts = SchematicSvgRenderer.#filterDrawableSchematicTexts(
|
|
78
|
+
schematic.texts || []
|
|
79
|
+
)
|
|
70
80
|
const lines = (schematic.lines || []).slice(0, 2500)
|
|
71
81
|
const polygons = (schematic.polygons || []).slice(0, 1000)
|
|
72
82
|
const rectangles = (schematic.rectangles || []).slice(0, 500)
|
|
@@ -146,25 +156,6 @@ export class SchematicSvgRenderer {
|
|
|
146
156
|
renderedSchematic,
|
|
147
157
|
contentClipId
|
|
148
158
|
)
|
|
149
|
-
const ownerlessLines = lines.filter((line) => !line.ownerIndex)
|
|
150
|
-
const ownerlessPolygons = polygons.filter(
|
|
151
|
-
(polygon) => !polygon.ownerIndex
|
|
152
|
-
)
|
|
153
|
-
const ownerlessRectangles = rectangles.filter(
|
|
154
|
-
(rectangle) => !rectangle.ownerIndex
|
|
155
|
-
)
|
|
156
|
-
const ownerlessRoundedRectangles = roundedRectangles.filter(
|
|
157
|
-
(rectangle) => !rectangle.ownerIndex
|
|
158
|
-
)
|
|
159
|
-
const ownerlessEllipses = ellipses.filter(
|
|
160
|
-
(ellipse) => !ellipse.ownerIndex
|
|
161
|
-
)
|
|
162
|
-
const ownerlessArcs = arcs.filter((arc) => !arc.ownerIndex)
|
|
163
|
-
const ownerlessBeziers = beziers.filter((bezier) => !bezier.ownerIndex)
|
|
164
|
-
const ownerlessPies = pies.filter((pie) => !pie.ownerIndex)
|
|
165
|
-
const ownerlessIeeeSymbols = ieeeSymbols.filter(
|
|
166
|
-
(symbol) => !symbol.ownerIndex
|
|
167
|
-
)
|
|
168
159
|
const resolvedTexts = texts.map((text) =>
|
|
169
160
|
text.recordType === '17'
|
|
170
161
|
? {
|
|
@@ -178,6 +169,77 @@ export class SchematicSvgRenderer {
|
|
|
178
169
|
}
|
|
179
170
|
: text
|
|
180
171
|
)
|
|
172
|
+
const partitionedPrimitives =
|
|
173
|
+
SchematicNativeFooterPartitioner.partitionPrimitives(
|
|
174
|
+
{
|
|
175
|
+
lines,
|
|
176
|
+
polygons,
|
|
177
|
+
rectangles,
|
|
178
|
+
roundedRectangles,
|
|
179
|
+
ellipses,
|
|
180
|
+
arcs,
|
|
181
|
+
beziers,
|
|
182
|
+
pies,
|
|
183
|
+
ieeeSymbols,
|
|
184
|
+
texts: resolvedTexts,
|
|
185
|
+
images
|
|
186
|
+
},
|
|
187
|
+
renderedSchematic.sheet
|
|
188
|
+
)
|
|
189
|
+
const contentMarkerThemedLines =
|
|
190
|
+
SchematicOwnerPinMarkerLineThemer.theme(
|
|
191
|
+
partitionedPrimitives.content.lines,
|
|
192
|
+
pins
|
|
193
|
+
)
|
|
194
|
+
const contentLines = SchematicSvgRenderer.#themeFooterChromeLines(
|
|
195
|
+
contentMarkerThemedLines,
|
|
196
|
+
renderedSheet.sheet,
|
|
197
|
+
contentHeight
|
|
198
|
+
)
|
|
199
|
+
const contentPolygons = partitionedPrimitives.content.polygons
|
|
200
|
+
const contentRectangles = partitionedPrimitives.content.rectangles
|
|
201
|
+
const contentRoundedRectangles =
|
|
202
|
+
partitionedPrimitives.content.roundedRectangles
|
|
203
|
+
const contentEllipses = partitionedPrimitives.content.ellipses
|
|
204
|
+
const contentArcs = partitionedPrimitives.content.arcs
|
|
205
|
+
const contentBeziers = partitionedPrimitives.content.beziers
|
|
206
|
+
const contentPies = partitionedPrimitives.content.pies
|
|
207
|
+
const contentIeeeSymbols = partitionedPrimitives.content.ieeeSymbols
|
|
208
|
+
const contentTexts = partitionedPrimitives.content.texts
|
|
209
|
+
const contentImages = partitionedPrimitives.content.images
|
|
210
|
+
const footerPrimitives = partitionedPrimitives.footer
|
|
211
|
+
const foregroundOwnerlessLines =
|
|
212
|
+
SchematicSvgRenderer.#collectForegroundOwnerlessLines(
|
|
213
|
+
contentLines,
|
|
214
|
+
contentPolygons,
|
|
215
|
+
contentRectangles,
|
|
216
|
+
contentRoundedRectangles,
|
|
217
|
+
contentEllipses,
|
|
218
|
+
contentPies
|
|
219
|
+
)
|
|
220
|
+
const ownerlessLines = contentLines.filter(
|
|
221
|
+
(line) => !line.ownerIndex && !foregroundOwnerlessLines.has(line)
|
|
222
|
+
)
|
|
223
|
+
const ownerlessPolygons = contentPolygons.filter(
|
|
224
|
+
(polygon) => !polygon.ownerIndex
|
|
225
|
+
)
|
|
226
|
+
const ownerlessRectangles = contentRectangles.filter(
|
|
227
|
+
(rectangle) => !rectangle.ownerIndex
|
|
228
|
+
)
|
|
229
|
+
const ownerlessRoundedRectangles = contentRoundedRectangles.filter(
|
|
230
|
+
(rectangle) => !rectangle.ownerIndex
|
|
231
|
+
)
|
|
232
|
+
const ownerlessEllipses = contentEllipses.filter(
|
|
233
|
+
(ellipse) => !ellipse.ownerIndex
|
|
234
|
+
)
|
|
235
|
+
const ownerlessArcs = contentArcs.filter((arc) => !arc.ownerIndex)
|
|
236
|
+
const ownerlessBeziers = contentBeziers.filter(
|
|
237
|
+
(bezier) => !bezier.ownerIndex
|
|
238
|
+
)
|
|
239
|
+
const ownerlessPies = contentPies.filter((pie) => !pie.ownerIndex)
|
|
240
|
+
const ownerlessIeeeSymbols = contentIeeeSymbols.filter(
|
|
241
|
+
(symbol) => !symbol.ownerIndex
|
|
242
|
+
)
|
|
181
243
|
const polygonMarkup = ownerlessPolygons
|
|
182
244
|
.map((polygon, index) =>
|
|
183
245
|
SchematicSvgRenderer.#appendSvgAttributes(
|
|
@@ -357,18 +419,33 @@ export class SchematicSvgRenderer {
|
|
|
357
419
|
.join('')
|
|
358
420
|
const ownerGeometryMarkup =
|
|
359
421
|
SchematicSvgRenderer.#buildOwnerGeometryMarkup(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
422
|
+
contentLines,
|
|
423
|
+
contentPolygons,
|
|
424
|
+
contentRectangles,
|
|
425
|
+
contentRoundedRectangles,
|
|
426
|
+
contentEllipses,
|
|
427
|
+
contentArcs,
|
|
428
|
+
contentBeziers,
|
|
429
|
+
contentPies,
|
|
430
|
+
contentIeeeSymbols,
|
|
369
431
|
contentHeight,
|
|
370
432
|
semanticContext
|
|
371
433
|
)
|
|
434
|
+
const foregroundLineMarkup = [...foregroundOwnerlessLines]
|
|
435
|
+
.map((line, index) =>
|
|
436
|
+
SchematicSvgRenderer.#buildSchematicLineMarkup(
|
|
437
|
+
line,
|
|
438
|
+
contentHeight,
|
|
439
|
+
SchematicSvgRenderer.#primitiveIndex(
|
|
440
|
+
semanticContext,
|
|
441
|
+
'lines',
|
|
442
|
+
line,
|
|
443
|
+
index
|
|
444
|
+
),
|
|
445
|
+
semanticContext
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
.join('')
|
|
372
449
|
const sheetSymbolMarkup =
|
|
373
450
|
SchematicSheetSymbolRenderer.buildSheetSymbolMarkup(
|
|
374
451
|
sheetSymbols,
|
|
@@ -390,7 +467,7 @@ export class SchematicSvgRenderer {
|
|
|
390
467
|
const resolvedAuthoredJunctions =
|
|
391
468
|
SchematicSvgRenderer.#resolveAuthoredSchematicJunctions(
|
|
392
469
|
authoredJunctions,
|
|
393
|
-
|
|
470
|
+
contentLines
|
|
394
471
|
)
|
|
395
472
|
const authoredJunctionMarkup = resolvedAuthoredJunctions
|
|
396
473
|
.map((junction) =>
|
|
@@ -401,18 +478,85 @@ export class SchematicSvgRenderer {
|
|
|
401
478
|
)
|
|
402
479
|
.join('')
|
|
403
480
|
const imageMarkup = SchematicImageRenderer.buildMarkup(
|
|
404
|
-
|
|
405
|
-
contentHeight
|
|
481
|
+
contentImages,
|
|
482
|
+
contentHeight,
|
|
483
|
+
{
|
|
484
|
+
colorizeImages: renderOptions.colorizeImages,
|
|
485
|
+
title:
|
|
486
|
+
renderModel?.summary?.title ||
|
|
487
|
+
renderedSchematic?.sheet?.titleBlock?.title ||
|
|
488
|
+
renderModel?.fileName ||
|
|
489
|
+
''
|
|
490
|
+
}
|
|
406
491
|
)
|
|
407
492
|
const markerDefsMarkup =
|
|
408
|
-
SchematicSvgRenderer.#buildSchematicLineMarkerDefs(
|
|
493
|
+
SchematicSvgRenderer.#buildSchematicLineMarkerDefs(contentLines)
|
|
494
|
+
const ownerTextBodyBounds =
|
|
495
|
+
SchematicSvgRenderer.#collectOwnerTextBodyBounds(
|
|
496
|
+
contentLines,
|
|
497
|
+
contentPolygons,
|
|
498
|
+
contentRectangles,
|
|
499
|
+
contentRoundedRectangles,
|
|
500
|
+
contentEllipses,
|
|
501
|
+
contentArcs,
|
|
502
|
+
contentPies
|
|
503
|
+
)
|
|
409
504
|
|
|
410
|
-
const textMarkup =
|
|
505
|
+
const textMarkup = contentTexts
|
|
411
506
|
.map((text, index) =>
|
|
412
507
|
SchematicSvgRenderer.#buildSchematicTextMarkup(
|
|
413
508
|
text,
|
|
414
509
|
contentHeight,
|
|
415
|
-
|
|
510
|
+
contentLines,
|
|
511
|
+
pins,
|
|
512
|
+
SchematicSvgRenderer.#primitiveIndex(
|
|
513
|
+
semanticContext,
|
|
514
|
+
'texts',
|
|
515
|
+
text,
|
|
516
|
+
index
|
|
517
|
+
),
|
|
518
|
+
semanticContext,
|
|
519
|
+
ownerTextBodyBounds
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
.join('')
|
|
523
|
+
const nativeFooterLines = SchematicSvgRenderer.#themeFooterChromeLines(
|
|
524
|
+
footerPrimitives.lines,
|
|
525
|
+
renderedSheet.sheet,
|
|
526
|
+
contentHeight
|
|
527
|
+
)
|
|
528
|
+
const nativeFooterOwnerGeometryMarkup =
|
|
529
|
+
SchematicSvgRenderer.#buildOwnerGeometryMarkup(
|
|
530
|
+
nativeFooterLines,
|
|
531
|
+
footerPrimitives.polygons,
|
|
532
|
+
footerPrimitives.rectangles,
|
|
533
|
+
footerPrimitives.roundedRectangles,
|
|
534
|
+
footerPrimitives.ellipses,
|
|
535
|
+
footerPrimitives.arcs,
|
|
536
|
+
footerPrimitives.beziers,
|
|
537
|
+
footerPrimitives.pies,
|
|
538
|
+
footerPrimitives.ieeeSymbols,
|
|
539
|
+
contentHeight,
|
|
540
|
+
semanticContext
|
|
541
|
+
)
|
|
542
|
+
const nativeFooterImageMarkup = SchematicImageRenderer.buildMarkup(
|
|
543
|
+
footerPrimitives.images,
|
|
544
|
+
contentHeight,
|
|
545
|
+
{
|
|
546
|
+
colorizeImages: renderOptions.colorizeImages,
|
|
547
|
+
title:
|
|
548
|
+
renderModel?.summary?.title ||
|
|
549
|
+
renderedSchematic?.sheet?.titleBlock?.title ||
|
|
550
|
+
renderModel?.fileName ||
|
|
551
|
+
''
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
const nativeFooterTextMarkup = footerPrimitives.texts
|
|
555
|
+
.map((text, index) =>
|
|
556
|
+
SchematicSvgRenderer.#buildSchematicTextMarkup(
|
|
557
|
+
text,
|
|
558
|
+
contentHeight,
|
|
559
|
+
contentLines,
|
|
416
560
|
pins,
|
|
417
561
|
SchematicSvgRenderer.#primitiveIndex(
|
|
418
562
|
semanticContext,
|
|
@@ -424,6 +568,36 @@ export class SchematicSvgRenderer {
|
|
|
424
568
|
)
|
|
425
569
|
)
|
|
426
570
|
.join('')
|
|
571
|
+
const nativeFooterMargin = Math.max(
|
|
572
|
+
Number(renderedSchematic?.sheet?.marginWidth || 20),
|
|
573
|
+
10
|
|
574
|
+
)
|
|
575
|
+
const nativeFooterOffsetX = partitionedPrimitives.footerBounds
|
|
576
|
+
? Math.max(
|
|
577
|
+
width -
|
|
578
|
+
nativeFooterMargin -
|
|
579
|
+
partitionedPrimitives.footerBounds.maxX,
|
|
580
|
+
0
|
|
581
|
+
)
|
|
582
|
+
: 0
|
|
583
|
+
const nativeFooterTransform =
|
|
584
|
+
nativeFooterOffsetX > 0.01
|
|
585
|
+
? ' transform="translate(' +
|
|
586
|
+
formatNumber(nativeFooterOffsetX) +
|
|
587
|
+
' 0)"'
|
|
588
|
+
: ''
|
|
589
|
+
const nativeFooterMarkup =
|
|
590
|
+
nativeFooterOwnerGeometryMarkup ||
|
|
591
|
+
nativeFooterImageMarkup ||
|
|
592
|
+
nativeFooterTextMarkup
|
|
593
|
+
? '<g class="schematic-native-footer" stroke-linecap="round"' +
|
|
594
|
+
nativeFooterTransform +
|
|
595
|
+
'>' +
|
|
596
|
+
nativeFooterOwnerGeometryMarkup +
|
|
597
|
+
nativeFooterImageMarkup +
|
|
598
|
+
nativeFooterTextMarkup +
|
|
599
|
+
'</g>'
|
|
600
|
+
: ''
|
|
427
601
|
|
|
428
602
|
const componentMarkup = drawableComponents
|
|
429
603
|
.map((component, index) =>
|
|
@@ -451,6 +625,21 @@ export class SchematicSvgRenderer {
|
|
|
451
625
|
texts,
|
|
452
626
|
pins
|
|
453
627
|
)
|
|
628
|
+
const compactExternalNumberLabelSides =
|
|
629
|
+
SchematicOwnerPinLabelLayout.collectCompactExternalNumberLabelSides(
|
|
630
|
+
pins
|
|
631
|
+
)
|
|
632
|
+
const internalNumberLabelBoxes =
|
|
633
|
+
SchematicOwnerPinLabelLayout.collectInternalNumberLabelBoxes(
|
|
634
|
+
pins,
|
|
635
|
+
contentRectangles
|
|
636
|
+
)
|
|
637
|
+
const overlappingExternalNumberLabelKeys =
|
|
638
|
+
SchematicOwnerPinLabelLayout.collectOverlappingExternalNumberLabelKeys(
|
|
639
|
+
pins,
|
|
640
|
+
contentTexts,
|
|
641
|
+
internalNumberLabelBoxes
|
|
642
|
+
)
|
|
454
643
|
const pinMarkup = pins
|
|
455
644
|
.map((pin, index) =>
|
|
456
645
|
SchematicSvgRenderer.#appendSvgAttributes(
|
|
@@ -460,7 +649,10 @@ export class SchematicSvgRenderer {
|
|
|
460
649
|
renderedSheet.contentSheet,
|
|
461
650
|
rotatedVerticalNumberOwners,
|
|
462
651
|
explicitOwnerPinNameLabels,
|
|
463
|
-
explicitOwnerPinLabelOffsets
|
|
652
|
+
explicitOwnerPinLabelOffsets,
|
|
653
|
+
compactExternalNumberLabelSides,
|
|
654
|
+
internalNumberLabelBoxes,
|
|
655
|
+
overlappingExternalNumberLabelKeys
|
|
464
656
|
),
|
|
465
657
|
SchematicSvgRenderer.#semanticAttributes(
|
|
466
658
|
'pin',
|
|
@@ -504,10 +696,10 @@ export class SchematicSvgRenderer {
|
|
|
504
696
|
)
|
|
505
697
|
.join('')
|
|
506
698
|
const junctionMarkup = SchematicJunctionRenderer.buildMarkup(
|
|
507
|
-
|
|
699
|
+
contentLines,
|
|
508
700
|
crosses,
|
|
509
701
|
ports,
|
|
510
|
-
|
|
702
|
+
contentTexts.filter((text) => text.recordType === '17'),
|
|
511
703
|
contentHeight,
|
|
512
704
|
resolvedAuthoredJunctions
|
|
513
705
|
)
|
|
@@ -590,6 +782,9 @@ export class SchematicSvgRenderer {
|
|
|
590
782
|
'<g class="schematic-owner-geometry" stroke-linecap="round">' +
|
|
591
783
|
ownerGeometryMarkup +
|
|
592
784
|
'</g>' +
|
|
785
|
+
'<g class="schematic-foreground-lines" stroke-linecap="round">' +
|
|
786
|
+
foregroundLineMarkup +
|
|
787
|
+
'</g>' +
|
|
593
788
|
'<g class="schematic-sheet-symbols">' +
|
|
594
789
|
sheetSymbolMarkup +
|
|
595
790
|
'</g>' +
|
|
@@ -626,6 +821,7 @@ export class SchematicSvgRenderer {
|
|
|
626
821
|
'</g>' +
|
|
627
822
|
'</g>' +
|
|
628
823
|
frameMarkup +
|
|
824
|
+
nativeFooterMarkup +
|
|
629
825
|
'<g class="schematic-regions">' +
|
|
630
826
|
regionMarkup +
|
|
631
827
|
'</g>' +
|
|
@@ -636,7 +832,7 @@ export class SchematicSvgRenderer {
|
|
|
636
832
|
/**
|
|
637
833
|
* Normalizes schematic SVG export options.
|
|
638
834
|
* @param {Record<string, unknown>} options Raw render options.
|
|
639
|
-
* @returns {{ includeViewBox: boolean, documentId: string, documentVersion: string, includeTextGeometrySidecar: boolean, includeRenderOperationsSidecar: boolean, renderOperationProfile: string }}
|
|
835
|
+
* @returns {{ includeViewBox: boolean, documentId: string, documentVersion: string, colorizeImages: boolean, includeTextGeometrySidecar: boolean, includeRenderOperationsSidecar: boolean, renderOperationProfile: string }}
|
|
640
836
|
*/
|
|
641
837
|
static #normalizeRenderOptions(options) {
|
|
642
838
|
const includeViewBox =
|
|
@@ -648,6 +844,9 @@ export class SchematicSvgRenderer {
|
|
|
648
844
|
documentVersion: String(
|
|
649
845
|
options?.documentVersion || options?.documentVer || ''
|
|
650
846
|
),
|
|
847
|
+
colorizeImages:
|
|
848
|
+
options?.colorizeImages === true ||
|
|
849
|
+
options?.colorize_images === true,
|
|
651
850
|
includeTextGeometrySidecar:
|
|
652
851
|
options?.includeTextGeometrySidecar === true ||
|
|
653
852
|
options?.textGeometry === 'sidecar',
|
|
@@ -660,6 +859,106 @@ export class SchematicSvgRenderer {
|
|
|
660
859
|
}
|
|
661
860
|
}
|
|
662
861
|
|
|
862
|
+
/**
|
|
863
|
+
* Removes unresolved simple Altium special-string placeholders from
|
|
864
|
+
* drawable text while preserving values resolved by project parameters.
|
|
865
|
+
* @param {object[]} texts Schematic text rows.
|
|
866
|
+
* @returns {object[]}
|
|
867
|
+
*/
|
|
868
|
+
static #filterDrawableSchematicTexts(texts) {
|
|
869
|
+
return (texts || []).filter(
|
|
870
|
+
(text) =>
|
|
871
|
+
!SchematicSvgRenderer.#isUnresolvedSimpleSpecialStringText(text)
|
|
872
|
+
)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Uses sheet-frame chrome color for native footer linework so recovered
|
|
877
|
+
* template cells match the synthesized frame and zone separators.
|
|
878
|
+
* @param {object[]} lines Schematic line primitives.
|
|
879
|
+
* @param {{ width?: number, borderOn?: boolean, marginWidth?: number }} sheet Sheet metadata.
|
|
880
|
+
* @param {number} sheetHeight Rendered sheet height for projection.
|
|
881
|
+
* @returns {object[]}
|
|
882
|
+
*/
|
|
883
|
+
static #themeFooterChromeLines(lines, sheet, sheetHeight) {
|
|
884
|
+
return (lines || []).map((line) =>
|
|
885
|
+
SchematicSvgRenderer.#isFooterChromeLine(line, sheet, sheetHeight)
|
|
886
|
+
? {
|
|
887
|
+
...line,
|
|
888
|
+
color: 'var(--schematic-sheet-frame-stroke)'
|
|
889
|
+
}
|
|
890
|
+
: line
|
|
891
|
+
)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Returns true for neutral owner-drawn title-block lines in the bottom
|
|
896
|
+
* right footer band.
|
|
897
|
+
* @param {{ x1?: number, y1?: number, x2?: number, y2?: number, color?: string, ownerIndex?: string }} line Schematic line candidate.
|
|
898
|
+
* @param {{ width?: number, borderOn?: boolean, marginWidth?: number }} sheet Sheet metadata.
|
|
899
|
+
* @param {number} sheetHeight Rendered sheet height for projection.
|
|
900
|
+
* @returns {boolean}
|
|
901
|
+
*/
|
|
902
|
+
static #isFooterChromeLine(line, sheet, sheetHeight) {
|
|
903
|
+
if (!sheet?.borderOn || !String(line?.ownerIndex || '').trim()) {
|
|
904
|
+
return false
|
|
905
|
+
}
|
|
906
|
+
if (
|
|
907
|
+
SchematicColorResolver.resolveColor(
|
|
908
|
+
line.color,
|
|
909
|
+
'--schematic-default-ink-color'
|
|
910
|
+
) !== 'var(--schematic-text-color)'
|
|
911
|
+
) {
|
|
912
|
+
return false
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const width = Number(sheet?.width || 0)
|
|
916
|
+
const height = Number(sheetHeight || 0)
|
|
917
|
+
const x1 = Number(line?.x1)
|
|
918
|
+
const x2 = Number(line?.x2)
|
|
919
|
+
const y1 = Number(line?.y1)
|
|
920
|
+
const y2 = Number(line?.y2)
|
|
921
|
+
|
|
922
|
+
if (
|
|
923
|
+
!Number.isFinite(width) ||
|
|
924
|
+
!Number.isFinite(height) ||
|
|
925
|
+
!Number.isFinite(x1) ||
|
|
926
|
+
!Number.isFinite(x2) ||
|
|
927
|
+
!Number.isFinite(y1) ||
|
|
928
|
+
!Number.isFinite(y2) ||
|
|
929
|
+
width <= 0 ||
|
|
930
|
+
height <= 0
|
|
931
|
+
) {
|
|
932
|
+
return false
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const margin = Math.max(Number(sheet?.marginWidth || 20), 10)
|
|
936
|
+
const lineLength = Math.hypot(x2 - x1, y2 - y1)
|
|
937
|
+
const minChromeLineLength = Math.max(margin * 0.6, 12)
|
|
938
|
+
|
|
939
|
+
if (lineLength < minChromeLineLength) {
|
|
940
|
+
return false
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const footerBandHeight = Math.max(margin * 6, 120)
|
|
944
|
+
const footerTop = Math.max(height - footerBandHeight, margin)
|
|
945
|
+
const projectedY1 = projectSchematicY(height, y1)
|
|
946
|
+
const projectedY2 = projectSchematicY(height, y2)
|
|
947
|
+
const minProjectedY = Math.min(projectedY1, projectedY2)
|
|
948
|
+
const maxX = Math.max(x1, x2)
|
|
949
|
+
|
|
950
|
+
return minProjectedY >= footerTop && maxX >= width * 0.5
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Returns true when a text row still contains one bare special string.
|
|
955
|
+
* @param {object} text Schematic text row.
|
|
956
|
+
* @returns {boolean}
|
|
957
|
+
*/
|
|
958
|
+
static #isUnresolvedSimpleSpecialStringText(text) {
|
|
959
|
+
return /^[.=][A-Za-z_][\w.-]*$/u.test(String(text?.text || '').trim())
|
|
960
|
+
}
|
|
961
|
+
|
|
663
962
|
/**
|
|
664
963
|
* Renders root SVG viewBox attributes according to export options.
|
|
665
964
|
* @param {{ includeViewBox: boolean }} options Normalized options.
|
|
@@ -1560,6 +1859,311 @@ export class SchematicSvgRenderer {
|
|
|
1560
1859
|
return height + margin * 2
|
|
1561
1860
|
}
|
|
1562
1861
|
|
|
1862
|
+
/**
|
|
1863
|
+
* Collects ownerless line primitives that were authored after an
|
|
1864
|
+
* owner-linked fill they overlap. These are symbol decoration strokes
|
|
1865
|
+
* stored outside the owner group and must paint above the body fill.
|
|
1866
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, x1: number, y1: number, x2: number, y2: number }[]} lines Line primitives.
|
|
1867
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, points?: { x: number, y: number }[] }[]} polygons Polygon primitives.
|
|
1868
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, x: number, y: number, width: number, height: number }[]} rectangles Rectangle primitives.
|
|
1869
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, x: number, y: number, width: number, height: number }[]} roundedRectangles Rounded rectangle primitives.
|
|
1870
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, x: number, y: number, radiusX: number, radiusY: number }[]} ellipses Ellipse primitives.
|
|
1871
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, x: number, y: number, radius: number, radiusY?: number }[]} pies Pie primitives.
|
|
1872
|
+
* @returns {Set<object>}
|
|
1873
|
+
*/
|
|
1874
|
+
static #collectForegroundOwnerlessLines(
|
|
1875
|
+
lines,
|
|
1876
|
+
polygons,
|
|
1877
|
+
rectangles,
|
|
1878
|
+
roundedRectangles,
|
|
1879
|
+
ellipses,
|
|
1880
|
+
pies
|
|
1881
|
+
) {
|
|
1882
|
+
const ownerFillBounds = SchematicSvgRenderer.#collectOwnerFillBounds(
|
|
1883
|
+
polygons,
|
|
1884
|
+
rectangles,
|
|
1885
|
+
roundedRectangles,
|
|
1886
|
+
ellipses,
|
|
1887
|
+
pies
|
|
1888
|
+
)
|
|
1889
|
+
|
|
1890
|
+
if (!ownerFillBounds.length) {
|
|
1891
|
+
return new Set()
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
return new Set(
|
|
1895
|
+
(lines || []).filter((line) =>
|
|
1896
|
+
SchematicSvgRenderer.#isForegroundOwnerlessLine(
|
|
1897
|
+
line,
|
|
1898
|
+
ownerFillBounds
|
|
1899
|
+
)
|
|
1900
|
+
)
|
|
1901
|
+
)
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
/**
|
|
1905
|
+
* Collects sortable bounds for owner-linked fill primitives.
|
|
1906
|
+
* @param {object[]} polygons Polygon primitives.
|
|
1907
|
+
* @param {object[]} rectangles Rectangle primitives.
|
|
1908
|
+
* @param {object[]} roundedRectangles Rounded rectangle primitives.
|
|
1909
|
+
* @param {object[]} ellipses Ellipse primitives.
|
|
1910
|
+
* @param {object[]} pies Pie primitives.
|
|
1911
|
+
* @returns {{ bounds: { minX: number, minY: number, maxX: number, maxY: number }, renderOrder: number }[]}
|
|
1912
|
+
*/
|
|
1913
|
+
static #collectOwnerFillBounds(
|
|
1914
|
+
polygons,
|
|
1915
|
+
rectangles,
|
|
1916
|
+
roundedRectangles,
|
|
1917
|
+
ellipses,
|
|
1918
|
+
pies
|
|
1919
|
+
) {
|
|
1920
|
+
return [
|
|
1921
|
+
...(polygons || []),
|
|
1922
|
+
...(rectangles || []),
|
|
1923
|
+
...(roundedRectangles || []),
|
|
1924
|
+
...(ellipses || []),
|
|
1925
|
+
...(pies || [])
|
|
1926
|
+
]
|
|
1927
|
+
.filter((primitive) => primitive?.ownerIndex)
|
|
1928
|
+
.map((primitive) => ({
|
|
1929
|
+
bounds: SchematicSvgRenderer.#primitiveBounds(primitive),
|
|
1930
|
+
renderOrder:
|
|
1931
|
+
SchematicSvgRenderer.#resolvePrimitiveRenderOrder(primitive)
|
|
1932
|
+
}))
|
|
1933
|
+
.filter(
|
|
1934
|
+
(entry) =>
|
|
1935
|
+
entry.bounds &&
|
|
1936
|
+
Number.isFinite(entry.renderOrder) &&
|
|
1937
|
+
entry.renderOrder < Number.MAX_SAFE_INTEGER
|
|
1938
|
+
)
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
/**
|
|
1942
|
+
* Collects unprojected owner body envelopes used to identify text that is
|
|
1943
|
+
* drawn inside a component body instead of on surrounding wires or sheet
|
|
1944
|
+
* chrome.
|
|
1945
|
+
* @param {object[]} lines Line primitives.
|
|
1946
|
+
* @param {object[]} polygons Polygon primitives.
|
|
1947
|
+
* @param {object[]} rectangles Rectangle primitives.
|
|
1948
|
+
* @param {object[]} roundedRectangles Rounded rectangle primitives.
|
|
1949
|
+
* @param {object[]} ellipses Ellipse primitives.
|
|
1950
|
+
* @param {object[]} arcs Arc primitives.
|
|
1951
|
+
* @param {object[]} pies Pie primitives.
|
|
1952
|
+
* @returns {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>}
|
|
1953
|
+
*/
|
|
1954
|
+
static #collectOwnerTextBodyBounds(
|
|
1955
|
+
lines,
|
|
1956
|
+
polygons,
|
|
1957
|
+
rectangles,
|
|
1958
|
+
roundedRectangles,
|
|
1959
|
+
ellipses,
|
|
1960
|
+
arcs,
|
|
1961
|
+
pies
|
|
1962
|
+
) {
|
|
1963
|
+
const ownerBounds = new Map()
|
|
1964
|
+
|
|
1965
|
+
for (const primitive of [
|
|
1966
|
+
...(lines || []),
|
|
1967
|
+
...(polygons || []),
|
|
1968
|
+
...(rectangles || []),
|
|
1969
|
+
...(roundedRectangles || []),
|
|
1970
|
+
...(ellipses || []),
|
|
1971
|
+
...(arcs || []),
|
|
1972
|
+
...(pies || [])
|
|
1973
|
+
]) {
|
|
1974
|
+
SchematicSvgRenderer.#extendOwnerTextBodyBounds(
|
|
1975
|
+
ownerBounds,
|
|
1976
|
+
primitive
|
|
1977
|
+
)
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
return ownerBounds
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/**
|
|
1984
|
+
* Extends the body-bounds map with one owner-linked primitive.
|
|
1985
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerBounds Owner bounds map.
|
|
1986
|
+
* @param {object} primitive Primitive candidate.
|
|
1987
|
+
* @returns {void}
|
|
1988
|
+
*/
|
|
1989
|
+
static #extendOwnerTextBodyBounds(ownerBounds, primitive) {
|
|
1990
|
+
const ownerIndex = String(primitive?.ownerIndex || '').trim()
|
|
1991
|
+
const bounds = SchematicSvgRenderer.#primitiveBounds(primitive)
|
|
1992
|
+
|
|
1993
|
+
if (!ownerIndex || !bounds) {
|
|
1994
|
+
return
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
const current = ownerBounds.get(ownerIndex)
|
|
1998
|
+
|
|
1999
|
+
if (!current) {
|
|
2000
|
+
ownerBounds.set(ownerIndex, { ...bounds })
|
|
2001
|
+
return
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
current.minX = Math.min(current.minX, bounds.minX)
|
|
2005
|
+
current.minY = Math.min(current.minY, bounds.minY)
|
|
2006
|
+
current.maxX = Math.max(current.maxX, bounds.maxX)
|
|
2007
|
+
current.maxY = Math.max(current.maxY, bounds.maxY)
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
/**
|
|
2011
|
+
* Returns true when one ownerless line should paint above an overlapping
|
|
2012
|
+
* owner fill because the native stream authored it later.
|
|
2013
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, x1: number, y1: number, x2: number, y2: number }} line Line primitive.
|
|
2014
|
+
* @param {{ bounds: { minX: number, minY: number, maxX: number, maxY: number }, renderOrder: number }[]} ownerFillBounds Fill bounds.
|
|
2015
|
+
* @returns {boolean}
|
|
2016
|
+
*/
|
|
2017
|
+
static #isForegroundOwnerlessLine(line, ownerFillBounds) {
|
|
2018
|
+
if (line?.ownerIndex) {
|
|
2019
|
+
return false
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
const renderOrder = Number(line?.renderOrder)
|
|
2023
|
+
if (!Number.isFinite(renderOrder)) {
|
|
2024
|
+
return false
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
const lineBounds = SchematicSvgRenderer.#primitiveBounds(line)
|
|
2028
|
+
if (!lineBounds) {
|
|
2029
|
+
return false
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
return ownerFillBounds.some(
|
|
2033
|
+
(fill) =>
|
|
2034
|
+
renderOrder > fill.renderOrder &&
|
|
2035
|
+
SchematicSvgRenderer.#boundsIntersect(
|
|
2036
|
+
lineBounds,
|
|
2037
|
+
fill.bounds,
|
|
2038
|
+
0.5
|
|
2039
|
+
)
|
|
2040
|
+
)
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* Resolves an unprojected coordinate envelope for one schematic primitive.
|
|
2045
|
+
* @param {object} primitive Primitive.
|
|
2046
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
2047
|
+
*/
|
|
2048
|
+
static #primitiveBounds(primitive) {
|
|
2049
|
+
if (Array.isArray(primitive?.points) && primitive.points.length > 0) {
|
|
2050
|
+
return SchematicSvgRenderer.#pointBounds(primitive.points)
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
if (
|
|
2054
|
+
Number.isFinite(Number(primitive?.x1)) &&
|
|
2055
|
+
Number.isFinite(Number(primitive?.y1)) &&
|
|
2056
|
+
Number.isFinite(Number(primitive?.x2)) &&
|
|
2057
|
+
Number.isFinite(Number(primitive?.y2))
|
|
2058
|
+
) {
|
|
2059
|
+
return SchematicSvgRenderer.#pointBounds([
|
|
2060
|
+
{ x: Number(primitive.x1), y: Number(primitive.y1) },
|
|
2061
|
+
{ x: Number(primitive.x2), y: Number(primitive.y2) }
|
|
2062
|
+
])
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
if (
|
|
2066
|
+
Number.isFinite(Number(primitive?.x)) &&
|
|
2067
|
+
Number.isFinite(Number(primitive?.y)) &&
|
|
2068
|
+
Number.isFinite(Number(primitive?.width)) &&
|
|
2069
|
+
Number.isFinite(Number(primitive?.height))
|
|
2070
|
+
) {
|
|
2071
|
+
const x = Number(primitive.x)
|
|
2072
|
+
const y = Number(primitive.y)
|
|
2073
|
+
const x2 = x + Number(primitive.width)
|
|
2074
|
+
const y2 = y + Number(primitive.height)
|
|
2075
|
+
|
|
2076
|
+
return {
|
|
2077
|
+
minX: Math.min(x, x2),
|
|
2078
|
+
minY: Math.min(y, y2),
|
|
2079
|
+
maxX: Math.max(x, x2),
|
|
2080
|
+
maxY: Math.max(y, y2)
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
if (
|
|
2085
|
+
Number.isFinite(Number(primitive?.x)) &&
|
|
2086
|
+
Number.isFinite(Number(primitive?.y)) &&
|
|
2087
|
+
Number.isFinite(Number(primitive?.radiusX)) &&
|
|
2088
|
+
Number.isFinite(Number(primitive?.radiusY))
|
|
2089
|
+
) {
|
|
2090
|
+
const x = Number(primitive.x)
|
|
2091
|
+
const y = Number(primitive.y)
|
|
2092
|
+
const radiusX = Math.abs(Number(primitive.radiusX))
|
|
2093
|
+
const radiusY = Math.abs(Number(primitive.radiusY))
|
|
2094
|
+
|
|
2095
|
+
return {
|
|
2096
|
+
minX: x - radiusX,
|
|
2097
|
+
minY: y - radiusY,
|
|
2098
|
+
maxX: x + radiusX,
|
|
2099
|
+
maxY: y + radiusY
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
if (
|
|
2104
|
+
Number.isFinite(Number(primitive?.x)) &&
|
|
2105
|
+
Number.isFinite(Number(primitive?.y)) &&
|
|
2106
|
+
Number.isFinite(Number(primitive?.radius))
|
|
2107
|
+
) {
|
|
2108
|
+
const x = Number(primitive.x)
|
|
2109
|
+
const y = Number(primitive.y)
|
|
2110
|
+
const radiusX = Math.abs(Number(primitive.radius))
|
|
2111
|
+
const radiusY = Math.abs(Number(primitive.radiusY ?? radiusX))
|
|
2112
|
+
|
|
2113
|
+
return {
|
|
2114
|
+
minX: x - radiusX,
|
|
2115
|
+
minY: y - radiusY,
|
|
2116
|
+
maxX: x + radiusX,
|
|
2117
|
+
maxY: y + radiusY
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
return null
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
/**
|
|
2125
|
+
* Resolves bounds for a point list.
|
|
2126
|
+
* @param {{ x: number, y: number }[]} points Points.
|
|
2127
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
2128
|
+
*/
|
|
2129
|
+
static #pointBounds(points) {
|
|
2130
|
+
const numericPoints = (points || [])
|
|
2131
|
+
.map((point) => ({
|
|
2132
|
+
x: Number(point?.x),
|
|
2133
|
+
y: Number(point?.y)
|
|
2134
|
+
}))
|
|
2135
|
+
.filter(
|
|
2136
|
+
(point) => Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
2137
|
+
)
|
|
2138
|
+
|
|
2139
|
+
if (!numericPoints.length) {
|
|
2140
|
+
return null
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
return {
|
|
2144
|
+
minX: Math.min(...numericPoints.map((point) => point.x)),
|
|
2145
|
+
minY: Math.min(...numericPoints.map((point) => point.y)),
|
|
2146
|
+
maxX: Math.max(...numericPoints.map((point) => point.x)),
|
|
2147
|
+
maxY: Math.max(...numericPoints.map((point) => point.y))
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
/**
|
|
2152
|
+
* Returns true when two unprojected coordinate bounds overlap.
|
|
2153
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} left Left bounds.
|
|
2154
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} right Right bounds.
|
|
2155
|
+
* @param {number} tolerance Coordinate tolerance.
|
|
2156
|
+
* @returns {boolean}
|
|
2157
|
+
*/
|
|
2158
|
+
static #boundsIntersect(left, right, tolerance = 0) {
|
|
2159
|
+
return (
|
|
2160
|
+
left.maxX >= right.minX - tolerance &&
|
|
2161
|
+
left.minX <= right.maxX + tolerance &&
|
|
2162
|
+
left.maxY >= right.minY - tolerance &&
|
|
2163
|
+
left.minY <= right.maxY + tolerance
|
|
2164
|
+
)
|
|
2165
|
+
}
|
|
2166
|
+
|
|
1563
2167
|
/**
|
|
1564
2168
|
* Builds interleaved owner geometry so symbol-internal primitives preserve
|
|
1565
2169
|
* their recovered Altium paint order instead of batching fills ahead of all
|
|
@@ -1900,12 +2504,7 @@ export class SchematicSvgRenderer {
|
|
|
1900
2504
|
'" y2="' +
|
|
1901
2505
|
formatNumber(projectSchematicY(sheetHeight, line.y2)) +
|
|
1902
2506
|
'" stroke="' +
|
|
1903
|
-
escapeHtml(
|
|
1904
|
-
SchematicColorResolver.resolveColor(
|
|
1905
|
-
line.color,
|
|
1906
|
-
'--schematic-default-ink-color'
|
|
1907
|
-
)
|
|
1908
|
-
) +
|
|
2507
|
+
escapeHtml(SchematicLineColorResolver.resolveColor(line)) +
|
|
1909
2508
|
'" stroke-width="' +
|
|
1910
2509
|
formatNumber(
|
|
1911
2510
|
SchematicSvgRenderer.#resolveSchematicLineWidth(line)
|
|
@@ -2015,6 +2614,7 @@ export class SchematicSvgRenderer {
|
|
|
2015
2614
|
* @param {{ x: number, y: number, length: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
2016
2615
|
* @param {number} index Stable primitive index.
|
|
2017
2616
|
* @param {object | undefined} semanticContext Semantic lookup context.
|
|
2617
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
2018
2618
|
* @returns {string}
|
|
2019
2619
|
*/
|
|
2020
2620
|
static #buildSchematicTextMarkup(
|
|
@@ -2023,7 +2623,8 @@ export class SchematicSvgRenderer {
|
|
|
2023
2623
|
lines,
|
|
2024
2624
|
pins,
|
|
2025
2625
|
index = 0,
|
|
2026
|
-
semanticContext = undefined
|
|
2626
|
+
semanticContext = undefined,
|
|
2627
|
+
ownerTextBodyBounds = new Map()
|
|
2027
2628
|
) {
|
|
2028
2629
|
const matchedOwnerPin =
|
|
2029
2630
|
SchematicOwnerPinLabelLayout.findExplicitOwnerPinLabelMatch(
|
|
@@ -2063,7 +2664,8 @@ export class SchematicSvgRenderer {
|
|
|
2063
2664
|
text,
|
|
2064
2665
|
sheetHeight,
|
|
2065
2666
|
lines,
|
|
2066
|
-
matchedOwnerPin
|
|
2667
|
+
matchedOwnerPin,
|
|
2668
|
+
ownerTextBodyBounds
|
|
2067
2669
|
)
|
|
2068
2670
|
|
|
2069
2671
|
return SchematicSvgRenderer.#appendSvgAttributes(
|
|
@@ -2098,13 +2700,15 @@ export class SchematicSvgRenderer {
|
|
|
2098
2700
|
* @param {number} sheetHeight
|
|
2099
2701
|
* @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines
|
|
2100
2702
|
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin
|
|
2703
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
2101
2704
|
* @returns {{ x: number, y: number, anchor: 'start' | 'middle' | 'end' }}
|
|
2102
2705
|
*/
|
|
2103
2706
|
static #resolveSchematicTextPlacement(
|
|
2104
2707
|
text,
|
|
2105
2708
|
sheetHeight,
|
|
2106
2709
|
lines,
|
|
2107
|
-
matchedOwnerPin
|
|
2710
|
+
matchedOwnerPin,
|
|
2711
|
+
ownerTextBodyBounds
|
|
2108
2712
|
) {
|
|
2109
2713
|
const mirroredOwnerPinPlacement =
|
|
2110
2714
|
SchematicOwnerPinLabelLayout.resolveMirroredOwnerPinLabelPlacement(
|
|
@@ -2112,6 +2716,7 @@ export class SchematicSvgRenderer {
|
|
|
2112
2716
|
matchedOwnerPin
|
|
2113
2717
|
)
|
|
2114
2718
|
const sourceY = mirroredOwnerPinPlacement?.y ?? text.y
|
|
2719
|
+
const x = mirroredOwnerPinPlacement?.x ?? text.x
|
|
2115
2720
|
const projectedY = projectSchematicY(sheetHeight, sourceY)
|
|
2116
2721
|
const fontSize =
|
|
2117
2722
|
SchematicTypography.resolveViewerFontSize(text.fontSize) || 0
|
|
@@ -2123,14 +2728,500 @@ export class SchematicSvgRenderer {
|
|
|
2123
2728
|
fontSize,
|
|
2124
2729
|
matchedOwnerPin
|
|
2125
2730
|
)
|
|
2731
|
+
const ownerBodyBaselineOffset =
|
|
2732
|
+
SchematicSvgRenderer.#resolveOwnerBodyTextBaselineOffset(
|
|
2733
|
+
text,
|
|
2734
|
+
x,
|
|
2735
|
+
sourceY,
|
|
2736
|
+
fontSize,
|
|
2737
|
+
matchedOwnerPin,
|
|
2738
|
+
ownerTextBodyBounds
|
|
2739
|
+
)
|
|
2740
|
+
|
|
2741
|
+
const unclampedY = projectedY - baselineLift + ownerBodyBaselineOffset
|
|
2126
2742
|
|
|
2127
2743
|
return {
|
|
2128
|
-
x
|
|
2129
|
-
y:
|
|
2744
|
+
x,
|
|
2745
|
+
y: SchematicSvgRenderer.#clampOwnerBodyTextBaseline(
|
|
2746
|
+
text,
|
|
2747
|
+
x,
|
|
2748
|
+
sourceY,
|
|
2749
|
+
unclampedY,
|
|
2750
|
+
sheetHeight,
|
|
2751
|
+
fontSize,
|
|
2752
|
+
matchedOwnerPin,
|
|
2753
|
+
ownerTextBodyBounds,
|
|
2754
|
+
lines
|
|
2755
|
+
),
|
|
2130
2756
|
anchor: text.anchor || 'start'
|
|
2131
2757
|
}
|
|
2132
2758
|
}
|
|
2133
2759
|
|
|
2760
|
+
/**
|
|
2761
|
+
* Keeps free schematic body annotations inside the owner body cell they
|
|
2762
|
+
* were authored over, including ownerless overlay text used by some
|
|
2763
|
+
* symbols for truth-table and switch-state labels.
|
|
2764
|
+
* @param {{ x: number, y: number, text: string, ownerIndex?: string, recordType?: string, rotation?: number }} text Text primitive.
|
|
2765
|
+
* @param {number} x Render X coordinate.
|
|
2766
|
+
* @param {number} sourceY Unprojected source Y coordinate.
|
|
2767
|
+
* @param {number} projectedY Candidate SVG baseline.
|
|
2768
|
+
* @param {number} sheetHeight Rendered sheet height.
|
|
2769
|
+
* @param {number} fontSize Viewer font size.
|
|
2770
|
+
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
|
|
2771
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
2772
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines Line primitives.
|
|
2773
|
+
* @returns {number}
|
|
2774
|
+
*/
|
|
2775
|
+
static #clampOwnerBodyTextBaseline(
|
|
2776
|
+
text,
|
|
2777
|
+
x,
|
|
2778
|
+
sourceY,
|
|
2779
|
+
projectedY,
|
|
2780
|
+
sheetHeight,
|
|
2781
|
+
fontSize,
|
|
2782
|
+
matchedOwnerPin,
|
|
2783
|
+
ownerTextBodyBounds,
|
|
2784
|
+
lines
|
|
2785
|
+
) {
|
|
2786
|
+
const bounds = SchematicSvgRenderer.#resolveOwnerBodyTextCellBounds(
|
|
2787
|
+
text,
|
|
2788
|
+
x,
|
|
2789
|
+
sourceY,
|
|
2790
|
+
fontSize,
|
|
2791
|
+
matchedOwnerPin,
|
|
2792
|
+
ownerTextBodyBounds,
|
|
2793
|
+
lines
|
|
2794
|
+
)
|
|
2795
|
+
|
|
2796
|
+
if (!bounds) {
|
|
2797
|
+
return projectedY
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
return SchematicSvgRenderer.#clampTextBaselineToBounds(
|
|
2801
|
+
text,
|
|
2802
|
+
projectedY,
|
|
2803
|
+
bounds,
|
|
2804
|
+
sheetHeight,
|
|
2805
|
+
fontSize
|
|
2806
|
+
)
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
/**
|
|
2810
|
+
* Resolves the owner body or internal owner body cell containing text.
|
|
2811
|
+
* @param {{ ownerIndex?: string, recordType?: string, rotation?: number }} text Text primitive.
|
|
2812
|
+
* @param {number} x Render X coordinate.
|
|
2813
|
+
* @param {number} sourceY Unprojected source Y coordinate.
|
|
2814
|
+
* @param {number} fontSize Viewer font size.
|
|
2815
|
+
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
|
|
2816
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
2817
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines Line primitives.
|
|
2818
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
2819
|
+
*/
|
|
2820
|
+
static #resolveOwnerBodyTextCellBounds(
|
|
2821
|
+
text,
|
|
2822
|
+
x,
|
|
2823
|
+
sourceY,
|
|
2824
|
+
fontSize,
|
|
2825
|
+
matchedOwnerPin,
|
|
2826
|
+
ownerTextBodyBounds,
|
|
2827
|
+
lines
|
|
2828
|
+
) {
|
|
2829
|
+
const bounds = SchematicSvgRenderer.#resolveOwnerBodyTextBounds(
|
|
2830
|
+
text,
|
|
2831
|
+
x,
|
|
2832
|
+
sourceY,
|
|
2833
|
+
fontSize,
|
|
2834
|
+
matchedOwnerPin,
|
|
2835
|
+
ownerTextBodyBounds
|
|
2836
|
+
)
|
|
2837
|
+
|
|
2838
|
+
if (!bounds) {
|
|
2839
|
+
return null
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
return (
|
|
2843
|
+
SchematicSvgRenderer.#resolveOwnerBodyTextLineCellBounds(
|
|
2844
|
+
text,
|
|
2845
|
+
x,
|
|
2846
|
+
sourceY,
|
|
2847
|
+
fontSize,
|
|
2848
|
+
bounds,
|
|
2849
|
+
lines
|
|
2850
|
+
) || bounds
|
|
2851
|
+
)
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
/**
|
|
2855
|
+
* Resolves the owner body containing one free text primitive.
|
|
2856
|
+
* @param {{ ownerIndex?: string, recordType?: string, rotation?: number }} text Text primitive.
|
|
2857
|
+
* @param {number} x Render X coordinate.
|
|
2858
|
+
* @param {number} sourceY Unprojected source Y coordinate.
|
|
2859
|
+
* @param {number} fontSize Viewer font size.
|
|
2860
|
+
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
|
|
2861
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
2862
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
2863
|
+
*/
|
|
2864
|
+
static #resolveOwnerBodyTextBounds(
|
|
2865
|
+
text,
|
|
2866
|
+
x,
|
|
2867
|
+
sourceY,
|
|
2868
|
+
fontSize,
|
|
2869
|
+
matchedOwnerPin,
|
|
2870
|
+
ownerTextBodyBounds
|
|
2871
|
+
) {
|
|
2872
|
+
if (matchedOwnerPin) return null
|
|
2873
|
+
if (text.recordType !== '4') return null
|
|
2874
|
+
if (SchematicSvgRenderer.#normalizeDegrees(text.rotation) !== 0) {
|
|
2875
|
+
return null
|
|
2876
|
+
}
|
|
2877
|
+
if (!Number.isFinite(fontSize) || fontSize <= 0) return null
|
|
2878
|
+
|
|
2879
|
+
const ownerIndex = String(text.ownerIndex || '').trim()
|
|
2880
|
+
const tolerance = Math.max(fontSize * 0.4, 1)
|
|
2881
|
+
const explicitBounds = ownerIndex
|
|
2882
|
+
? ownerTextBodyBounds.get(ownerIndex)
|
|
2883
|
+
: null
|
|
2884
|
+
|
|
2885
|
+
if (
|
|
2886
|
+
explicitBounds &&
|
|
2887
|
+
SchematicSvgRenderer.#containsPointWithTolerance(
|
|
2888
|
+
explicitBounds,
|
|
2889
|
+
x,
|
|
2890
|
+
sourceY,
|
|
2891
|
+
tolerance
|
|
2892
|
+
)
|
|
2893
|
+
) {
|
|
2894
|
+
return explicitBounds
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
if (ownerIndex) {
|
|
2898
|
+
return null
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
return SchematicSvgRenderer.#findContainingOwnerBodyBounds(
|
|
2902
|
+
ownerTextBodyBounds,
|
|
2903
|
+
x,
|
|
2904
|
+
sourceY,
|
|
2905
|
+
tolerance
|
|
2906
|
+
)
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
/**
|
|
2910
|
+
* Finds the smallest owner body containing one ownerless overlay label.
|
|
2911
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
2912
|
+
* @param {number} x Text X coordinate.
|
|
2913
|
+
* @param {number} y Text baseline Y coordinate.
|
|
2914
|
+
* @param {number} tolerance Coordinate tolerance.
|
|
2915
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
2916
|
+
*/
|
|
2917
|
+
static #findContainingOwnerBodyBounds(
|
|
2918
|
+
ownerTextBodyBounds,
|
|
2919
|
+
x,
|
|
2920
|
+
y,
|
|
2921
|
+
tolerance
|
|
2922
|
+
) {
|
|
2923
|
+
let bestBounds = null
|
|
2924
|
+
let bestArea = Number.POSITIVE_INFINITY
|
|
2925
|
+
|
|
2926
|
+
for (const bounds of ownerTextBodyBounds.values()) {
|
|
2927
|
+
if (
|
|
2928
|
+
!SchematicSvgRenderer.#containsPointWithTolerance(
|
|
2929
|
+
bounds,
|
|
2930
|
+
x,
|
|
2931
|
+
y,
|
|
2932
|
+
tolerance
|
|
2933
|
+
)
|
|
2934
|
+
) {
|
|
2935
|
+
continue
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
const area =
|
|
2939
|
+
Math.max(Number(bounds.maxX) - Number(bounds.minX), 0) *
|
|
2940
|
+
Math.max(Number(bounds.maxY) - Number(bounds.minY), 0)
|
|
2941
|
+
|
|
2942
|
+
if (area < bestArea) {
|
|
2943
|
+
bestArea = area
|
|
2944
|
+
bestBounds = bounds
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
return bestBounds
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
/**
|
|
2952
|
+
* Resolves the internal horizontal-line cell containing one text label.
|
|
2953
|
+
* @param {number} x Text X coordinate.
|
|
2954
|
+
* @param {number} y Text Y coordinate.
|
|
2955
|
+
* @param {number} fontSize Viewer font size.
|
|
2956
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Owner body bounds.
|
|
2957
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines Line primitives.
|
|
2958
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
2959
|
+
*/
|
|
2960
|
+
static #resolveOwnerBodyTextLineCellBounds(
|
|
2961
|
+
text,
|
|
2962
|
+
x,
|
|
2963
|
+
y,
|
|
2964
|
+
fontSize,
|
|
2965
|
+
bounds,
|
|
2966
|
+
lines
|
|
2967
|
+
) {
|
|
2968
|
+
const separators =
|
|
2969
|
+
SchematicSvgRenderer.#collectOwnerBodyHorizontalSeparators(
|
|
2970
|
+
x,
|
|
2971
|
+
fontSize,
|
|
2972
|
+
bounds,
|
|
2973
|
+
lines
|
|
2974
|
+
)
|
|
2975
|
+
|
|
2976
|
+
if (separators.length === 0) {
|
|
2977
|
+
return null
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
const edges = [
|
|
2981
|
+
Number(bounds.minY),
|
|
2982
|
+
...separators,
|
|
2983
|
+
Number(bounds.maxY)
|
|
2984
|
+
].sort((left, right) => left - right)
|
|
2985
|
+
const tolerance = Math.max(fontSize * 0.4, 1)
|
|
2986
|
+
const descent =
|
|
2987
|
+
fontSize * SchematicSvgRenderer.#resolveBodyTextDescentRatio(text)
|
|
2988
|
+
const sampleY = y + (fontSize * BODY_TEXT_ASCENT_RATIO - descent) / 2
|
|
2989
|
+
|
|
2990
|
+
for (let index = 0; index < edges.length - 1; index += 1) {
|
|
2991
|
+
const minY = edges[index]
|
|
2992
|
+
const maxY = edges[index + 1]
|
|
2993
|
+
|
|
2994
|
+
if (sampleY >= minY && sampleY <= maxY) {
|
|
2995
|
+
return {
|
|
2996
|
+
minX: bounds.minX,
|
|
2997
|
+
maxX: bounds.maxX,
|
|
2998
|
+
minY,
|
|
2999
|
+
maxY
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
for (let index = 0; index < edges.length - 1; index += 1) {
|
|
3005
|
+
const minY = edges[index]
|
|
3006
|
+
const maxY = edges[index + 1]
|
|
3007
|
+
|
|
3008
|
+
if (sampleY >= minY - tolerance && sampleY <= maxY + tolerance) {
|
|
3009
|
+
return {
|
|
3010
|
+
minX: bounds.minX,
|
|
3011
|
+
maxX: bounds.maxX,
|
|
3012
|
+
minY,
|
|
3013
|
+
maxY
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
return null
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
/**
|
|
3022
|
+
* Collects horizontal separators that behave like internal body cell rows.
|
|
3023
|
+
* @param {number} x Text X coordinate.
|
|
3024
|
+
* @param {number} fontSize Viewer font size.
|
|
3025
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Owner body bounds.
|
|
3026
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines Line primitives.
|
|
3027
|
+
* @returns {number[]}
|
|
3028
|
+
*/
|
|
3029
|
+
static #collectOwnerBodyHorizontalSeparators(x, fontSize, bounds, lines) {
|
|
3030
|
+
const tolerance = Math.max(fontSize * 0.4, 1)
|
|
3031
|
+
const bodyWidth = Math.max(Number(bounds.maxX) - Number(bounds.minX), 0)
|
|
3032
|
+
const minSpan = Math.max(
|
|
3033
|
+
bodyWidth * BODY_TEXT_MIN_SEPARATOR_SPAN_RATIO,
|
|
3034
|
+
fontSize * 1.5
|
|
3035
|
+
)
|
|
3036
|
+
const separators = new Set()
|
|
3037
|
+
|
|
3038
|
+
for (const line of lines || []) {
|
|
3039
|
+
const y1 = Number(line?.y1)
|
|
3040
|
+
const y2 = Number(line?.y2)
|
|
3041
|
+
const x1 = Number(line?.x1)
|
|
3042
|
+
const x2 = Number(line?.x2)
|
|
3043
|
+
|
|
3044
|
+
if (
|
|
3045
|
+
!Number.isFinite(x1) ||
|
|
3046
|
+
!Number.isFinite(x2) ||
|
|
3047
|
+
!Number.isFinite(y1) ||
|
|
3048
|
+
!Number.isFinite(y2) ||
|
|
3049
|
+
Math.abs(y1 - y2) > tolerance
|
|
3050
|
+
) {
|
|
3051
|
+
continue
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
const minX = Math.min(x1, x2)
|
|
3055
|
+
const maxX = Math.max(x1, x2)
|
|
3056
|
+
const span = maxX - minX
|
|
3057
|
+
|
|
3058
|
+
if (span < minSpan) continue
|
|
3059
|
+
if (y1 <= bounds.minY + tolerance) continue
|
|
3060
|
+
if (y1 >= bounds.maxY - tolerance) continue
|
|
3061
|
+
if (minX < bounds.minX - tolerance) continue
|
|
3062
|
+
if (maxX > bounds.maxX + tolerance) continue
|
|
3063
|
+
if (x < minX - tolerance || x > maxX + tolerance) continue
|
|
3064
|
+
|
|
3065
|
+
separators.add(Number(formatNumber(y1)))
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
return [...separators].sort((left, right) => left - right)
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
/**
|
|
3072
|
+
* Clamps a projected SVG text baseline so its visual box stays in bounds.
|
|
3073
|
+
* @param {{ text?: string, resolvedText?: string }} text Text primitive.
|
|
3074
|
+
* @param {number} y Candidate SVG baseline.
|
|
3075
|
+
* @param {{ minY: number, maxY: number }} bounds Unprojected source bounds.
|
|
3076
|
+
* @param {number} sheetHeight Rendered sheet height.
|
|
3077
|
+
* @param {number} fontSize Viewer font size.
|
|
3078
|
+
* @returns {number}
|
|
3079
|
+
*/
|
|
3080
|
+
static #clampTextBaselineToBounds(text, y, bounds, sheetHeight, fontSize) {
|
|
3081
|
+
const top = projectSchematicY(sheetHeight, bounds.maxY)
|
|
3082
|
+
const bottom = projectSchematicY(sheetHeight, bounds.minY)
|
|
3083
|
+
const ascent = fontSize * BODY_TEXT_ASCENT_RATIO
|
|
3084
|
+
const descent =
|
|
3085
|
+
fontSize * SchematicSvgRenderer.#resolveBodyTextDescentRatio(text)
|
|
3086
|
+
const visualHeight = ascent + descent
|
|
3087
|
+
|
|
3088
|
+
if (bottom - top < visualHeight) {
|
|
3089
|
+
return (top + bottom) / 2 + (ascent - descent) / 2
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
let resolvedY = y
|
|
3093
|
+
const visualTop = resolvedY - ascent
|
|
3094
|
+
if (visualTop < top) {
|
|
3095
|
+
resolvedY += top - visualTop
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
const visualBottom = resolvedY + descent
|
|
3099
|
+
if (visualBottom > bottom) {
|
|
3100
|
+
resolvedY -= visualBottom - bottom
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
return resolvedY
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
/**
|
|
3107
|
+
* Estimates the descent needed for body-cell text clamping.
|
|
3108
|
+
* @param {{ text?: string, resolvedText?: string }} text Text primitive.
|
|
3109
|
+
* @returns {number}
|
|
3110
|
+
*/
|
|
3111
|
+
static #resolveBodyTextDescentRatio(text) {
|
|
3112
|
+
const value = String(text?.resolvedText ?? text?.text ?? '')
|
|
3113
|
+
|
|
3114
|
+
return BODY_TEXT_DESCENDER_PATTERN.test(value)
|
|
3115
|
+
? BODY_TEXT_DESCENT_RATIO
|
|
3116
|
+
: BODY_TEXT_FLAT_DESCENT_RATIO
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
/**
|
|
3120
|
+
* Returns true when a point falls inside bounds with tolerance.
|
|
3121
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Bounds.
|
|
3122
|
+
* @param {number} x X coordinate.
|
|
3123
|
+
* @param {number} y Y coordinate.
|
|
3124
|
+
* @param {number} tolerance Coordinate tolerance.
|
|
3125
|
+
* @returns {boolean}
|
|
3126
|
+
*/
|
|
3127
|
+
static #containsPointWithTolerance(bounds, x, y, tolerance) {
|
|
3128
|
+
const numericX = Number(x)
|
|
3129
|
+
const numericY = Number(y)
|
|
3130
|
+
|
|
3131
|
+
return (
|
|
3132
|
+
Number.isFinite(numericX) &&
|
|
3133
|
+
Number.isFinite(numericY) &&
|
|
3134
|
+
numericX >= bounds.minX - tolerance &&
|
|
3135
|
+
numericX <= bounds.maxX + tolerance &&
|
|
3136
|
+
numericY >= bounds.minY - tolerance &&
|
|
3137
|
+
numericY <= bounds.maxY + tolerance
|
|
3138
|
+
)
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
/**
|
|
3142
|
+
* Adds the SVG baseline correction for mirrored owner-local symbol text
|
|
3143
|
+
* authored with the native orientation that stores the top of the text run.
|
|
3144
|
+
* Baseline-oriented mirrored labels already use SVG-like y coordinates.
|
|
3145
|
+
* @param {{ ownerIndex?: string, recordType?: string, rotation?: number, sourceOrientation?: number, isMirrored?: boolean }} text Text primitive.
|
|
3146
|
+
* @param {number} x Render X coordinate.
|
|
3147
|
+
* @param {number} sourceY Unprojected source Y coordinate.
|
|
3148
|
+
* @param {number} fontSize Viewer font size.
|
|
3149
|
+
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
|
|
3150
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
3151
|
+
* @returns {number}
|
|
3152
|
+
*/
|
|
3153
|
+
static #resolveOwnerBodyTextBaselineOffset(
|
|
3154
|
+
text,
|
|
3155
|
+
x,
|
|
3156
|
+
sourceY,
|
|
3157
|
+
fontSize,
|
|
3158
|
+
matchedOwnerPin,
|
|
3159
|
+
ownerTextBodyBounds
|
|
3160
|
+
) {
|
|
3161
|
+
if (
|
|
3162
|
+
!SchematicSvgRenderer.#isOwnerBodyBaselineText(
|
|
3163
|
+
text,
|
|
3164
|
+
x,
|
|
3165
|
+
sourceY,
|
|
3166
|
+
fontSize,
|
|
3167
|
+
matchedOwnerPin,
|
|
3168
|
+
ownerTextBodyBounds
|
|
3169
|
+
)
|
|
3170
|
+
) {
|
|
3171
|
+
return 0
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
return fontSize
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
/**
|
|
3178
|
+
* Returns true when one text primitive is a top-origin mirrored owner-local
|
|
3179
|
+
* symbol annotation sitting inside its owner's drawable body.
|
|
3180
|
+
* @param {{ ownerIndex?: string, recordType?: string, rotation?: number, sourceOrientation?: number, isMirrored?: boolean }} text Text primitive.
|
|
3181
|
+
* @param {number} x Render X coordinate.
|
|
3182
|
+
* @param {number} sourceY Unprojected source Y coordinate.
|
|
3183
|
+
* @param {number} fontSize Viewer font size.
|
|
3184
|
+
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
|
|
3185
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
3186
|
+
* @returns {boolean}
|
|
3187
|
+
*/
|
|
3188
|
+
static #isOwnerBodyBaselineText(
|
|
3189
|
+
text,
|
|
3190
|
+
x,
|
|
3191
|
+
sourceY,
|
|
3192
|
+
fontSize,
|
|
3193
|
+
matchedOwnerPin,
|
|
3194
|
+
ownerTextBodyBounds
|
|
3195
|
+
) {
|
|
3196
|
+
if (matchedOwnerPin) return false
|
|
3197
|
+
if (text.recordType !== '4') return false
|
|
3198
|
+
if (!text.isMirrored) return false
|
|
3199
|
+
if (Number(text.sourceOrientation ?? 0) !== 2) return false
|
|
3200
|
+
if (SchematicSvgRenderer.#normalizeDegrees(text.rotation) !== 0) {
|
|
3201
|
+
return false
|
|
3202
|
+
}
|
|
3203
|
+
if (!Number.isFinite(fontSize) || fontSize <= 0) return false
|
|
3204
|
+
|
|
3205
|
+
const ownerIndex = String(text.ownerIndex || '').trim()
|
|
3206
|
+
if (!ownerIndex) return false
|
|
3207
|
+
|
|
3208
|
+
const bounds = ownerTextBodyBounds.get(ownerIndex)
|
|
3209
|
+
if (!bounds) return false
|
|
3210
|
+
|
|
3211
|
+
const numericX = Number(x)
|
|
3212
|
+
const numericY = Number(sourceY)
|
|
3213
|
+
const tolerance = Math.max(fontSize * 0.4, 1)
|
|
3214
|
+
|
|
3215
|
+
return (
|
|
3216
|
+
Number.isFinite(numericX) &&
|
|
3217
|
+
Number.isFinite(numericY) &&
|
|
3218
|
+
numericX >= bounds.minX - tolerance &&
|
|
3219
|
+
numericX <= bounds.maxX + tolerance &&
|
|
3220
|
+
numericY >= bounds.minY - tolerance &&
|
|
3221
|
+
numericY <= bounds.maxY + tolerance
|
|
3222
|
+
)
|
|
3223
|
+
}
|
|
3224
|
+
|
|
2134
3225
|
/**
|
|
2135
3226
|
* Lifts large section headings clear of authored dash-dot frame baselines.
|
|
2136
3227
|
* @param {{ x: number, y: number, ownerIndex?: string, recordType?: string, fontSize?: number, rotation?: number }} text
|
|
@@ -2229,11 +3320,18 @@ export class SchematicSvgRenderer {
|
|
|
2229
3320
|
|
|
2230
3321
|
/**
|
|
2231
3322
|
* Builds one schematic cross marker.
|
|
2232
|
-
* @param {{ x: number, y: number, size: number, color: string }} cross
|
|
3323
|
+
* @param {{ x: number, y: number, size: number, color: string, symbolName?: string }} cross
|
|
2233
3324
|
* @param {number} sheetHeight
|
|
2234
3325
|
* @returns {string}
|
|
2235
3326
|
*/
|
|
2236
3327
|
static #buildSchematicCrossMarkup(cross, sheetHeight) {
|
|
3328
|
+
if (String(cross.symbolName || '').toLowerCase() === 'checkbox') {
|
|
3329
|
+
return SchematicSvgRenderer.#buildSchematicCheckboxMarkup(
|
|
3330
|
+
cross,
|
|
3331
|
+
sheetHeight
|
|
3332
|
+
)
|
|
3333
|
+
}
|
|
3334
|
+
|
|
2237
3335
|
const x = cross.x
|
|
2238
3336
|
const y = projectSchematicY(sheetHeight, cross.y)
|
|
2239
3337
|
const half = Math.max(Number(cross.size || 6), 4) / 2
|
|
@@ -2273,6 +3371,64 @@ export class SchematicSvgRenderer {
|
|
|
2273
3371
|
)
|
|
2274
3372
|
}
|
|
2275
3373
|
|
|
3374
|
+
/**
|
|
3375
|
+
* Builds one checkbox-shaped no-ERC marker.
|
|
3376
|
+
* @param {{ x: number, y: number, size: number, color: string }} cross
|
|
3377
|
+
* @param {number} sheetHeight
|
|
3378
|
+
* @returns {string}
|
|
3379
|
+
*/
|
|
3380
|
+
static #buildSchematicCheckboxMarkup(cross, sheetHeight) {
|
|
3381
|
+
const x = Number(cross.x)
|
|
3382
|
+
const y = projectSchematicY(sheetHeight, cross.y)
|
|
3383
|
+
const size = Math.max(Number(cross.size || 6), 4)
|
|
3384
|
+
const left = x - 1
|
|
3385
|
+
const top = y - size - 7
|
|
3386
|
+
const color = escapeHtml(
|
|
3387
|
+
SchematicColorResolver.resolveColor(
|
|
3388
|
+
cross.color,
|
|
3389
|
+
'--schematic-alert-color'
|
|
3390
|
+
)
|
|
3391
|
+
)
|
|
3392
|
+
|
|
3393
|
+
return (
|
|
3394
|
+
'<g class="schematic-cross schematic-cross--checkbox"><line x1="' +
|
|
3395
|
+
formatNumber(x) +
|
|
3396
|
+
'" y1="' +
|
|
3397
|
+
formatNumber(y) +
|
|
3398
|
+
'" x2="' +
|
|
3399
|
+
formatNumber(left) +
|
|
3400
|
+
'" y2="' +
|
|
3401
|
+
formatNumber(top + size) +
|
|
3402
|
+
'" stroke="' +
|
|
3403
|
+
color +
|
|
3404
|
+
'" /><rect x="' +
|
|
3405
|
+
formatNumber(left) +
|
|
3406
|
+
'" y="' +
|
|
3407
|
+
formatNumber(top) +
|
|
3408
|
+
'" width="' +
|
|
3409
|
+
formatNumber(size) +
|
|
3410
|
+
'" height="' +
|
|
3411
|
+
formatNumber(size) +
|
|
3412
|
+
'" fill="none" stroke="' +
|
|
3413
|
+
color +
|
|
3414
|
+
'" /><polyline points="' +
|
|
3415
|
+
formatNumber(left + size * 0.2) +
|
|
3416
|
+
',' +
|
|
3417
|
+
formatNumber(top + size * 0.5) +
|
|
3418
|
+
' ' +
|
|
3419
|
+
formatNumber(left + size * 0.5) +
|
|
3420
|
+
',' +
|
|
3421
|
+
formatNumber(top + size * 0.8) +
|
|
3422
|
+
' ' +
|
|
3423
|
+
formatNumber(left + size * 0.9666666667) +
|
|
3424
|
+
',' +
|
|
3425
|
+
formatNumber(top + size * 0.2) +
|
|
3426
|
+
'" fill="none" stroke="' +
|
|
3427
|
+
color +
|
|
3428
|
+
'" /></g>'
|
|
3429
|
+
)
|
|
3430
|
+
}
|
|
3431
|
+
|
|
2276
3432
|
/**
|
|
2277
3433
|
* Builds one authored schematic junction dot.
|
|
2278
3434
|
* @param {{ x: number, y: number, color: string }} junction
|
|
@@ -2287,7 +3443,7 @@ export class SchematicSvgRenderer {
|
|
|
2287
3443
|
formatNumber(projectSchematicY(sheetHeight, junction.y)) +
|
|
2288
3444
|
'" r="2.4" fill="' +
|
|
2289
3445
|
escapeHtml(
|
|
2290
|
-
SchematicColorResolver.
|
|
3446
|
+
SchematicColorResolver.resolveNonTextColor(
|
|
2291
3447
|
junction.color,
|
|
2292
3448
|
'--schematic-default-ink-color'
|
|
2293
3449
|
)
|
|
@@ -2321,30 +3477,13 @@ export class SchematicSvgRenderer {
|
|
|
2321
3477
|
static #resolveAuthoredSchematicJunctionColor(junction, lines) {
|
|
2322
3478
|
const connectedLine = lines.find(
|
|
2323
3479
|
(line) =>
|
|
2324
|
-
|
|
3480
|
+
SchematicLineColorResolver.isElectricalLine(line) &&
|
|
2325
3481
|
SchematicSvgRenderer.#schematicLineContainsPoint(line, junction)
|
|
2326
3482
|
)
|
|
2327
3483
|
|
|
2328
3484
|
return connectedLine?.color || junction.color
|
|
2329
3485
|
}
|
|
2330
3486
|
|
|
2331
|
-
/**
|
|
2332
|
-
* Returns true when one normalized line can carry schematic net color.
|
|
2333
|
-
* @param {{ ownerIndex?: string, isBus?: boolean, recordType?: string } | null | undefined} line
|
|
2334
|
-
* @returns {boolean}
|
|
2335
|
-
*/
|
|
2336
|
-
static #isElectricalSchematicLine(line) {
|
|
2337
|
-
if (line?.ownerIndex || line?.isBus === true) {
|
|
2338
|
-
return false
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
if (!Object.prototype.hasOwnProperty.call(line || {}, 'recordType')) {
|
|
2342
|
-
return true
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
return !['6', '7', '26'].includes(String(line.recordType || ''))
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
3487
|
/**
|
|
2349
3488
|
* Returns true when one schematic line segment contains the given point.
|
|
2350
3489
|
* @param {{ x1: number, y1: number, x2: number, y2: number }} line
|