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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Schematic groups

- Declare PCB blocks with component properties: `PCB Block`, `PCB Module`, `PCB Attach` and `PCB Role`. Circuit assembly writes `PCB Block` from each component's `block_name` on both assembly paths, per instance and keeping other properties; a failed write is logged and never fails assembly or skips a component. `get_schematic_groups` returns `declared_blocks`, `declared_modules` and `declaration_errors` next to the heuristic groups, merging a block across pages in full mode; conflicting block attributes are reported and omitted, and a package whose sections are declared in different blocks is reported and left out of them. Copies of a component already on the page never inherit its PCB declarations. Without declarations the result is unchanged. PCB validation receives the declarations separately from heuristic groups, so declaration problems never mark those incomplete.

### PCB layout

- Honor explicit `PCB_LAYOUT_SUBTREE_WORKERS`, `PCB_BOARD_PACKER_THREADS` and `PCB_POST_PLACE_THREADS` values up to the number of logical cores instead of clamping them to the default. The default stays at half the logical cores, at most 8. Requires a backend that applies the same rule.
Expand Down
11 changes: 11 additions & 0 deletions extension/src/eda/assemble-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { normalizeMultipartSourceDesignators } from "./multipart-source-designat
import { LEGACY_PORT_LENGTHS, matchingWirePort, wirePortLabel, wirePortLengths, wirePortMinLength } from './wire-port-layout';
import { formatUnresolvedNetPortMessage, type UnresolvedNetPortSample } from './unresolved-net-port-message';
import { readPartUuidFromPrimitive, storePartUuidOnPrimitive } from './component-part-ref';
import { assemblyPcbBlock, PCB_BLOCK_PROPERTY_KEYS, storePcbBlockOnPrimitive } from './pcb-block-properties';
import { samePartUuid } from '@copilot/shared/types/lcsc';
import { DRAWING_LIBRARY_UUID, DRAWING_SHEETS, selectDrawingSheet } from './schematic-page-geometry';

Expand Down Expand Up @@ -572,6 +573,9 @@ function cloneComponentsIntoSource(
for (const attributeTemplate of template.attributes) {
const attribute = cloneSourceRecord(attributeTemplate);
if (!attribute.inner) continue;
// The template may be another component on the page; its PCB
// declarations are not this instance's. finalize writes PCB Block.
if (PCB_BLOCK_PROPERTY_KEYS.has(String(attribute.inner.key))) continue;

attribute.outer.id = makeSourceId();
attribute.outer.ticket = ++ticket;
Expand Down Expand Up @@ -2004,11 +2008,14 @@ async function setSourceAndRefresh(source: string, label: string): Promise<void>

async function finalizeSourceComponents(plans: PlannedComponent[], signal?: AbortSignal): Promise<void> {
const named = new Map<string, string>();
const blocks = new Map<string, string>();
const ids = new Set<string>();
for (const plan of plans) {
if (!plan.primitiveId) continue;
ids.add(plan.primitiveId);
if (isNamedNetSymbol(plan.input)) named.set(plan.primitiveId, getSpecialSignalName(plan.input));
// Each instance gets its own block: clones inherit the seed's properties.
else { const block = assemblyPcbBlock(plan.input); if (block) blocks.set(plan.primitiveId, block); }
}

let completed = 0;
Expand All @@ -2031,6 +2038,10 @@ async function finalizeSourceComponents(plans: PlannedComponent[], signal?: Abor
'Global Net Name': net,
});
}
const block = blocks.get(id);
if (block && !storePcbBlockOnPrimitive(editable, block)) {
eda.sys_Log.add(`[source-assemble] PCB Block property not written for ${id}`, ESYS_LogType.WARNING);
}
await withTimeout(editable.done(), 10_000, `Timed out finalizing component ${id}`, signal);
completed++;
} catch (error) {
Expand Down
3 changes: 3 additions & 0 deletions extension/src/eda/assemble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import PQueue from 'p-queue';
import { runAssemblyQueueTask } from './assembly-queue';
import { getPartLibraryUuid, getPartUuid } from '@copilot/shared/types/lcsc';
import { storePartUuidOnPrimitive } from './component-part-ref';
import { assemblyPcbBlock, storePcbBlockOnPrimitive } from './pcb-block-properties';

const assembleQueue = new PQueue({ concurrency: 1 });
const COPILOT_BLOCK_COLOR = "#808080";
Expand Down Expand Up @@ -95,6 +96,8 @@ async function createComponent(component: CircuitAssembly['components'][0], offs

comp = comp.setState_Designator(rmPartFromDesignator(designator));
storePartUuidOnPrimitive(comp, partUuid);
const block = assemblyPcbBlock(component);
if (block && !storePcbBlockOnPrimitive(comp, block)) eda.sys_Log.add(`PCB Block property not written for ${designator}`, ESYS_LogType.WARNING);
}

eda.sys_Log.add(`Place component ${designator} ${JSON.stringify(partUuid)} at ${x} ${y} rot: ${pos.rotate}`);
Expand Down
51 changes: 51 additions & 0 deletions extension/src/eda/pcb-block-properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/** Component properties that declare the PCB block hierarchy in the schematic.
* Components without `PCB Block` keep the heuristic grouping. */
export const PCB_BLOCK_PROPERTIES = {
block: 'PCB Block',
module: 'PCB Module',
attach: 'PCB Attach',
role: 'PCB Role',
} as const;

/** Declarations belong to one instance: copies of a template must not inherit them. */
export const PCB_BLOCK_PROPERTY_KEYS: ReadonlySet<string> = new Set(Object.values(PCB_BLOCK_PROPERTIES));

export interface PcbDeclaration { block: string; module?: string; attach?: string; role?: string }

type PropertyPrimitive = {
getState_OtherProperty?(): Record<string, unknown> | null | undefined;
setState_OtherProperty?(otherProperty: Record<string, string | number | boolean>): unknown;
};

const text = (value: unknown) => typeof value === 'string' && value.trim() ? value.trim() : undefined;

/** User-edited values: anything but a non-empty string is treated as absent. */
export function readPcbDeclaration(properties: Record<string, unknown> | null | undefined): PcbDeclaration | undefined {
const block = text(properties?.[PCB_BLOCK_PROPERTIES.block]);
if (!block) return undefined;
const declaration: PcbDeclaration = { block };
for (const key of ['module', 'attach', 'role'] as const) {
const value = text(properties?.[PCB_BLOCK_PROPERTIES[key]]);
if (value) declaration[key] = value;
}
return declaration;
}

/** Block written during assembly. Net symbols and internal assembly names get none. */
export function assemblyPcbBlock(component: { designator: string; block_name?: string }): string | undefined {
const name = text(component.block_name);
return name && !name.startsWith('__') && !component.designator.includes('|') ? name : undefined;
}

/** Never throws: a failed write must not fail assembly or skip the component.
* Other properties are kept; when they cannot be read, nothing is written. */
export function storePcbBlockOnPrimitive(primitive: PropertyPrimitive, block: string | undefined): boolean {
if (!block) return false;
try {
if (!primitive.setState_OtherProperty || !primitive.getState_OtherProperty) return false;
const properties = primitive.getState_OtherProperty() ?? {};
if (properties[PCB_BLOCK_PROPERTIES.block] === block) return true;
primitive.setState_OtherProperty({ ...properties, [PCB_BLOCK_PROPERTIES.block]: block } as Record<string, string | number | boolean>);
return true;
} catch { return false; }
}
Loading
Loading