altium-toolkit 1.1.3 → 1.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -5
- package/docs/api.md +171 -23
- package/docs/model-format.md +192 -20
- package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
- package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
- package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
- package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +513 -3
- package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
- package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
- package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
- package/docs/testing.md +7 -0
- package/examples/README.md +21 -0
- package/examples/cli-utils.mjs +148 -0
- package/examples/corpus-smoke.mjs +523 -0
- package/examples/extract-bom.mjs +47 -0
- package/examples/generate-pnp.mjs +59 -0
- package/examples/inspect-board.mjs +70 -0
- package/examples/inspect-schematic.mjs +406 -0
- package/examples/library-catalog.mjs +115 -0
- package/examples/net-report.mjs +61 -0
- package/examples/validate-library.mjs +59 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +357 -16
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +52 -11
- package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
- package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
- package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
- package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
- package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
- package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
- package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
- package/src/core/altium/NormalizedModelSchema.mjs +3 -31
- package/src/core/altium/ParameterCollection.mjs +431 -0
- package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
- package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
- package/src/core/altium/ParserErrors.mjs +90 -0
- package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
- package/src/core/altium/ParserUtils.mjs +24 -0
- package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
- package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
- package/src/core/altium/PcbLayerGroups.mjs +308 -0
- package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
- package/src/core/altium/PcbLibModelParser.mjs +20 -4
- package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
- package/src/core/altium/PcbModelParser.mjs +223 -4
- package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PcbStreamExtractor.mjs +130 -6
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
- package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
- package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
- package/src/core/altium/SchLibModelParser.mjs +840 -0
- package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
- package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
- package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
- package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
- package/src/core/altium/SchematicHarnessParser.mjs +9 -3
- package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +99 -65
- package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
- package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
- package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
- package/src/core/altium/SchematicSheetParser.mjs +8 -2
- package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
- package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
- package/src/core/altium/SchematicTextParser.mjs +28 -12
- package/src/core/altium/SchematicTextRunParser.mjs +81 -0
- package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
- 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/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +43 -1
- package/src/renderers.mjs +1 -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 +185 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNoteRenderer.mjs +5 -1
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
- package/src/ui/SchematicShapeRenderer.mjs +86 -17
- package/src/ui/SchematicSvgRenderer.mjs +980 -58
- package/src/ui/SchematicTypography.mjs +4 -3
|
@@ -12,6 +12,7 @@ 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'
|
|
@@ -19,6 +20,7 @@ import { SchematicRegionRenderer } from './SchematicRegionRenderer.mjs'
|
|
|
19
20
|
import { SchematicSheetSymbolRenderer } from './SchematicSheetSymbolRenderer.mjs'
|
|
20
21
|
import { SchematicImageRenderer } from './SchematicImageRenderer.mjs'
|
|
21
22
|
import { SchematicNativeFooterPartitioner } from './SchematicNativeFooterPartitioner.mjs'
|
|
23
|
+
import { SchematicOwnerPinMarkerLineThemer } from './SchematicOwnerPinMarkerLineThemer.mjs'
|
|
22
24
|
import { TextGeometrySidecarBuilder } from './TextGeometrySidecarBuilder.mjs'
|
|
23
25
|
import { SchematicRenderOpsSidecarBuilder } from './SchematicRenderOpsSidecarBuilder.mjs'
|
|
24
26
|
import { SchematicProjectParameterResolver } from '../core/altium/SchematicProjectParameterResolver.mjs'
|
|
@@ -29,6 +31,11 @@ const SECTION_HEADING_MIN_FONT_SIZE = 18
|
|
|
29
31
|
const SECTION_HEADING_BASELINE_LIFT_RATIO = 0.36
|
|
30
32
|
const SECTION_HEADING_LINE_Y_TOLERANCE = 0.75
|
|
31
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_,;]/
|
|
32
39
|
|
|
33
40
|
/**
|
|
34
41
|
* Renders normalized schematic models into presentational SVG.
|
|
@@ -38,7 +45,7 @@ export class SchematicSvgRenderer {
|
|
|
38
45
|
|
|
39
46
|
/**
|
|
40
47
|
* Renders a normalized schematic model into SVG markup.
|
|
41
|
-
* @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
|
|
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, textSegments?: { text: string, overline: boolean }[], 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
|
|
42
49
|
* @param {{ projectParameters?: Record<string, string | number | boolean | null | undefined>, colorizeImages?: boolean, colorize_images?: boolean }} options Render options.
|
|
43
50
|
* @returns {string}
|
|
44
51
|
*/
|
|
@@ -179,8 +186,13 @@ export class SchematicSvgRenderer {
|
|
|
179
186
|
},
|
|
180
187
|
renderedSchematic.sheet
|
|
181
188
|
)
|
|
189
|
+
const contentMarkerThemedLines =
|
|
190
|
+
SchematicOwnerPinMarkerLineThemer.theme(
|
|
191
|
+
partitionedPrimitives.content.lines,
|
|
192
|
+
pins
|
|
193
|
+
)
|
|
182
194
|
const contentLines = SchematicSvgRenderer.#themeFooterChromeLines(
|
|
183
|
-
|
|
195
|
+
contentMarkerThemedLines,
|
|
184
196
|
renderedSheet.sheet,
|
|
185
197
|
contentHeight
|
|
186
198
|
)
|
|
@@ -196,7 +208,18 @@ export class SchematicSvgRenderer {
|
|
|
196
208
|
const contentTexts = partitionedPrimitives.content.texts
|
|
197
209
|
const contentImages = partitionedPrimitives.content.images
|
|
198
210
|
const footerPrimitives = partitionedPrimitives.footer
|
|
199
|
-
const
|
|
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
|
+
)
|
|
200
223
|
const ownerlessPolygons = contentPolygons.filter(
|
|
201
224
|
(polygon) => !polygon.ownerIndex
|
|
202
225
|
)
|
|
@@ -408,6 +431,21 @@ export class SchematicSvgRenderer {
|
|
|
408
431
|
contentHeight,
|
|
409
432
|
semanticContext
|
|
410
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('')
|
|
411
449
|
const sheetSymbolMarkup =
|
|
412
450
|
SchematicSheetSymbolRenderer.buildSheetSymbolMarkup(
|
|
413
451
|
sheetSymbols,
|
|
@@ -453,6 +491,16 @@ export class SchematicSvgRenderer {
|
|
|
453
491
|
)
|
|
454
492
|
const markerDefsMarkup =
|
|
455
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
|
+
)
|
|
456
504
|
|
|
457
505
|
const textMarkup = contentTexts
|
|
458
506
|
.map((text, index) =>
|
|
@@ -467,7 +515,8 @@ export class SchematicSvgRenderer {
|
|
|
467
515
|
text,
|
|
468
516
|
index
|
|
469
517
|
),
|
|
470
|
-
semanticContext
|
|
518
|
+
semanticContext,
|
|
519
|
+
ownerTextBodyBounds
|
|
471
520
|
)
|
|
472
521
|
)
|
|
473
522
|
.join('')
|
|
@@ -585,6 +634,12 @@ export class SchematicSvgRenderer {
|
|
|
585
634
|
pins,
|
|
586
635
|
contentRectangles
|
|
587
636
|
)
|
|
637
|
+
const overlappingExternalNumberLabelKeys =
|
|
638
|
+
SchematicOwnerPinLabelLayout.collectOverlappingExternalNumberLabelKeys(
|
|
639
|
+
pins,
|
|
640
|
+
contentTexts,
|
|
641
|
+
internalNumberLabelBoxes
|
|
642
|
+
)
|
|
588
643
|
const pinMarkup = pins
|
|
589
644
|
.map((pin, index) =>
|
|
590
645
|
SchematicSvgRenderer.#appendSvgAttributes(
|
|
@@ -596,7 +651,8 @@ export class SchematicSvgRenderer {
|
|
|
596
651
|
explicitOwnerPinNameLabels,
|
|
597
652
|
explicitOwnerPinLabelOffsets,
|
|
598
653
|
compactExternalNumberLabelSides,
|
|
599
|
-
internalNumberLabelBoxes
|
|
654
|
+
internalNumberLabelBoxes,
|
|
655
|
+
overlappingExternalNumberLabelKeys
|
|
600
656
|
),
|
|
601
657
|
SchematicSvgRenderer.#semanticAttributes(
|
|
602
658
|
'pin',
|
|
@@ -726,6 +782,9 @@ export class SchematicSvgRenderer {
|
|
|
726
782
|
'<g class="schematic-owner-geometry" stroke-linecap="round">' +
|
|
727
783
|
ownerGeometryMarkup +
|
|
728
784
|
'</g>' +
|
|
785
|
+
'<g class="schematic-foreground-lines" stroke-linecap="round">' +
|
|
786
|
+
foregroundLineMarkup +
|
|
787
|
+
'</g>' +
|
|
729
788
|
'<g class="schematic-sheet-symbols">' +
|
|
730
789
|
sheetSymbolMarkup +
|
|
731
790
|
'</g>' +
|
|
@@ -845,8 +904,10 @@ export class SchematicSvgRenderer {
|
|
|
845
904
|
return false
|
|
846
905
|
}
|
|
847
906
|
if (
|
|
848
|
-
|
|
849
|
-
|
|
907
|
+
SchematicColorResolver.resolveColor(
|
|
908
|
+
line.color,
|
|
909
|
+
'--schematic-default-ink-color'
|
|
910
|
+
) !== 'var(--schematic-text-color)'
|
|
850
911
|
) {
|
|
851
912
|
return false
|
|
852
913
|
}
|
|
@@ -872,6 +933,13 @@ export class SchematicSvgRenderer {
|
|
|
872
933
|
}
|
|
873
934
|
|
|
874
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
|
+
|
|
875
943
|
const footerBandHeight = Math.max(margin * 6, 120)
|
|
876
944
|
const footerTop = Math.max(height - footerBandHeight, margin)
|
|
877
945
|
const projectedY1 = projectSchematicY(height, y1)
|
|
@@ -1013,7 +1081,9 @@ export class SchematicSvgRenderer {
|
|
|
1013
1081
|
const id = SchematicSvgRenderer.#schematicLineMarkerId(marker)
|
|
1014
1082
|
const shapeName = String(marker?.shapeName || '')
|
|
1015
1083
|
const fill =
|
|
1016
|
-
shapeName === 'filled-arrow' ||
|
|
1084
|
+
shapeName === 'filled-arrow' ||
|
|
1085
|
+
shapeName === 'filled-tail' ||
|
|
1086
|
+
shapeName === 'square'
|
|
1017
1087
|
? 'context-stroke'
|
|
1018
1088
|
: 'none'
|
|
1019
1089
|
const shape =
|
|
@@ -1021,9 +1091,13 @@ export class SchematicSvgRenderer {
|
|
|
1021
1091
|
? '<circle cx="5" cy="5" r="3.2" fill="none" stroke="context-stroke" stroke-width="1.4" />'
|
|
1022
1092
|
: shapeName === 'square'
|
|
1023
1093
|
? '<rect x="2" y="2" width="6" height="6" fill="context-stroke" stroke="context-stroke" stroke-width="1" />'
|
|
1024
|
-
:
|
|
1025
|
-
fill +
|
|
1026
|
-
|
|
1094
|
+
: shapeName === 'tail' || shapeName === 'filled-tail'
|
|
1095
|
+
? '<path d="M 9 1 L 1 5 L 9 9" fill="' +
|
|
1096
|
+
fill +
|
|
1097
|
+
'" stroke="context-stroke" stroke-width="1.4" stroke-linejoin="round" />'
|
|
1098
|
+
: '<path d="M 1 1 L 9 5 L 1 9" fill="' +
|
|
1099
|
+
fill +
|
|
1100
|
+
'" stroke="context-stroke" stroke-width="1.4" stroke-linejoin="round" />'
|
|
1027
1101
|
|
|
1028
1102
|
return (
|
|
1029
1103
|
'<marker id="' +
|
|
@@ -1791,6 +1865,311 @@ export class SchematicSvgRenderer {
|
|
|
1791
1865
|
return height + margin * 2
|
|
1792
1866
|
}
|
|
1793
1867
|
|
|
1868
|
+
/**
|
|
1869
|
+
* Collects ownerless line primitives that were authored after an
|
|
1870
|
+
* owner-linked fill they overlap. These are symbol decoration strokes
|
|
1871
|
+
* stored outside the owner group and must paint above the body fill.
|
|
1872
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, x1: number, y1: number, x2: number, y2: number }[]} lines Line primitives.
|
|
1873
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, points?: { x: number, y: number }[] }[]} polygons Polygon primitives.
|
|
1874
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, x: number, y: number, width: number, height: number }[]} rectangles Rectangle primitives.
|
|
1875
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, x: number, y: number, width: number, height: number }[]} roundedRectangles Rounded rectangle primitives.
|
|
1876
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, x: number, y: number, radiusX: number, radiusY: number }[]} ellipses Ellipse primitives.
|
|
1877
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, x: number, y: number, radius: number, radiusY?: number }[]} pies Pie primitives.
|
|
1878
|
+
* @returns {Set<object>}
|
|
1879
|
+
*/
|
|
1880
|
+
static #collectForegroundOwnerlessLines(
|
|
1881
|
+
lines,
|
|
1882
|
+
polygons,
|
|
1883
|
+
rectangles,
|
|
1884
|
+
roundedRectangles,
|
|
1885
|
+
ellipses,
|
|
1886
|
+
pies
|
|
1887
|
+
) {
|
|
1888
|
+
const ownerFillBounds = SchematicSvgRenderer.#collectOwnerFillBounds(
|
|
1889
|
+
polygons,
|
|
1890
|
+
rectangles,
|
|
1891
|
+
roundedRectangles,
|
|
1892
|
+
ellipses,
|
|
1893
|
+
pies
|
|
1894
|
+
)
|
|
1895
|
+
|
|
1896
|
+
if (!ownerFillBounds.length) {
|
|
1897
|
+
return new Set()
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
return new Set(
|
|
1901
|
+
(lines || []).filter((line) =>
|
|
1902
|
+
SchematicSvgRenderer.#isForegroundOwnerlessLine(
|
|
1903
|
+
line,
|
|
1904
|
+
ownerFillBounds
|
|
1905
|
+
)
|
|
1906
|
+
)
|
|
1907
|
+
)
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
/**
|
|
1911
|
+
* Collects sortable bounds for owner-linked fill primitives.
|
|
1912
|
+
* @param {object[]} polygons Polygon primitives.
|
|
1913
|
+
* @param {object[]} rectangles Rectangle primitives.
|
|
1914
|
+
* @param {object[]} roundedRectangles Rounded rectangle primitives.
|
|
1915
|
+
* @param {object[]} ellipses Ellipse primitives.
|
|
1916
|
+
* @param {object[]} pies Pie primitives.
|
|
1917
|
+
* @returns {{ bounds: { minX: number, minY: number, maxX: number, maxY: number }, renderOrder: number }[]}
|
|
1918
|
+
*/
|
|
1919
|
+
static #collectOwnerFillBounds(
|
|
1920
|
+
polygons,
|
|
1921
|
+
rectangles,
|
|
1922
|
+
roundedRectangles,
|
|
1923
|
+
ellipses,
|
|
1924
|
+
pies
|
|
1925
|
+
) {
|
|
1926
|
+
return [
|
|
1927
|
+
...(polygons || []),
|
|
1928
|
+
...(rectangles || []),
|
|
1929
|
+
...(roundedRectangles || []),
|
|
1930
|
+
...(ellipses || []),
|
|
1931
|
+
...(pies || [])
|
|
1932
|
+
]
|
|
1933
|
+
.filter((primitive) => primitive?.ownerIndex)
|
|
1934
|
+
.map((primitive) => ({
|
|
1935
|
+
bounds: SchematicSvgRenderer.#primitiveBounds(primitive),
|
|
1936
|
+
renderOrder:
|
|
1937
|
+
SchematicSvgRenderer.#resolvePrimitiveRenderOrder(primitive)
|
|
1938
|
+
}))
|
|
1939
|
+
.filter(
|
|
1940
|
+
(entry) =>
|
|
1941
|
+
entry.bounds &&
|
|
1942
|
+
Number.isFinite(entry.renderOrder) &&
|
|
1943
|
+
entry.renderOrder < Number.MAX_SAFE_INTEGER
|
|
1944
|
+
)
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/**
|
|
1948
|
+
* Collects unprojected owner body envelopes used to identify text that is
|
|
1949
|
+
* drawn inside a component body instead of on surrounding wires or sheet
|
|
1950
|
+
* chrome.
|
|
1951
|
+
* @param {object[]} lines Line primitives.
|
|
1952
|
+
* @param {object[]} polygons Polygon primitives.
|
|
1953
|
+
* @param {object[]} rectangles Rectangle primitives.
|
|
1954
|
+
* @param {object[]} roundedRectangles Rounded rectangle primitives.
|
|
1955
|
+
* @param {object[]} ellipses Ellipse primitives.
|
|
1956
|
+
* @param {object[]} arcs Arc primitives.
|
|
1957
|
+
* @param {object[]} pies Pie primitives.
|
|
1958
|
+
* @returns {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>}
|
|
1959
|
+
*/
|
|
1960
|
+
static #collectOwnerTextBodyBounds(
|
|
1961
|
+
lines,
|
|
1962
|
+
polygons,
|
|
1963
|
+
rectangles,
|
|
1964
|
+
roundedRectangles,
|
|
1965
|
+
ellipses,
|
|
1966
|
+
arcs,
|
|
1967
|
+
pies
|
|
1968
|
+
) {
|
|
1969
|
+
const ownerBounds = new Map()
|
|
1970
|
+
|
|
1971
|
+
for (const primitive of [
|
|
1972
|
+
...(lines || []),
|
|
1973
|
+
...(polygons || []),
|
|
1974
|
+
...(rectangles || []),
|
|
1975
|
+
...(roundedRectangles || []),
|
|
1976
|
+
...(ellipses || []),
|
|
1977
|
+
...(arcs || []),
|
|
1978
|
+
...(pies || [])
|
|
1979
|
+
]) {
|
|
1980
|
+
SchematicSvgRenderer.#extendOwnerTextBodyBounds(
|
|
1981
|
+
ownerBounds,
|
|
1982
|
+
primitive
|
|
1983
|
+
)
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
return ownerBounds
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* Extends the body-bounds map with one owner-linked primitive.
|
|
1991
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerBounds Owner bounds map.
|
|
1992
|
+
* @param {object} primitive Primitive candidate.
|
|
1993
|
+
* @returns {void}
|
|
1994
|
+
*/
|
|
1995
|
+
static #extendOwnerTextBodyBounds(ownerBounds, primitive) {
|
|
1996
|
+
const ownerIndex = String(primitive?.ownerIndex || '').trim()
|
|
1997
|
+
const bounds = SchematicSvgRenderer.#primitiveBounds(primitive)
|
|
1998
|
+
|
|
1999
|
+
if (!ownerIndex || !bounds) {
|
|
2000
|
+
return
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
const current = ownerBounds.get(ownerIndex)
|
|
2004
|
+
|
|
2005
|
+
if (!current) {
|
|
2006
|
+
ownerBounds.set(ownerIndex, { ...bounds })
|
|
2007
|
+
return
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
current.minX = Math.min(current.minX, bounds.minX)
|
|
2011
|
+
current.minY = Math.min(current.minY, bounds.minY)
|
|
2012
|
+
current.maxX = Math.max(current.maxX, bounds.maxX)
|
|
2013
|
+
current.maxY = Math.max(current.maxY, bounds.maxY)
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
/**
|
|
2017
|
+
* Returns true when one ownerless line should paint above an overlapping
|
|
2018
|
+
* owner fill because the native stream authored it later.
|
|
2019
|
+
* @param {{ ownerIndex?: string, renderOrder?: number, x1: number, y1: number, x2: number, y2: number }} line Line primitive.
|
|
2020
|
+
* @param {{ bounds: { minX: number, minY: number, maxX: number, maxY: number }, renderOrder: number }[]} ownerFillBounds Fill bounds.
|
|
2021
|
+
* @returns {boolean}
|
|
2022
|
+
*/
|
|
2023
|
+
static #isForegroundOwnerlessLine(line, ownerFillBounds) {
|
|
2024
|
+
if (line?.ownerIndex) {
|
|
2025
|
+
return false
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
const renderOrder = Number(line?.renderOrder)
|
|
2029
|
+
if (!Number.isFinite(renderOrder)) {
|
|
2030
|
+
return false
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
const lineBounds = SchematicSvgRenderer.#primitiveBounds(line)
|
|
2034
|
+
if (!lineBounds) {
|
|
2035
|
+
return false
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
return ownerFillBounds.some(
|
|
2039
|
+
(fill) =>
|
|
2040
|
+
renderOrder > fill.renderOrder &&
|
|
2041
|
+
SchematicSvgRenderer.#boundsIntersect(
|
|
2042
|
+
lineBounds,
|
|
2043
|
+
fill.bounds,
|
|
2044
|
+
0.5
|
|
2045
|
+
)
|
|
2046
|
+
)
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
/**
|
|
2050
|
+
* Resolves an unprojected coordinate envelope for one schematic primitive.
|
|
2051
|
+
* @param {object} primitive Primitive.
|
|
2052
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
2053
|
+
*/
|
|
2054
|
+
static #primitiveBounds(primitive) {
|
|
2055
|
+
if (Array.isArray(primitive?.points) && primitive.points.length > 0) {
|
|
2056
|
+
return SchematicSvgRenderer.#pointBounds(primitive.points)
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
if (
|
|
2060
|
+
Number.isFinite(Number(primitive?.x1)) &&
|
|
2061
|
+
Number.isFinite(Number(primitive?.y1)) &&
|
|
2062
|
+
Number.isFinite(Number(primitive?.x2)) &&
|
|
2063
|
+
Number.isFinite(Number(primitive?.y2))
|
|
2064
|
+
) {
|
|
2065
|
+
return SchematicSvgRenderer.#pointBounds([
|
|
2066
|
+
{ x: Number(primitive.x1), y: Number(primitive.y1) },
|
|
2067
|
+
{ x: Number(primitive.x2), y: Number(primitive.y2) }
|
|
2068
|
+
])
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
if (
|
|
2072
|
+
Number.isFinite(Number(primitive?.x)) &&
|
|
2073
|
+
Number.isFinite(Number(primitive?.y)) &&
|
|
2074
|
+
Number.isFinite(Number(primitive?.width)) &&
|
|
2075
|
+
Number.isFinite(Number(primitive?.height))
|
|
2076
|
+
) {
|
|
2077
|
+
const x = Number(primitive.x)
|
|
2078
|
+
const y = Number(primitive.y)
|
|
2079
|
+
const x2 = x + Number(primitive.width)
|
|
2080
|
+
const y2 = y + Number(primitive.height)
|
|
2081
|
+
|
|
2082
|
+
return {
|
|
2083
|
+
minX: Math.min(x, x2),
|
|
2084
|
+
minY: Math.min(y, y2),
|
|
2085
|
+
maxX: Math.max(x, x2),
|
|
2086
|
+
maxY: Math.max(y, y2)
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
if (
|
|
2091
|
+
Number.isFinite(Number(primitive?.x)) &&
|
|
2092
|
+
Number.isFinite(Number(primitive?.y)) &&
|
|
2093
|
+
Number.isFinite(Number(primitive?.radiusX)) &&
|
|
2094
|
+
Number.isFinite(Number(primitive?.radiusY))
|
|
2095
|
+
) {
|
|
2096
|
+
const x = Number(primitive.x)
|
|
2097
|
+
const y = Number(primitive.y)
|
|
2098
|
+
const radiusX = Math.abs(Number(primitive.radiusX))
|
|
2099
|
+
const radiusY = Math.abs(Number(primitive.radiusY))
|
|
2100
|
+
|
|
2101
|
+
return {
|
|
2102
|
+
minX: x - radiusX,
|
|
2103
|
+
minY: y - radiusY,
|
|
2104
|
+
maxX: x + radiusX,
|
|
2105
|
+
maxY: y + radiusY
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
if (
|
|
2110
|
+
Number.isFinite(Number(primitive?.x)) &&
|
|
2111
|
+
Number.isFinite(Number(primitive?.y)) &&
|
|
2112
|
+
Number.isFinite(Number(primitive?.radius))
|
|
2113
|
+
) {
|
|
2114
|
+
const x = Number(primitive.x)
|
|
2115
|
+
const y = Number(primitive.y)
|
|
2116
|
+
const radiusX = Math.abs(Number(primitive.radius))
|
|
2117
|
+
const radiusY = Math.abs(Number(primitive.radiusY ?? radiusX))
|
|
2118
|
+
|
|
2119
|
+
return {
|
|
2120
|
+
minX: x - radiusX,
|
|
2121
|
+
minY: y - radiusY,
|
|
2122
|
+
maxX: x + radiusX,
|
|
2123
|
+
maxY: y + radiusY
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
return null
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
/**
|
|
2131
|
+
* Resolves bounds for a point list.
|
|
2132
|
+
* @param {{ x: number, y: number }[]} points Points.
|
|
2133
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
2134
|
+
*/
|
|
2135
|
+
static #pointBounds(points) {
|
|
2136
|
+
const numericPoints = (points || [])
|
|
2137
|
+
.map((point) => ({
|
|
2138
|
+
x: Number(point?.x),
|
|
2139
|
+
y: Number(point?.y)
|
|
2140
|
+
}))
|
|
2141
|
+
.filter(
|
|
2142
|
+
(point) => Number.isFinite(point.x) && Number.isFinite(point.y)
|
|
2143
|
+
)
|
|
2144
|
+
|
|
2145
|
+
if (!numericPoints.length) {
|
|
2146
|
+
return null
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
return {
|
|
2150
|
+
minX: Math.min(...numericPoints.map((point) => point.x)),
|
|
2151
|
+
minY: Math.min(...numericPoints.map((point) => point.y)),
|
|
2152
|
+
maxX: Math.max(...numericPoints.map((point) => point.x)),
|
|
2153
|
+
maxY: Math.max(...numericPoints.map((point) => point.y))
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
/**
|
|
2158
|
+
* Returns true when two unprojected coordinate bounds overlap.
|
|
2159
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} left Left bounds.
|
|
2160
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} right Right bounds.
|
|
2161
|
+
* @param {number} tolerance Coordinate tolerance.
|
|
2162
|
+
* @returns {boolean}
|
|
2163
|
+
*/
|
|
2164
|
+
static #boundsIntersect(left, right, tolerance = 0) {
|
|
2165
|
+
return (
|
|
2166
|
+
left.maxX >= right.minX - tolerance &&
|
|
2167
|
+
left.minX <= right.maxX + tolerance &&
|
|
2168
|
+
left.maxY >= right.minY - tolerance &&
|
|
2169
|
+
left.minY <= right.maxY + tolerance
|
|
2170
|
+
)
|
|
2171
|
+
}
|
|
2172
|
+
|
|
1794
2173
|
/**
|
|
1795
2174
|
* Builds interleaved owner geometry so symbol-internal primitives preserve
|
|
1796
2175
|
* their recovered Altium paint order instead of batching fills ahead of all
|
|
@@ -2131,7 +2510,7 @@ export class SchematicSvgRenderer {
|
|
|
2131
2510
|
'" y2="' +
|
|
2132
2511
|
formatNumber(projectSchematicY(sheetHeight, line.y2)) +
|
|
2133
2512
|
'" stroke="' +
|
|
2134
|
-
escapeHtml(
|
|
2513
|
+
escapeHtml(SchematicLineColorResolver.resolveColor(line)) +
|
|
2135
2514
|
'" stroke-width="' +
|
|
2136
2515
|
formatNumber(
|
|
2137
2516
|
SchematicSvgRenderer.#resolveSchematicLineWidth(line)
|
|
@@ -2162,26 +2541,6 @@ export class SchematicSvgRenderer {
|
|
|
2162
2541
|
return Math.max(baseWidth * 3, 3)
|
|
2163
2542
|
}
|
|
2164
2543
|
|
|
2165
|
-
/**
|
|
2166
|
-
* Resolves line stroke color, treating black electrical wires as themed
|
|
2167
|
-
* schematic connectivity while leaving graphic linework source-colored.
|
|
2168
|
-
* @param {{ color: string, ownerIndex?: string, isBus?: boolean, recordType?: string }} line
|
|
2169
|
-
* @returns {string}
|
|
2170
|
-
*/
|
|
2171
|
-
static #resolveSchematicLineColor(line) {
|
|
2172
|
-
if (SchematicSvgRenderer.#isElectricalSchematicLine(line)) {
|
|
2173
|
-
return SchematicColorResolver.resolveNonTextColor(
|
|
2174
|
-
line.color,
|
|
2175
|
-
'--schematic-default-ink-color'
|
|
2176
|
-
)
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
|
-
return SchematicColorResolver.resolveColor(
|
|
2180
|
-
line.color,
|
|
2181
|
-
'--schematic-default-ink-color'
|
|
2182
|
-
)
|
|
2183
|
-
}
|
|
2184
|
-
|
|
2185
2544
|
/**
|
|
2186
2545
|
* Returns SVG stroke attributes for one schematic line style.
|
|
2187
2546
|
* @param {{ width: number, lineStyle?: number }} line
|
|
@@ -2255,12 +2614,13 @@ export class SchematicSvgRenderer {
|
|
|
2255
2614
|
|
|
2256
2615
|
/**
|
|
2257
2616
|
* Builds one free text primitive with font metadata.
|
|
2258
|
-
* @param {{ 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', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
|
|
2617
|
+
* @param {{ x: number, y: number, text: string, textSegments?: { text: string, overline: boolean }[], color: string, recordType?: string, style?: number, fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, rotation?: number, sourceOrientation?: number, isMirrored?: boolean, anchor?: 'start' | 'middle' | 'end', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
|
|
2259
2618
|
* @param {number} sheetHeight
|
|
2260
2619
|
* @param {{ x1: number, y1: number, x2: number, y2: number }[]} lines
|
|
2261
2620
|
* @param {{ x: number, y: number, length: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
2262
2621
|
* @param {number} index Stable primitive index.
|
|
2263
2622
|
* @param {object | undefined} semanticContext Semantic lookup context.
|
|
2623
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
2264
2624
|
* @returns {string}
|
|
2265
2625
|
*/
|
|
2266
2626
|
static #buildSchematicTextMarkup(
|
|
@@ -2269,7 +2629,8 @@ export class SchematicSvgRenderer {
|
|
|
2269
2629
|
lines,
|
|
2270
2630
|
pins,
|
|
2271
2631
|
index = 0,
|
|
2272
|
-
semanticContext = undefined
|
|
2632
|
+
semanticContext = undefined,
|
|
2633
|
+
ownerTextBodyBounds = new Map()
|
|
2273
2634
|
) {
|
|
2274
2635
|
const matchedOwnerPin =
|
|
2275
2636
|
SchematicOwnerPinLabelLayout.findExplicitOwnerPinLabelMatch(
|
|
@@ -2309,7 +2670,8 @@ export class SchematicSvgRenderer {
|
|
|
2309
2670
|
text,
|
|
2310
2671
|
sheetHeight,
|
|
2311
2672
|
lines,
|
|
2312
|
-
matchedOwnerPin
|
|
2673
|
+
matchedOwnerPin,
|
|
2674
|
+
ownerTextBodyBounds
|
|
2313
2675
|
)
|
|
2314
2676
|
|
|
2315
2677
|
return SchematicSvgRenderer.#appendSvgAttributes(
|
|
@@ -2340,17 +2702,19 @@ export class SchematicSvgRenderer {
|
|
|
2340
2702
|
|
|
2341
2703
|
/**
|
|
2342
2704
|
* Resolves final text placement for schematic free-text annotations.
|
|
2343
|
-
* @param {{ x: number, y: number, text: string, ownerIndex?: string, recordType?: string, fontSize?: number, rotation?: number, anchor?: 'start' | 'middle' | 'end' }} text
|
|
2705
|
+
* @param {{ x: number, y: number, text: string, ownerIndex?: string, recordType?: string, fontSize?: number, rotation?: number, anchor?: 'start' | 'middle' | 'end', verticalAnchor?: 'top' }} text
|
|
2344
2706
|
* @param {number} sheetHeight
|
|
2345
2707
|
* @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines
|
|
2346
2708
|
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin
|
|
2709
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
2347
2710
|
* @returns {{ x: number, y: number, anchor: 'start' | 'middle' | 'end' }}
|
|
2348
2711
|
*/
|
|
2349
2712
|
static #resolveSchematicTextPlacement(
|
|
2350
2713
|
text,
|
|
2351
2714
|
sheetHeight,
|
|
2352
2715
|
lines,
|
|
2353
|
-
matchedOwnerPin
|
|
2716
|
+
matchedOwnerPin,
|
|
2717
|
+
ownerTextBodyBounds
|
|
2354
2718
|
) {
|
|
2355
2719
|
const mirroredOwnerPinPlacement =
|
|
2356
2720
|
SchematicOwnerPinLabelLayout.resolveMirroredOwnerPinLabelPlacement(
|
|
@@ -2358,6 +2722,7 @@ export class SchematicSvgRenderer {
|
|
|
2358
2722
|
matchedOwnerPin
|
|
2359
2723
|
)
|
|
2360
2724
|
const sourceY = mirroredOwnerPinPlacement?.y ?? text.y
|
|
2725
|
+
const x = mirroredOwnerPinPlacement?.x ?? text.x
|
|
2361
2726
|
const projectedY = projectSchematicY(sheetHeight, sourceY)
|
|
2362
2727
|
const fontSize =
|
|
2363
2728
|
SchematicTypography.resolveViewerFontSize(text.fontSize) || 0
|
|
@@ -2369,14 +2734,523 @@ export class SchematicSvgRenderer {
|
|
|
2369
2734
|
fontSize,
|
|
2370
2735
|
matchedOwnerPin
|
|
2371
2736
|
)
|
|
2737
|
+
const ownerBodyBaselineOffset =
|
|
2738
|
+
SchematicSvgRenderer.#resolveOwnerBodyTextBaselineOffset(
|
|
2739
|
+
text,
|
|
2740
|
+
x,
|
|
2741
|
+
sourceY,
|
|
2742
|
+
fontSize,
|
|
2743
|
+
matchedOwnerPin,
|
|
2744
|
+
ownerTextBodyBounds
|
|
2745
|
+
)
|
|
2746
|
+
const verticalAnchorBaselineOffset =
|
|
2747
|
+
SchematicSvgRenderer.#resolveVerticalAnchorBaselineOffset(
|
|
2748
|
+
text,
|
|
2749
|
+
fontSize
|
|
2750
|
+
)
|
|
2751
|
+
|
|
2752
|
+
const unclampedY =
|
|
2753
|
+
projectedY -
|
|
2754
|
+
baselineLift +
|
|
2755
|
+
ownerBodyBaselineOffset +
|
|
2756
|
+
verticalAnchorBaselineOffset
|
|
2372
2757
|
|
|
2373
2758
|
return {
|
|
2374
|
-
x
|
|
2375
|
-
y:
|
|
2759
|
+
x,
|
|
2760
|
+
y: SchematicSvgRenderer.#clampOwnerBodyTextBaseline(
|
|
2761
|
+
text,
|
|
2762
|
+
x,
|
|
2763
|
+
sourceY,
|
|
2764
|
+
unclampedY,
|
|
2765
|
+
sheetHeight,
|
|
2766
|
+
fontSize,
|
|
2767
|
+
matchedOwnerPin,
|
|
2768
|
+
ownerTextBodyBounds,
|
|
2769
|
+
lines
|
|
2770
|
+
),
|
|
2376
2771
|
anchor: text.anchor || 'start'
|
|
2377
2772
|
}
|
|
2378
2773
|
}
|
|
2379
2774
|
|
|
2775
|
+
/**
|
|
2776
|
+
* Keeps free schematic body annotations inside the owner body cell they
|
|
2777
|
+
* were authored over, including ownerless overlay text used by some
|
|
2778
|
+
* symbols for truth-table and switch-state labels.
|
|
2779
|
+
* @param {{ x: number, y: number, text: string, ownerIndex?: string, recordType?: string, rotation?: number }} text Text primitive.
|
|
2780
|
+
* @param {number} x Render X coordinate.
|
|
2781
|
+
* @param {number} sourceY Unprojected source Y coordinate.
|
|
2782
|
+
* @param {number} projectedY Candidate SVG baseline.
|
|
2783
|
+
* @param {number} sheetHeight Rendered sheet height.
|
|
2784
|
+
* @param {number} fontSize Viewer font size.
|
|
2785
|
+
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
|
|
2786
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
2787
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines Line primitives.
|
|
2788
|
+
* @returns {number}
|
|
2789
|
+
*/
|
|
2790
|
+
static #clampOwnerBodyTextBaseline(
|
|
2791
|
+
text,
|
|
2792
|
+
x,
|
|
2793
|
+
sourceY,
|
|
2794
|
+
projectedY,
|
|
2795
|
+
sheetHeight,
|
|
2796
|
+
fontSize,
|
|
2797
|
+
matchedOwnerPin,
|
|
2798
|
+
ownerTextBodyBounds,
|
|
2799
|
+
lines
|
|
2800
|
+
) {
|
|
2801
|
+
const bounds = SchematicSvgRenderer.#resolveOwnerBodyTextCellBounds(
|
|
2802
|
+
text,
|
|
2803
|
+
x,
|
|
2804
|
+
sourceY,
|
|
2805
|
+
fontSize,
|
|
2806
|
+
matchedOwnerPin,
|
|
2807
|
+
ownerTextBodyBounds,
|
|
2808
|
+
lines
|
|
2809
|
+
)
|
|
2810
|
+
|
|
2811
|
+
if (!bounds) {
|
|
2812
|
+
return projectedY
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
return SchematicSvgRenderer.#clampTextBaselineToBounds(
|
|
2816
|
+
text,
|
|
2817
|
+
projectedY,
|
|
2818
|
+
bounds,
|
|
2819
|
+
sheetHeight,
|
|
2820
|
+
fontSize
|
|
2821
|
+
)
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
/**
|
|
2825
|
+
* Converts top-anchored text coordinates into SVG baseline coordinates.
|
|
2826
|
+
* @param {{ verticalAnchor?: 'top' }} text Text primitive.
|
|
2827
|
+
* @param {number} fontSize Viewer font size.
|
|
2828
|
+
* @returns {number}
|
|
2829
|
+
*/
|
|
2830
|
+
static #resolveVerticalAnchorBaselineOffset(text, fontSize) {
|
|
2831
|
+
if (text?.verticalAnchor !== 'top') {
|
|
2832
|
+
return 0
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
return Number.isFinite(fontSize) && fontSize > 0 ? fontSize : 0
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
/**
|
|
2839
|
+
* Resolves the owner body or internal owner body cell containing text.
|
|
2840
|
+
* @param {{ ownerIndex?: string, recordType?: string, rotation?: number }} text Text primitive.
|
|
2841
|
+
* @param {number} x Render X coordinate.
|
|
2842
|
+
* @param {number} sourceY Unprojected source Y coordinate.
|
|
2843
|
+
* @param {number} fontSize Viewer font size.
|
|
2844
|
+
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
|
|
2845
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
2846
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines Line primitives.
|
|
2847
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
2848
|
+
*/
|
|
2849
|
+
static #resolveOwnerBodyTextCellBounds(
|
|
2850
|
+
text,
|
|
2851
|
+
x,
|
|
2852
|
+
sourceY,
|
|
2853
|
+
fontSize,
|
|
2854
|
+
matchedOwnerPin,
|
|
2855
|
+
ownerTextBodyBounds,
|
|
2856
|
+
lines
|
|
2857
|
+
) {
|
|
2858
|
+
const bounds = SchematicSvgRenderer.#resolveOwnerBodyTextBounds(
|
|
2859
|
+
text,
|
|
2860
|
+
x,
|
|
2861
|
+
sourceY,
|
|
2862
|
+
fontSize,
|
|
2863
|
+
matchedOwnerPin,
|
|
2864
|
+
ownerTextBodyBounds
|
|
2865
|
+
)
|
|
2866
|
+
|
|
2867
|
+
if (!bounds) {
|
|
2868
|
+
return null
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
return (
|
|
2872
|
+
SchematicSvgRenderer.#resolveOwnerBodyTextLineCellBounds(
|
|
2873
|
+
text,
|
|
2874
|
+
x,
|
|
2875
|
+
sourceY,
|
|
2876
|
+
fontSize,
|
|
2877
|
+
bounds,
|
|
2878
|
+
lines
|
|
2879
|
+
) || bounds
|
|
2880
|
+
)
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
/**
|
|
2884
|
+
* Resolves the owner body containing one free text primitive.
|
|
2885
|
+
* @param {{ ownerIndex?: string, recordType?: string, rotation?: number }} text Text primitive.
|
|
2886
|
+
* @param {number} x Render X coordinate.
|
|
2887
|
+
* @param {number} sourceY Unprojected source Y coordinate.
|
|
2888
|
+
* @param {number} fontSize Viewer font size.
|
|
2889
|
+
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
|
|
2890
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
2891
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
2892
|
+
*/
|
|
2893
|
+
static #resolveOwnerBodyTextBounds(
|
|
2894
|
+
text,
|
|
2895
|
+
x,
|
|
2896
|
+
sourceY,
|
|
2897
|
+
fontSize,
|
|
2898
|
+
matchedOwnerPin,
|
|
2899
|
+
ownerTextBodyBounds
|
|
2900
|
+
) {
|
|
2901
|
+
if (matchedOwnerPin) return null
|
|
2902
|
+
if (text.recordType !== '4') return null
|
|
2903
|
+
if (SchematicSvgRenderer.#normalizeDegrees(text.rotation) !== 0) {
|
|
2904
|
+
return null
|
|
2905
|
+
}
|
|
2906
|
+
if (!Number.isFinite(fontSize) || fontSize <= 0) return null
|
|
2907
|
+
|
|
2908
|
+
const ownerIndex = String(text.ownerIndex || '').trim()
|
|
2909
|
+
const tolerance = Math.max(fontSize * 0.4, 1)
|
|
2910
|
+
const explicitBounds = ownerIndex
|
|
2911
|
+
? ownerTextBodyBounds.get(ownerIndex)
|
|
2912
|
+
: null
|
|
2913
|
+
|
|
2914
|
+
if (
|
|
2915
|
+
explicitBounds &&
|
|
2916
|
+
SchematicSvgRenderer.#containsPointWithTolerance(
|
|
2917
|
+
explicitBounds,
|
|
2918
|
+
x,
|
|
2919
|
+
sourceY,
|
|
2920
|
+
tolerance
|
|
2921
|
+
)
|
|
2922
|
+
) {
|
|
2923
|
+
return explicitBounds
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
if (ownerIndex) {
|
|
2927
|
+
return null
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
return SchematicSvgRenderer.#findContainingOwnerBodyBounds(
|
|
2931
|
+
ownerTextBodyBounds,
|
|
2932
|
+
x,
|
|
2933
|
+
sourceY,
|
|
2934
|
+
tolerance
|
|
2935
|
+
)
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
/**
|
|
2939
|
+
* Finds the smallest owner body containing one ownerless overlay label.
|
|
2940
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
2941
|
+
* @param {number} x Text X coordinate.
|
|
2942
|
+
* @param {number} y Text baseline Y coordinate.
|
|
2943
|
+
* @param {number} tolerance Coordinate tolerance.
|
|
2944
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
2945
|
+
*/
|
|
2946
|
+
static #findContainingOwnerBodyBounds(
|
|
2947
|
+
ownerTextBodyBounds,
|
|
2948
|
+
x,
|
|
2949
|
+
y,
|
|
2950
|
+
tolerance
|
|
2951
|
+
) {
|
|
2952
|
+
let bestBounds = null
|
|
2953
|
+
let bestArea = Number.POSITIVE_INFINITY
|
|
2954
|
+
|
|
2955
|
+
for (const bounds of ownerTextBodyBounds.values()) {
|
|
2956
|
+
if (
|
|
2957
|
+
!SchematicSvgRenderer.#containsPointWithTolerance(
|
|
2958
|
+
bounds,
|
|
2959
|
+
x,
|
|
2960
|
+
y,
|
|
2961
|
+
tolerance
|
|
2962
|
+
)
|
|
2963
|
+
) {
|
|
2964
|
+
continue
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
const area =
|
|
2968
|
+
Math.max(Number(bounds.maxX) - Number(bounds.minX), 0) *
|
|
2969
|
+
Math.max(Number(bounds.maxY) - Number(bounds.minY), 0)
|
|
2970
|
+
|
|
2971
|
+
if (area < bestArea) {
|
|
2972
|
+
bestArea = area
|
|
2973
|
+
bestBounds = bounds
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
return bestBounds
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
/**
|
|
2981
|
+
* Resolves the internal horizontal-line cell containing one text label.
|
|
2982
|
+
* @param {number} x Text X coordinate.
|
|
2983
|
+
* @param {number} y Text Y coordinate.
|
|
2984
|
+
* @param {number} fontSize Viewer font size.
|
|
2985
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Owner body bounds.
|
|
2986
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines Line primitives.
|
|
2987
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
|
|
2988
|
+
*/
|
|
2989
|
+
static #resolveOwnerBodyTextLineCellBounds(
|
|
2990
|
+
text,
|
|
2991
|
+
x,
|
|
2992
|
+
y,
|
|
2993
|
+
fontSize,
|
|
2994
|
+
bounds,
|
|
2995
|
+
lines
|
|
2996
|
+
) {
|
|
2997
|
+
const separators =
|
|
2998
|
+
SchematicSvgRenderer.#collectOwnerBodyHorizontalSeparators(
|
|
2999
|
+
x,
|
|
3000
|
+
fontSize,
|
|
3001
|
+
bounds,
|
|
3002
|
+
lines
|
|
3003
|
+
)
|
|
3004
|
+
|
|
3005
|
+
if (separators.length === 0) {
|
|
3006
|
+
return null
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
const edges = [
|
|
3010
|
+
Number(bounds.minY),
|
|
3011
|
+
...separators,
|
|
3012
|
+
Number(bounds.maxY)
|
|
3013
|
+
].sort((left, right) => left - right)
|
|
3014
|
+
const tolerance = Math.max(fontSize * 0.4, 1)
|
|
3015
|
+
const descent =
|
|
3016
|
+
fontSize * SchematicSvgRenderer.#resolveBodyTextDescentRatio(text)
|
|
3017
|
+
const sampleY = y + (fontSize * BODY_TEXT_ASCENT_RATIO - descent) / 2
|
|
3018
|
+
|
|
3019
|
+
for (let index = 0; index < edges.length - 1; index += 1) {
|
|
3020
|
+
const minY = edges[index]
|
|
3021
|
+
const maxY = edges[index + 1]
|
|
3022
|
+
|
|
3023
|
+
if (sampleY >= minY && sampleY <= maxY) {
|
|
3024
|
+
return {
|
|
3025
|
+
minX: bounds.minX,
|
|
3026
|
+
maxX: bounds.maxX,
|
|
3027
|
+
minY,
|
|
3028
|
+
maxY
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
for (let index = 0; index < edges.length - 1; index += 1) {
|
|
3034
|
+
const minY = edges[index]
|
|
3035
|
+
const maxY = edges[index + 1]
|
|
3036
|
+
|
|
3037
|
+
if (sampleY >= minY - tolerance && sampleY <= maxY + tolerance) {
|
|
3038
|
+
return {
|
|
3039
|
+
minX: bounds.minX,
|
|
3040
|
+
maxX: bounds.maxX,
|
|
3041
|
+
minY,
|
|
3042
|
+
maxY
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
return null
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
/**
|
|
3051
|
+
* Collects horizontal separators that behave like internal body cell rows.
|
|
3052
|
+
* @param {number} x Text X coordinate.
|
|
3053
|
+
* @param {number} fontSize Viewer font size.
|
|
3054
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Owner body bounds.
|
|
3055
|
+
* @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines Line primitives.
|
|
3056
|
+
* @returns {number[]}
|
|
3057
|
+
*/
|
|
3058
|
+
static #collectOwnerBodyHorizontalSeparators(x, fontSize, bounds, lines) {
|
|
3059
|
+
const tolerance = Math.max(fontSize * 0.4, 1)
|
|
3060
|
+
const bodyWidth = Math.max(Number(bounds.maxX) - Number(bounds.minX), 0)
|
|
3061
|
+
const minSpan = Math.max(
|
|
3062
|
+
bodyWidth * BODY_TEXT_MIN_SEPARATOR_SPAN_RATIO,
|
|
3063
|
+
fontSize * 1.5
|
|
3064
|
+
)
|
|
3065
|
+
const separators = new Set()
|
|
3066
|
+
|
|
3067
|
+
for (const line of lines || []) {
|
|
3068
|
+
const y1 = Number(line?.y1)
|
|
3069
|
+
const y2 = Number(line?.y2)
|
|
3070
|
+
const x1 = Number(line?.x1)
|
|
3071
|
+
const x2 = Number(line?.x2)
|
|
3072
|
+
|
|
3073
|
+
if (
|
|
3074
|
+
!Number.isFinite(x1) ||
|
|
3075
|
+
!Number.isFinite(x2) ||
|
|
3076
|
+
!Number.isFinite(y1) ||
|
|
3077
|
+
!Number.isFinite(y2) ||
|
|
3078
|
+
Math.abs(y1 - y2) > tolerance
|
|
3079
|
+
) {
|
|
3080
|
+
continue
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
const minX = Math.min(x1, x2)
|
|
3084
|
+
const maxX = Math.max(x1, x2)
|
|
3085
|
+
const span = maxX - minX
|
|
3086
|
+
|
|
3087
|
+
if (span < minSpan) continue
|
|
3088
|
+
if (y1 <= bounds.minY + tolerance) continue
|
|
3089
|
+
if (y1 >= bounds.maxY - tolerance) continue
|
|
3090
|
+
if (minX < bounds.minX - tolerance) continue
|
|
3091
|
+
if (maxX > bounds.maxX + tolerance) continue
|
|
3092
|
+
if (x < minX - tolerance || x > maxX + tolerance) continue
|
|
3093
|
+
|
|
3094
|
+
separators.add(Number(formatNumber(y1)))
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
return [...separators].sort((left, right) => left - right)
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
/**
|
|
3101
|
+
* Clamps a projected SVG text baseline so its visual box stays in bounds.
|
|
3102
|
+
* @param {{ text?: string, resolvedText?: string }} text Text primitive.
|
|
3103
|
+
* @param {number} y Candidate SVG baseline.
|
|
3104
|
+
* @param {{ minY: number, maxY: number }} bounds Unprojected source bounds.
|
|
3105
|
+
* @param {number} sheetHeight Rendered sheet height.
|
|
3106
|
+
* @param {number} fontSize Viewer font size.
|
|
3107
|
+
* @returns {number}
|
|
3108
|
+
*/
|
|
3109
|
+
static #clampTextBaselineToBounds(text, y, bounds, sheetHeight, fontSize) {
|
|
3110
|
+
const top = projectSchematicY(sheetHeight, bounds.maxY)
|
|
3111
|
+
const bottom = projectSchematicY(sheetHeight, bounds.minY)
|
|
3112
|
+
const ascent = fontSize * BODY_TEXT_ASCENT_RATIO
|
|
3113
|
+
const descent =
|
|
3114
|
+
fontSize * SchematicSvgRenderer.#resolveBodyTextDescentRatio(text)
|
|
3115
|
+
const visualHeight = ascent + descent
|
|
3116
|
+
|
|
3117
|
+
if (bottom - top < visualHeight) {
|
|
3118
|
+
return (top + bottom) / 2 + (ascent - descent) / 2
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
let resolvedY = y
|
|
3122
|
+
const visualTop = resolvedY - ascent
|
|
3123
|
+
if (visualTop < top) {
|
|
3124
|
+
resolvedY += top - visualTop
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
const visualBottom = resolvedY + descent
|
|
3128
|
+
if (visualBottom > bottom) {
|
|
3129
|
+
resolvedY -= visualBottom - bottom
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
return resolvedY
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
/**
|
|
3136
|
+
* Estimates the descent needed for body-cell text clamping.
|
|
3137
|
+
* @param {{ text?: string, resolvedText?: string }} text Text primitive.
|
|
3138
|
+
* @returns {number}
|
|
3139
|
+
*/
|
|
3140
|
+
static #resolveBodyTextDescentRatio(text) {
|
|
3141
|
+
const value = String(text?.resolvedText ?? text?.text ?? '')
|
|
3142
|
+
|
|
3143
|
+
return BODY_TEXT_DESCENDER_PATTERN.test(value)
|
|
3144
|
+
? BODY_TEXT_DESCENT_RATIO
|
|
3145
|
+
: BODY_TEXT_FLAT_DESCENT_RATIO
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
/**
|
|
3149
|
+
* Returns true when a point falls inside bounds with tolerance.
|
|
3150
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} bounds Bounds.
|
|
3151
|
+
* @param {number} x X coordinate.
|
|
3152
|
+
* @param {number} y Y coordinate.
|
|
3153
|
+
* @param {number} tolerance Coordinate tolerance.
|
|
3154
|
+
* @returns {boolean}
|
|
3155
|
+
*/
|
|
3156
|
+
static #containsPointWithTolerance(bounds, x, y, tolerance) {
|
|
3157
|
+
const numericX = Number(x)
|
|
3158
|
+
const numericY = Number(y)
|
|
3159
|
+
|
|
3160
|
+
return (
|
|
3161
|
+
Number.isFinite(numericX) &&
|
|
3162
|
+
Number.isFinite(numericY) &&
|
|
3163
|
+
numericX >= bounds.minX - tolerance &&
|
|
3164
|
+
numericX <= bounds.maxX + tolerance &&
|
|
3165
|
+
numericY >= bounds.minY - tolerance &&
|
|
3166
|
+
numericY <= bounds.maxY + tolerance
|
|
3167
|
+
)
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
/**
|
|
3171
|
+
* Adds the SVG baseline correction for mirrored owner-local symbol text
|
|
3172
|
+
* authored with the native orientation that stores the top of the text run.
|
|
3173
|
+
* Baseline-oriented mirrored labels already use SVG-like y coordinates.
|
|
3174
|
+
* @param {{ ownerIndex?: string, recordType?: string, rotation?: number, sourceOrientation?: number, isMirrored?: boolean }} text Text primitive.
|
|
3175
|
+
* @param {number} x Render X coordinate.
|
|
3176
|
+
* @param {number} sourceY Unprojected source Y coordinate.
|
|
3177
|
+
* @param {number} fontSize Viewer font size.
|
|
3178
|
+
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
|
|
3179
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
3180
|
+
* @returns {number}
|
|
3181
|
+
*/
|
|
3182
|
+
static #resolveOwnerBodyTextBaselineOffset(
|
|
3183
|
+
text,
|
|
3184
|
+
x,
|
|
3185
|
+
sourceY,
|
|
3186
|
+
fontSize,
|
|
3187
|
+
matchedOwnerPin,
|
|
3188
|
+
ownerTextBodyBounds
|
|
3189
|
+
) {
|
|
3190
|
+
if (
|
|
3191
|
+
!SchematicSvgRenderer.#isOwnerBodyBaselineText(
|
|
3192
|
+
text,
|
|
3193
|
+
x,
|
|
3194
|
+
sourceY,
|
|
3195
|
+
fontSize,
|
|
3196
|
+
matchedOwnerPin,
|
|
3197
|
+
ownerTextBodyBounds
|
|
3198
|
+
)
|
|
3199
|
+
) {
|
|
3200
|
+
return 0
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
return fontSize
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
/**
|
|
3207
|
+
* Returns true when one text primitive is a top-origin mirrored owner-local
|
|
3208
|
+
* symbol annotation sitting inside its owner's drawable body.
|
|
3209
|
+
* @param {{ ownerIndex?: string, recordType?: string, rotation?: number, sourceOrientation?: number, isMirrored?: boolean }} text Text primitive.
|
|
3210
|
+
* @param {number} x Render X coordinate.
|
|
3211
|
+
* @param {number} sourceY Unprojected source Y coordinate.
|
|
3212
|
+
* @param {number} fontSize Viewer font size.
|
|
3213
|
+
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin Matched pin label, if any.
|
|
3214
|
+
* @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerTextBodyBounds Owner body bounds keyed by owner index.
|
|
3215
|
+
* @returns {boolean}
|
|
3216
|
+
*/
|
|
3217
|
+
static #isOwnerBodyBaselineText(
|
|
3218
|
+
text,
|
|
3219
|
+
x,
|
|
3220
|
+
sourceY,
|
|
3221
|
+
fontSize,
|
|
3222
|
+
matchedOwnerPin,
|
|
3223
|
+
ownerTextBodyBounds
|
|
3224
|
+
) {
|
|
3225
|
+
if (matchedOwnerPin) return false
|
|
3226
|
+
if (text.recordType !== '4') return false
|
|
3227
|
+
if (!text.isMirrored) return false
|
|
3228
|
+
if (Number(text.sourceOrientation ?? 0) !== 2) return false
|
|
3229
|
+
if (SchematicSvgRenderer.#normalizeDegrees(text.rotation) !== 0) {
|
|
3230
|
+
return false
|
|
3231
|
+
}
|
|
3232
|
+
if (!Number.isFinite(fontSize) || fontSize <= 0) return false
|
|
3233
|
+
|
|
3234
|
+
const ownerIndex = String(text.ownerIndex || '').trim()
|
|
3235
|
+
if (!ownerIndex) return false
|
|
3236
|
+
|
|
3237
|
+
const bounds = ownerTextBodyBounds.get(ownerIndex)
|
|
3238
|
+
if (!bounds) return false
|
|
3239
|
+
|
|
3240
|
+
const numericX = Number(x)
|
|
3241
|
+
const numericY = Number(sourceY)
|
|
3242
|
+
const tolerance = Math.max(fontSize * 0.4, 1)
|
|
3243
|
+
|
|
3244
|
+
return (
|
|
3245
|
+
Number.isFinite(numericX) &&
|
|
3246
|
+
Number.isFinite(numericY) &&
|
|
3247
|
+
numericX >= bounds.minX - tolerance &&
|
|
3248
|
+
numericX <= bounds.maxX + tolerance &&
|
|
3249
|
+
numericY >= bounds.minY - tolerance &&
|
|
3250
|
+
numericY <= bounds.maxY + tolerance
|
|
3251
|
+
)
|
|
3252
|
+
}
|
|
3253
|
+
|
|
2380
3254
|
/**
|
|
2381
3255
|
* Lifts large section headings clear of authored dash-dot frame baselines.
|
|
2382
3256
|
* @param {{ x: number, y: number, ownerIndex?: string, recordType?: string, fontSize?: number, rotation?: number }} text
|
|
@@ -2475,11 +3349,18 @@ export class SchematicSvgRenderer {
|
|
|
2475
3349
|
|
|
2476
3350
|
/**
|
|
2477
3351
|
* Builds one schematic cross marker.
|
|
2478
|
-
* @param {{ x: number, y: number, size: number, color: string }} cross
|
|
3352
|
+
* @param {{ x: number, y: number, size: number, color: string, symbolName?: string }} cross
|
|
2479
3353
|
* @param {number} sheetHeight
|
|
2480
3354
|
* @returns {string}
|
|
2481
3355
|
*/
|
|
2482
3356
|
static #buildSchematicCrossMarkup(cross, sheetHeight) {
|
|
3357
|
+
if (String(cross.symbolName || '').toLowerCase() === 'checkbox') {
|
|
3358
|
+
return SchematicSvgRenderer.#buildSchematicCheckboxMarkup(
|
|
3359
|
+
cross,
|
|
3360
|
+
sheetHeight
|
|
3361
|
+
)
|
|
3362
|
+
}
|
|
3363
|
+
|
|
2483
3364
|
const x = cross.x
|
|
2484
3365
|
const y = projectSchematicY(sheetHeight, cross.y)
|
|
2485
3366
|
const half = Math.max(Number(cross.size || 6), 4) / 2
|
|
@@ -2519,6 +3400,64 @@ export class SchematicSvgRenderer {
|
|
|
2519
3400
|
)
|
|
2520
3401
|
}
|
|
2521
3402
|
|
|
3403
|
+
/**
|
|
3404
|
+
* Builds one checkbox-shaped no-ERC marker.
|
|
3405
|
+
* @param {{ x: number, y: number, size: number, color: string }} cross
|
|
3406
|
+
* @param {number} sheetHeight
|
|
3407
|
+
* @returns {string}
|
|
3408
|
+
*/
|
|
3409
|
+
static #buildSchematicCheckboxMarkup(cross, sheetHeight) {
|
|
3410
|
+
const x = Number(cross.x)
|
|
3411
|
+
const y = projectSchematicY(sheetHeight, cross.y)
|
|
3412
|
+
const size = Math.max(Number(cross.size || 6), 4)
|
|
3413
|
+
const left = x - 1
|
|
3414
|
+
const top = y - size - 7
|
|
3415
|
+
const color = escapeHtml(
|
|
3416
|
+
SchematicColorResolver.resolveColor(
|
|
3417
|
+
cross.color,
|
|
3418
|
+
'--schematic-alert-color'
|
|
3419
|
+
)
|
|
3420
|
+
)
|
|
3421
|
+
|
|
3422
|
+
return (
|
|
3423
|
+
'<g class="schematic-cross schematic-cross--checkbox"><line x1="' +
|
|
3424
|
+
formatNumber(x) +
|
|
3425
|
+
'" y1="' +
|
|
3426
|
+
formatNumber(y) +
|
|
3427
|
+
'" x2="' +
|
|
3428
|
+
formatNumber(left) +
|
|
3429
|
+
'" y2="' +
|
|
3430
|
+
formatNumber(top + size) +
|
|
3431
|
+
'" stroke="' +
|
|
3432
|
+
color +
|
|
3433
|
+
'" /><rect x="' +
|
|
3434
|
+
formatNumber(left) +
|
|
3435
|
+
'" y="' +
|
|
3436
|
+
formatNumber(top) +
|
|
3437
|
+
'" width="' +
|
|
3438
|
+
formatNumber(size) +
|
|
3439
|
+
'" height="' +
|
|
3440
|
+
formatNumber(size) +
|
|
3441
|
+
'" fill="none" stroke="' +
|
|
3442
|
+
color +
|
|
3443
|
+
'" /><polyline points="' +
|
|
3444
|
+
formatNumber(left + size * 0.2) +
|
|
3445
|
+
',' +
|
|
3446
|
+
formatNumber(top + size * 0.5) +
|
|
3447
|
+
' ' +
|
|
3448
|
+
formatNumber(left + size * 0.5) +
|
|
3449
|
+
',' +
|
|
3450
|
+
formatNumber(top + size * 0.8) +
|
|
3451
|
+
' ' +
|
|
3452
|
+
formatNumber(left + size * 0.9666666667) +
|
|
3453
|
+
',' +
|
|
3454
|
+
formatNumber(top + size * 0.2) +
|
|
3455
|
+
'" fill="none" stroke="' +
|
|
3456
|
+
color +
|
|
3457
|
+
'" /></g>'
|
|
3458
|
+
)
|
|
3459
|
+
}
|
|
3460
|
+
|
|
2522
3461
|
/**
|
|
2523
3462
|
* Builds one authored schematic junction dot.
|
|
2524
3463
|
* @param {{ x: number, y: number, color: string }} junction
|
|
@@ -2567,30 +3506,13 @@ export class SchematicSvgRenderer {
|
|
|
2567
3506
|
static #resolveAuthoredSchematicJunctionColor(junction, lines) {
|
|
2568
3507
|
const connectedLine = lines.find(
|
|
2569
3508
|
(line) =>
|
|
2570
|
-
|
|
3509
|
+
SchematicLineColorResolver.isElectricalLine(line) &&
|
|
2571
3510
|
SchematicSvgRenderer.#schematicLineContainsPoint(line, junction)
|
|
2572
3511
|
)
|
|
2573
3512
|
|
|
2574
3513
|
return connectedLine?.color || junction.color
|
|
2575
3514
|
}
|
|
2576
3515
|
|
|
2577
|
-
/**
|
|
2578
|
-
* Returns true when one normalized line can carry schematic net color.
|
|
2579
|
-
* @param {{ ownerIndex?: string, isBus?: boolean, recordType?: string } | null | undefined} line
|
|
2580
|
-
* @returns {boolean}
|
|
2581
|
-
*/
|
|
2582
|
-
static #isElectricalSchematicLine(line) {
|
|
2583
|
-
if (line?.ownerIndex || line?.isBus === true) {
|
|
2584
|
-
return false
|
|
2585
|
-
}
|
|
2586
|
-
|
|
2587
|
-
if (!Object.prototype.hasOwnProperty.call(line || {}, 'recordType')) {
|
|
2588
|
-
return true
|
|
2589
|
-
}
|
|
2590
|
-
|
|
2591
|
-
return !['6', '7', '26'].includes(String(line.recordType || ''))
|
|
2592
|
-
}
|
|
2593
|
-
|
|
2594
3516
|
/**
|
|
2595
3517
|
* Returns true when one schematic line segment contains the given point.
|
|
2596
3518
|
* @param {{ x1: number, y1: number, x2: number, y2: number }} line
|