Skip to content

Commit fbdf677

Browse files
committed
feat: add default library items
1 parent ea7c10c commit fbdf677

4 files changed

Lines changed: 242 additions & 4 deletions

File tree

src/AppNavigation.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,7 +541,7 @@
541541
Add from library
542542
<Icon class="inline-flex" icon="mdi:chevron-right" />
543543
</DropdownItem>
544-
<Dropdown class="min-w-44 z-20" placement="right-start" trigger="hover">
544+
<Dropdown class="min-w-44 z-20" placement="right-start" simple trigger="hover">
545545
{#each getLibraryStoreValue().toSorted( (a, b) => a.name.localeCompare(b.name) ) as libraryItem}
546546
<DropdownItem onclick={() => addItemFromLibrary(libraryItem)}>
547547
{libraryItem.name} ({libraryItem.type})

src/components/modal/ModalLibrary.svelte

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
66
import FileInput from '$components/base/input/FileInput.svelte';
77
import { virtualDownload } from '$lib/download';
8-
import { libraryStore } from '$stores/libraryStore';
9-
import { showModalNameEdit } from '$stores/modalStore';
8+
import { libraryStore, loadDefaultsToLibrary } from '$stores/libraryStore';
9+
import { showModalConfirm, showModalNameEdit } from '$stores/modalStore';
1010
import { Library, LibraryItem } from '$types/Library';
1111
1212
import EscapeClose from './util/EscapeClose.svelte';
@@ -32,14 +32,25 @@
3232
item === libraryItem ? { ...item, name } : item
3333
);
3434
};
35+
36+
const clearLibrary = async () => {
37+
const { confirmed } = await showModalConfirm(
38+
'Are you sure you want to clear the entire library? This action cannot be undone.'
39+
);
40+
if (confirmed) $libraryStore = [];
41+
};
3542
</script>
3643

3744
<EscapeClose on:escape={resolve}>
3845
<Modal dismissable={false} open={true} size="lg" title="Library">
3946
<div class="grid">
4047
<ButtonGroup class="justify-self-end">
48+
<Button onclick={loadDefaultsToLibrary}>Load defaults</Button>
4149
<Button onclick={importLibrary}>Import</Button>
4250
<Button disabled={$libraryStore.length === 0} onclick={exportLibrary}>Export</Button>
51+
<Button color="red" disabled={$libraryStore.length === 0} onclick={clearLibrary}
52+
>Clear</Button
53+
>
4354
</ButtonGroup>
4455
</div>
4556
<div class="grid grid-cols-3 gap-2 pt-4">

src/stores/libraryStore.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@
3737
import { get, type Updater } from 'svelte/store';
3838
import { persisted } from 'svelte-persisted-store';
3939

40-
import type { Library } from '$types/Library';
40+
import {
41+
convertDefaultComponentToLibraryItem,
42+
DEFAULT_COMPONENTS,
43+
type Library
44+
} from '$types/Library';
4145

4246
/**
4347
* Persisted store holding array of saved component templates
@@ -64,3 +68,38 @@ export const setLibraryStoreValue = (library: Library) => libraryStore.set(libra
6468
* @param updater - Function that receives current library and returns updated library
6569
*/
6670
export const updateLibraryStoreValue = (updater: Updater<Library>) => libraryStore.update(updater);
71+
72+
/**
73+
* Loads default THT components into the library
74+
*
75+
* Appends predefined common components (DIP sockets, capacitors, buzzers, etc.)
76+
* to the existing library. Performs duplicate detection by name - components whose
77+
* names already exist in the library are automatically skipped.
78+
*
79+
* **Behavior:**
80+
* - Preserves all existing library items
81+
* - Only adds defaults with names not already present
82+
* - Uses O(1) Set lookup for efficient duplicate checking
83+
* - Safe to call multiple times (won't create duplicates)
84+
*
85+
* **Use Case:**
86+
* Called when user clicks "Load defaults" button in Library modal to quickly
87+
* populate their library with standard component footprints.
88+
*
89+
* @see DEFAULT_COMPONENTS in Library.ts for the list of default components
90+
* @see convertDefaultComponentToLibraryItem for dimension mapping logic
91+
*/
92+
export const loadDefaultsToLibrary = () => {
93+
const currentLibrary = getLibraryStoreValue();
94+
95+
// Build set of existing names for O(1) duplicate checking
96+
const existingNames = new Set(currentLibrary.map((item) => item.name));
97+
98+
// Filter out defaults that already exist, then convert to LibraryItem format
99+
const defaultsToAdd = DEFAULT_COMPONENTS.filter(
100+
(component) => !existingNames.has(component.name)
101+
).map((component) => convertDefaultComponentToLibraryItem(component));
102+
103+
// Append defaults to existing library
104+
if (defaultsToAdd.length > 0) setLibraryStoreValue([...currentLibrary, ...defaultsToAdd]);
105+
};

src/types/Library.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,191 @@ export type LibraryItem = z.infer<typeof LibraryItem>;
6565
*/
6666
export const Library = z.array(LibraryItem);
6767
export type Library = z.infer<typeof Library>;
68+
69+
/**
70+
* Component shape discriminator for default components
71+
*/
72+
export type ComponentShape = 'rect' | 'circle';
73+
74+
/**
75+
* Default component specification for pre-defined library items
76+
*
77+
* This format is used to define common THT components with standardized dimensions.
78+
* The fields map to LibraryItem properties as follows:
79+
*
80+
* **For rectangles (shape = "rect"):**
81+
* - `width` → LibraryItem.width (X dimension in mm)
82+
* - `depth` → LibraryItem.height (Y dimension along board in mm)
83+
* - `height` → LibraryItem.depth (component height above PCB → hole depth in mm)
84+
*
85+
* **For circles (shape = "circle"):**
86+
* - `width` → LibraryItem.radius (diameter in mm, converted to radius by dividing by 2)
87+
* - `depth` → Not used (kept equal to width for consistency)
88+
* - `height` → LibraryItem.depth (component height above PCB → hole depth in mm)
89+
*
90+
* @see convertDefaultComponentToLibraryItem for conversion logic
91+
*/
92+
export interface DefaultComponent {
93+
/** User-friendly component name */
94+
name: string;
95+
/** Shape type: "rect" for rectangular, "circle" for round components */
96+
shape: ComponentShape;
97+
/** For rect: X dimension (width). For circle: diameter */
98+
width: number;
99+
/** For rect: Y dimension (along board). For circle: not used (set = width) */
100+
depth: number;
101+
/** Component height above PCB (maps to hole depth in LibraryItem) */
102+
height: number;
103+
}
104+
105+
/**
106+
* Pre-defined common THT components for quick library initialization
107+
*
108+
* Includes standard footprints with dimensions sourced from datasheets:
109+
* - DIP IC sockets (narrow and wide)
110+
* - Radial electrolytic capacitors (various sizes)
111+
* - PCB-mount buzzers (small and large)
112+
* - Screw terminal blocks (5.08mm pitch)
113+
* - Generic relays (small and large)
114+
*
115+
* Users can load these defaults into their library via the "Load defaults"
116+
* button in the Library modal. Duplicates (by name) are automatically skipped.
117+
*/
118+
export const DEFAULT_COMPONENTS: DefaultComponent[] = [
119+
// DIP IC sockets
120+
{
121+
name: 'DIP-8 socket (narrow)',
122+
shape: 'rect',
123+
width: 7.6, // body width ~7.6 mm
124+
depth: 10, // body length ~10 mm
125+
height: 5 // profile height ~3.5–5 mm
126+
},
127+
{
128+
name: 'DIP-14/16 socket (narrow)',
129+
shape: 'rect',
130+
width: 7.6,
131+
depth: 20, // ~19–20 mm body length
132+
height: 5
133+
},
134+
{
135+
name: 'DIP-28 socket (wide)',
136+
shape: 'rect',
137+
width: 15.2, // wide row spacing ~15.24 mm
138+
depth: 37, // ~37 mm body length
139+
height: 5
140+
},
141+
{
142+
name: 'DIP-40 socket (wide)',
143+
shape: 'rect',
144+
width: 15.2,
145+
depth: 52, // ~52 mm body length
146+
height: 5
147+
},
148+
149+
// Radial electrolytic capacitors (DxL families)
150+
{
151+
name: 'Electrolytic Ø5×11',
152+
shape: 'circle',
153+
width: 5, // diameter 5 mm
154+
depth: 5, // not used for circle, keep = diameter
155+
height: 11 // can-height ~11 mm
156+
},
157+
{
158+
name: 'Electrolytic Ø8×12',
159+
shape: 'circle',
160+
width: 8, // diameter 8 mm
161+
depth: 8,
162+
height: 12 // height 12 mm
163+
},
164+
{
165+
name: 'Electrolytic Ø10×16',
166+
shape: 'circle',
167+
width: 10, // diameter 10 mm
168+
depth: 10,
169+
height: 16 // height 16 mm
170+
},
171+
172+
// Buzzers
173+
{
174+
name: 'Buzzer small Ø14×8',
175+
shape: 'circle',
176+
width: 14, // small PCB buzzer ~13–14 mm Ø
177+
depth: 14,
178+
height: 8 // ~7–8 mm height above PCB
179+
},
180+
{
181+
name: 'Buzzer large Ø31×14',
182+
shape: 'circle',
183+
width: 31, // typical large buzzer ~30–32 mm Ø
184+
depth: 31,
185+
height: 14 // ~14–15 mm height
186+
},
187+
188+
// Terminal blocks (5.08 mm pitch family)
189+
{
190+
name: 'Terminal block 2×5.08',
191+
shape: 'rect',
192+
width: 10.5, // ≈2×5.08 + housing margins
193+
depth: 8, // body depth ~7–9 mm
194+
height: 11 // ~10–12 mm above PCB
195+
},
196+
{
197+
name: 'Terminal block 3×5.08',
198+
shape: 'rect',
199+
width: 15.5, // ≈3×5.08 + housing margins
200+
depth: 8,
201+
height: 11
202+
},
203+
204+
// Generic board parts
205+
{
206+
name: 'Small relay',
207+
shape: 'rect',
208+
width: 10,
209+
depth: 19,
210+
height: 15
211+
},
212+
{
213+
name: 'Large relay',
214+
shape: 'rect',
215+
width: 15,
216+
depth: 28,
217+
height: 20
218+
}
219+
];
220+
221+
/**
222+
* Converts a DefaultComponent to a LibraryItem for storage
223+
*
224+
* Applies dimension mappings according to shape type:
225+
*
226+
* **Rectangle conversion:**
227+
* - width → width (X dimension preserved)
228+
* - depth → height (Y dimension - confusingly named in LibraryItem)
229+
* - height → depth (component height above PCB becomes hole depth)
230+
* - rotation defaults to 0
231+
*
232+
* **Circle conversion:**
233+
* - width (diameter) → radius (divided by 2)
234+
* - height → depth (component height above PCB becomes hole depth)
235+
* - depth field in DefaultComponent is unused for circles
236+
*
237+
* @param component - Default component specification
238+
* @returns LibraryItem ready for storage in library
239+
*/
240+
export const convertDefaultComponentToLibraryItem = (component: DefaultComponent): LibraryItem =>
241+
component.shape === 'circle'
242+
? {
243+
type: 'circle',
244+
name: component.name,
245+
radius: component.width / 2, // Convert diameter to radius
246+
depth: component.height // Component height → hole depth
247+
}
248+
: {
249+
type: 'rectangle',
250+
name: component.name,
251+
width: component.width, // X dimension
252+
height: component.depth, // Y dimension (confusingly named in LibraryItem)
253+
depth: component.height, // Component height → hole depth
254+
rotation: 0 // Default rotation
255+
};

0 commit comments

Comments
 (0)