Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export * from "./records/altium-track-record"
export * from "./records/altium-unknown-record"
export * from "./records/altium-via-record"
export * from "./schematic-index"
export * from "./schematic-parameter-reference"
export * from "./serialization/altium-serialization"
export * from "./serialization/serialize-altium-binary-documents"
export * from "./source-location"
Expand Down
170 changes: 170 additions & 0 deletions lib/schematic-parameter-reference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import type { AltiumPrjPcb } from "./altium-prj-pcb"
import type { AltiumSchDoc } from "./altium-sch-doc"

type SchematicParameterName = string

interface CachedSchematicParameters {
parameters: Map<SchematicParameterName, string>
revision: number
}

export interface ResolveSchematicParameterReferenceInput {
document: AltiumSchDoc
/** Current schematic filename, including its extension. */
documentName?: string
/** Parsed project that supplies user-defined project parameters. */
project?: AltiumPrjPcb
/** Current project filename, including its extension. */
projectName?: string
reference: string
}

interface ResolveParameterInput {
parameterName: SchematicParameterName
parameters: ReadonlyMap<SchematicParameterName, string>
visitedParameterNames: Set<SchematicParameterName>
}

const PARAMETER_REFERENCE = /^=([A-Za-z][A-Za-z0-9_]*)$/u
const DOCUMENT_PARAMETER_CACHE = new WeakMap<
AltiumSchDoc,
CachedSchematicParameters
>()
const PROJECT_PARAMETER_CACHE = new WeakMap<
AltiumPrjPcb,
CachedSchematicParameters
>()

/**
* Resolves an Altium `=ParameterName` reference against document-level
* schematic parameters.
*/
export function resolveSchematicParameterReference(
document: AltiumSchDoc,
reference: string,
): string | undefined {
return resolveSchematicParameterReferenceWithContext({
document,
reference,
})
}

export function resolveSchematicParameterReferenceWithContext({
document,
documentName,
project,
projectName,
reference,
}: ResolveSchematicParameterReferenceInput): string | undefined {
const match = PARAMETER_REFERENCE.exec(reference)
const parameterName = match?.[1]
if (!parameterName) return undefined

const parameters = new Map<SchematicParameterName, string>(
project ? getSchematicProjectParameters(project) : [],
)
for (const [name, text] of getSchematicDocumentParameters(document)) {
parameters.set(name, text)
}
if (projectName) parameters.set("projectname", projectName)
if (documentName) parameters.set("documentname", documentName)

return resolveParameter({
parameterName,
parameters,
visitedParameterNames: new Set(),
})
}

function getSchematicDocumentParameters(
document: AltiumSchDoc,
): Map<SchematicParameterName, string> {
const cached = DOCUMENT_PARAMETER_CACHE.get(document)
if (cached?.revision === document.revision) return cached.parameters

const parameters = new Map<SchematicParameterName, string>()
for (const record of document.records) {
if (
record.recordKind !== "41" ||
record.getBoolean("ISHIDDEN") !== true ||
document.getParent(record) !== undefined
) {
continue
}

const name = record.getDecoded("NAME")
const parameterText = record.getDecoded("TEXT")
if (name && parameterText !== undefined) {
parameters.set(name.toLowerCase(), parameterText)
}
}

DOCUMENT_PARAMETER_CACHE.set(document, {
parameters,
revision: document.revision,
})
return parameters
}

function getSchematicProjectParameters(
project: AltiumPrjPcb,
): Map<SchematicParameterName, string> {
const cached = PROJECT_PARAMETER_CACHE.get(project)
if (cached?.revision === project.revision) return cached.parameters

const parameters = new Map<SchematicParameterName, string>()
for (const section of project.sections) {
if (/^PARAMETER\d+$/iu.test(section.name)) {
const parameterName = section.entries.find(
(entry) => entry.key.toUpperCase() === "NAME",
)?.value
const parameterText = section.entries.find(
(entry) => entry.key.toUpperCase() === "VALUE",
)?.value
if (parameterName && parameterText !== undefined) {
parameters.set(parameterName.toLowerCase(), parameterText)
}
continue
}

if (!/^PARAMETERS?$/iu.test(section.name)) continue
for (const entry of section.entries) {
const separatorIndex = entry.value.indexOf("=")
if (separatorIndex <= 0) continue
const parameterName = entry.value.slice(0, separatorIndex).trim()
const parameterText = entry.value.slice(separatorIndex + 1)
if (parameterName) {
parameters.set(parameterName.toLowerCase(), parameterText)
}
}
}

PROJECT_PARAMETER_CACHE.set(project, {
parameters,
revision: project.revision,
})
return parameters
}

function resolveParameter({
parameterName,
parameters,
visitedParameterNames,
}: ResolveParameterInput): string | undefined {
const normalizedName = parameterName.toLowerCase()
if (visitedParameterNames.has(normalizedName)) return undefined

const parameterText = parameters.get(normalizedName)
if (parameterText === undefined || parameterText === "*") return undefined

const nestedReference = PARAMETER_REFERENCE.exec(parameterText)?.[1]
if (!nestedReference) return parameterText

const nextVisitedParameterNames = new Set(visitedParameterNames)
nextVisitedParameterNames.add(normalizedName)
return resolveParameter({
parameterName: nestedReference,
parameters,
visitedParameterNames: nextVisitedParameterNames,
})
}
29 changes: 28 additions & 1 deletion lib/svg-serialization/serialize-altium-sheet-to-svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
AltiumSchImageRecord,
type AltiumSchSheetRecord,
} from "../records/altium-schematic-records"
import { resolveSchematicParameterReferenceWithContext } from "../schematic-parameter-reference"
import {
altiumColorToCss,
getSchematicCoordinate,
Expand Down Expand Up @@ -248,11 +249,21 @@ function renderSchematicRecord(
const location = getSchematicLocation(record)
const x = viewport.toX(location.x)
const y = viewport.toY(location.y)
const text =
const sourceText =
record.getDecoded("TEXT") ??
record.getDecoded("NAME") ??
record.getDecoded("DESIGNATOR") ??
""
const text =
context.document && !hasSchematicComponentAncestor(record, context)
? (resolveSchematicParameterReferenceWithContext({
document: context.document,
documentName: options.documentName,
project: options.project,
projectName: options.projectName,
reference: sourceText,
}) ?? sourceText)
: sourceText
if (!text) return undefined
const font = getSchematicFont(record, context.sheetRecord, 9)
const positioning = getSchematicTextPositioning(record)
Expand Down Expand Up @@ -311,6 +322,22 @@ function renderSchematicRecord(
return undefined
}

function hasSchematicComponentAncestor(
record: AltiumRecord,
context: SchematicRenderContext,
): boolean {
let current = context.document?.getParent(record)
const visited = new Set<AltiumRecord>()

while (current && !visited.has(current)) {
if (current.recordKind === "1") return true
visited.add(current)
current = context.document?.getParent(current)
}

return false
}

function renderSchematicRectangle(
record: AltiumRecord,
viewport: SvgViewport,
Expand Down
8 changes: 8 additions & 0 deletions lib/svg-serialization/svg-types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { AltiumPrjPcb } from "../altium-prj-pcb"

export interface SvgPoint {
x: number
y: number
Expand Down Expand Up @@ -43,6 +45,12 @@ export interface AltiumPcbSvgOptions extends AltiumSvgRenderOptions {
}

export interface AltiumSheetSvgOptions extends AltiumSvgRenderOptions {
/** Current schematic filename, including its extension. */
documentName?: string
/** Parsed project that supplies user-defined project parameters. */
project?: AltiumPrjPcb
/** Current project filename, including its extension. */
projectName?: string
showBorder?: boolean
}

Expand Down
14 changes: 7 additions & 7 deletions tests/svg/__snapshots__/elk-pi-main-sheet.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions tests/svg/__snapshots__/schematic-parameter-reference.snap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions tests/svg/schematic-parameter-reference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect, test } from "bun:test"
import {
parseAltiumSchDoc,
resolveSchematicParameterReference,
serializeAltiumSheetToSvg,
} from "../../lib"

test("renders schematic document parameter references as their values", async () => {
const source = [
"|HEADER=Protel for Windows - Schematic Capture Ascii File Version 5.0",
"|RECORD=31|FONTIDCOUNT=1|SIZE1=10|FONTNAME1=Arial|CUSTOMX=240|CUSTOMY=140",
"|RECORD=39|FILENAME=title-block.SchDot",
"|RECORD=4|OWNERINDEX=1|LOCATION.X=20|LOCATION.Y=110|FONTID=1|TEXT==title",
"|RECORD=4|OWNERINDEX=1|LOCATION.X=20|LOCATION.Y=80|FONTID=1|TEXT==Revision",
"|RECORD=4|OWNERINDEX=1|LOCATION.X=20|LOCATION.Y=50|FONTID=1|TEXT==DocumentNumber",
"|RECORD=4|OWNERINDEX=1|LOCATION.X=20|LOCATION.Y=20|FONTID=1|TEXT==ProjectName",
"|RECORD=41|OWNERINDEX=-1|ISHIDDEN=T|NAME=Title|TEXT=Power Distribution",
"|RECORD=41|OWNERINDEX=-1|ISHIDDEN=T|NAME=Revision|TEXT=B",
"|RECORD=41|OWNERINDEX=-1|ISHIDDEN=T|NAME=DocumentNumber|TEXT==SheetNumber",
"|RECORD=41|OWNERINDEX=-1|ISHIDDEN=T|NAME=SheetNumber|TEXT=7",
].join("\n")
const document = parseAltiumSchDoc(source)
const svg = serializeAltiumSheetToSvg(document, {
title: "Resolved schematic parameter references",
})

expect(resolveSchematicParameterReference(document, "=TITLE")).toBe(
"Power Distribution",
)
expect(svg).toContain(">Power Distribution</text>")
expect(svg).toContain(">B</text>")
expect(svg).toContain(">7</text>")
expect(svg).toContain(">=ProjectName</text>")
expect(svg).not.toContain(">=title</text>")
await expect(svg).toMatchSvgSnapshot(import.meta.path)
})
Loading