mirror of https://codeberg.org/topola/topola.git
feat(topola/board): Load boundary into `Board`
This commit is contained in:
parent
439c005fa2
commit
78242f16fc
|
|
@ -11,7 +11,7 @@ merge_commit = false
|
|||
allowed_scopes = [
|
||||
# Generated using `ls crates | awk '{ print "\"" $1 "\","; }' | sort`.
|
||||
"planar-incr-embed",
|
||||
"specctra-core",
|
||||
"specctra",
|
||||
"specctra_derive",
|
||||
"topola-cli",
|
||||
"topola-egui",
|
||||
|
|
|
|||
|
|
@ -1,34 +1,48 @@
|
|||
# SPDX-FileCopyrightText: 2024 Topola contributors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "topola-egui"
|
||||
description = "Graphical user interface for Topola PCB router in Egui/Eframe framework."
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["xdg-portal"]
|
||||
gtk3 = ["rfd/gtk3"]
|
||||
xdg-portal = ["rfd/xdg-portal"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
||||
|
||||
[dependencies]
|
||||
egui = "0.33.0"
|
||||
eframe = { version = "0.33.0", default-features = false, features = [
|
||||
"accesskit", # Make egui compatible with screen readers. NOTE: adds a lot of dependencies.
|
||||
derive-getters.workspace = true
|
||||
egui = "0.33"
|
||||
eframe = { version = "0.33", default-features = false, features = [
|
||||
#"accesskit", # Make egui compatible with screen readers. NOTE: adds a lot of dependencies.
|
||||
"default_fonts", # Embed the default egui fonts.
|
||||
"glow", # Use the glow rendering backend. Alternative: "wgpu".
|
||||
"persistence", # Enable restoring app state when restarting the app.
|
||||
"wayland", # To support Linux (and CI).
|
||||
"x11", # To support older Linux distributions (restores one of the default features).
|
||||
] }
|
||||
log = "0.4.27"
|
||||
topola = { version = "0.1", path = "../topola" }
|
||||
fluent-templates = "0.13"
|
||||
log = "0.4"
|
||||
rfd = { version = "0.17", default-features = false }
|
||||
specctra = { path = "../specctra" }
|
||||
topola = { path = "../topola" }
|
||||
unic-langid = { version = "0.9", features = ["macros", "serde"] }
|
||||
|
||||
# You only need serde if you want app persistence:
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
# Native:
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
env_logger = "0.11.8"
|
||||
futures-lite = "2.6"
|
||||
env_logger = "0.11"
|
||||
|
||||
# Web:
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen-futures = "0.4.50"
|
||||
web-sys = "0.3.70" # to access the DOM to hide the loading text.
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-sys = "0.3" # To access the DOM to hide the loading text.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
// SPDX-FileCopyrightText: 2024 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
pub struct Action {
|
||||
name: String,
|
||||
shortcut: Option<egui::KeyboardShortcut>,
|
||||
}
|
||||
|
||||
pub struct Trigger {
|
||||
action: Action,
|
||||
triggered: bool,
|
||||
}
|
||||
|
||||
pub struct Switch {
|
||||
action: Action,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn new(name: String, modifiers: egui::Modifiers, key: egui::Key) -> Self {
|
||||
Self {
|
||||
name,
|
||||
shortcut: Some(egui::KeyboardShortcut::new(modifiers, key)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_keyless(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
shortcut: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn widget_text(&self) -> String {
|
||||
if let Some(shortcut) = self.shortcut {
|
||||
format!(
|
||||
"{} ({})",
|
||||
self.name,
|
||||
shortcut.format(&egui::ModifierNames::NAMES, false)
|
||||
)
|
||||
} else {
|
||||
self.name.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_trigger(self) -> Trigger {
|
||||
Trigger {
|
||||
action: self,
|
||||
triggered: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_switch(self) -> Switch {
|
||||
Switch { action: self }
|
||||
}
|
||||
}
|
||||
|
||||
impl Trigger {
|
||||
pub fn button(&mut self, _ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||
self.triggered = ui.button(self.action.widget_text()).clicked();
|
||||
}
|
||||
|
||||
pub fn hyperlink(&self, _ctx: &egui::Context, ui: &mut egui::Ui, url: &str) {
|
||||
ui.hyperlink_to(self.action.widget_text(), url);
|
||||
}
|
||||
|
||||
pub fn consume_key_triggered(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) -> bool {
|
||||
self.consume_key(ctx, ui);
|
||||
self.triggered
|
||||
}
|
||||
|
||||
fn consume_key(&mut self, ctx: &egui::Context, _ui: &mut egui::Ui) {
|
||||
if let Some(shortcut) = self.action.shortcut {
|
||||
if ctx.input_mut(|i| i.consume_shortcut(&shortcut)) {
|
||||
self.triggered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Switch {
|
||||
pub fn toggle_widget(&self, ui: &mut egui::Ui, selected: &mut bool) {
|
||||
ui.toggle_value(selected, self.action.widget_text());
|
||||
}
|
||||
|
||||
pub fn checkbox(&self, ui: &mut egui::Ui, selected: &mut bool) {
|
||||
ui.checkbox(selected, self.action.widget_text());
|
||||
}
|
||||
|
||||
pub fn consume_key_enabled(
|
||||
&self,
|
||||
ctx: &egui::Context,
|
||||
_ui: &mut egui::Ui,
|
||||
selected: &mut bool,
|
||||
) -> bool {
|
||||
if let Some(shortcut) = self.action.shortcut {
|
||||
if ctx.input_mut(|i| i.consume_shortcut(&shortcut)) {
|
||||
*selected = !*selected;
|
||||
}
|
||||
}
|
||||
|
||||
*selected
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{
|
||||
action::{Action, Trigger},
|
||||
translator::Translator,
|
||||
};
|
||||
|
||||
pub struct Actions {
|
||||
pub file: FileActions,
|
||||
}
|
||||
|
||||
impl Actions {
|
||||
pub fn new(tr: &Translator) -> Self {
|
||||
Self {
|
||||
file: FileActions::new(tr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileActions {
|
||||
pub open_design: Trigger,
|
||||
pub export_session: Trigger,
|
||||
pub quit: Trigger,
|
||||
}
|
||||
|
||||
impl FileActions {
|
||||
pub fn new(tr: &Translator) -> Self {
|
||||
Self {
|
||||
open_design: Action::new(
|
||||
tr.text("tr-menu-file-open"),
|
||||
egui::Modifiers::CTRL,
|
||||
egui::Key::O,
|
||||
)
|
||||
.into_trigger(),
|
||||
export_session: Action::new(
|
||||
tr.text("tr-menu-file-export-session-file"),
|
||||
egui::Modifiers::CTRL,
|
||||
egui::Key::O,
|
||||
)
|
||||
.into_trigger(),
|
||||
quit: Action::new(
|
||||
tr.text("tr-menu-file-quit"),
|
||||
egui::Modifiers::CTRL,
|
||||
egui::Key::Q,
|
||||
)
|
||||
.into_trigger(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_menu(&mut self, ctx: &egui::Context, ui: &mut egui::Ui, have_workspace: bool) {
|
||||
self.open_design.button(ctx, ui);
|
||||
ui.add_enabled_ui(have_workspace, |ui| {
|
||||
self.export_session.button(ctx, ui);
|
||||
|
||||
ui.separator();
|
||||
|
||||
/*self.import_history.button(ctx, ui);
|
||||
self.export_history.button(ctx, ui);*/
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
// "Quit" button wouldn't work on a Web page.
|
||||
if !cfg!(target_arch = "wasm32") {
|
||||
self.quit.button(ctx, ui);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,39 @@
|
|||
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
||||
#[derive(serde::Deserialize, serde::Serialize)]
|
||||
#[serde(default)] // if we add new fields, give them default values when deserializing old state
|
||||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
|
||||
use specctra::{error::ParseErrorContext, structure::DsnFile};
|
||||
use topola::Board;
|
||||
use unic_langid::langid;
|
||||
|
||||
use crate::{menu_bar::MenuBar, translator::Translator, workspace::Workspace};
|
||||
|
||||
pub struct App {
|
||||
translator: Translator,
|
||||
|
||||
content_channel: (
|
||||
Sender<Result<DsnFile, ParseErrorContext>>,
|
||||
Receiver<Result<DsnFile, ParseErrorContext>>,
|
||||
),
|
||||
|
||||
menu_bar: MenuBar,
|
||||
view_rect: egui::Rect,
|
||||
workspace: Option<Workspace>,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
translator: Translator::new(langid!("en-US")),
|
||||
content_channel: channel(),
|
||||
menu_bar: MenuBar::new(),
|
||||
view_rect: egui::Rect::from_min_max(
|
||||
egui::pos2(-100.0, 100.0),
|
||||
egui::pos2(100.0, 100.0),
|
||||
),
|
||||
workspace: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,38 +44,91 @@ impl App {
|
|||
// Restore the persistent part of the app's state from its previous run
|
||||
// if there is one.
|
||||
if let Some(storage) = cc.storage {
|
||||
eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default()
|
||||
//eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default()
|
||||
Default::default()
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn update_state(&mut self) {
|
||||
if let Ok(data) = self.content_channel.1.try_recv() {
|
||||
self.workspace = Some(Workspace::new(
|
||||
Board::from_specctra(data.unwrap()),
|
||||
&self.translator,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the title displayed on the application window's frame to show the
|
||||
/// currently opened file, if any, and other possible information about the
|
||||
/// application state.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn update_title(&mut self, ctx: &egui::Context) {
|
||||
/*if let Some(workspace) = &self.maybe_workspace {
|
||||
if let Some(filename) = Path::new(workspace.design.get_name())
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
{
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Title(filename.to_string()));
|
||||
// TODO: Also show file's dirty bit.
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
/// Update the title displayed on the application window's frame to show the
|
||||
/// currently opened file, if any, and other possible information about the
|
||||
/// application state.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn update_title(&mut self, ctx: &egui::Context) {
|
||||
if let Some(workspace) = &self.maybe_workspace {
|
||||
if let Some(filename) = Path::new(workspace.design.get_name())
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
{
|
||||
let document = eframe::web_sys::window()
|
||||
.expect("No window")
|
||||
.document()
|
||||
.expect("No document");
|
||||
|
||||
document.set_title(filename);
|
||||
// TODO: Also show file's dirty bit.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a possible locale change.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn update_locale(&mut self) {
|
||||
// I don't know any equivalent of changing the lang property in desktop.
|
||||
}
|
||||
|
||||
/// Handle a possible locale change.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn update_locale(&mut self) {
|
||||
let document_element = eframe::web_sys::window()
|
||||
.expect("No window")
|
||||
.document()
|
||||
.expect("No document")
|
||||
.document_element()
|
||||
.expect("No document element");
|
||||
|
||||
document_element.set_attribute("lang", &self.translator.langid().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
/// Called to save state before shutdown.
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
eframe::set_value(storage, eframe::APP_KEY, self);
|
||||
//eframe::set_value(storage, eframe::APP_KEY, self);
|
||||
}
|
||||
|
||||
/// Called each time the UI has to be repainted.
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::MenuBar::new().ui(ui, |ui| {
|
||||
ui.menu_button("File", |ui| {
|
||||
// Quit is not present on Web; the app can't close a webpage
|
||||
// by itself.
|
||||
if !cfg!(target_arch = "wasm32") {
|
||||
if ui.button("Quit").clicked() {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
}
|
||||
});
|
||||
self.menu_bar
|
||||
.update(ctx, &mut self.translator, self.content_channel.0.clone());
|
||||
|
||||
ui.separator();
|
||||
|
||||
egui::widgets::global_theme_preference_switch(ui);
|
||||
});
|
||||
});
|
||||
self.update_state();
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
egui::Scene::new()
|
||||
|
|
@ -63,5 +138,30 @@ impl eframe::App for App {
|
|||
.circle_filled(egui::pos2(0.0, 0.0), 20.0, egui::Color32::RED);
|
||||
});
|
||||
});
|
||||
|
||||
self.update_locale();
|
||||
self.update_title(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn execute<F: Future<Output = ()> + Send + 'static>(f: F) {
|
||||
std::thread::spawn(move || futures_lite::future::block_on(f));
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn execute<F: Future<Output = ()> + 'static>(f: F) {
|
||||
wasm_bindgen_futures::spawn_local(f);
|
||||
}
|
||||
|
||||
pub async fn handle_file(
|
||||
file_handle: &rfd::FileHandle,
|
||||
) -> std::io::Result<impl std::io::BufRead + std::io::Seek> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let res = std::io::BufReader::new(std::fs::File::open(file_handle.path())?);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let res = std::io::Cursor::new(file_handle.read().await);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
mod action;
|
||||
mod actions;
|
||||
mod app;
|
||||
use app::App;
|
||||
mod menu_bar;
|
||||
mod translator;
|
||||
mod workspace;
|
||||
use crate::app::App;
|
||||
|
||||
// When compiling natively:
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,69 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specctra::{
|
||||
error::{ParseError, ParseErrorContext},
|
||||
read::ListTokenizer,
|
||||
structure::DsnFile,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
actions::Actions,
|
||||
app::{execute, handle_file},
|
||||
translator::Translator,
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct MenuBar {
|
||||
step_rate: f32,
|
||||
}
|
||||
|
||||
impl MenuBar {
|
||||
pub fn new() -> Self {
|
||||
Self { step_rate: 1.0 }
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
ctx: &egui::Context,
|
||||
tr: &mut Translator,
|
||||
content_sender: Sender<Result<DsnFile, ParseErrorContext>>,
|
||||
) {
|
||||
let mut actions = Actions::new(tr);
|
||||
|
||||
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
|
||||
egui::MenuBar::new().ui(ui, |ui| {
|
||||
ui.menu_button("File", |ui| {
|
||||
actions.file.render_menu(ctx, ui, false);
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
egui::widgets::global_theme_preference_switch(ui);
|
||||
});
|
||||
|
||||
if actions.file.open_design.consume_key_triggered(ctx, ui) {
|
||||
// NOTE: On Linux, this requires Zenity to be installed on your system.
|
||||
let ctx = ctx.clone();
|
||||
let task = rfd::AsyncFileDialog::new()
|
||||
.add_filter(tr.text("tr-menu-open-specctra-design-file"), &["dsn"])
|
||||
.pick_file();
|
||||
|
||||
execute(async move {
|
||||
if let Some(file_handle) = task.await {
|
||||
let data = handle_file(&file_handle)
|
||||
.await
|
||||
.map_err(|e| ParseError::from(e).add_context((0, 0)))
|
||||
.and_then(|reader| ListTokenizer::new(reader).read_value::<DsnFile>());
|
||||
content_sender.send(data);
|
||||
|
||||
ctx.request_repaint();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use derive_getters::Getters;
|
||||
use fluent_templates::{static_loader, Loader};
|
||||
use fluent_templates::{Loader, static_loader};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
||||
static_loader! {
|
||||
static LOCALES = {
|
||||
locales: "../../locales",
|
||||
locales: "../locales",
|
||||
fallback_language: "en-US",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use topola::Board;
|
||||
|
||||
use crate::translator::Translator;
|
||||
|
||||
pub struct Workspace {
|
||||
pub board: Board,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn new(board: Board, tr: &Translator) -> Self {
|
||||
Self { board }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
# SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
#
|
||||
# SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "topola"
|
||||
description = "Work-in-progress free and open-source topological (rubberband) router and autorouter for printed circuit boards (PCBs)"
|
||||
|
|
@ -8,6 +12,7 @@ edition = "2024"
|
|||
derive-getters.workspace = true
|
||||
derive_more.workspace = true
|
||||
serde.workspace = true
|
||||
specctra = { path = "../specctra" }
|
||||
stable-vec = "0.4"
|
||||
undoredo.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use derive_getters::{Dissolve, Getters};
|
||||
use undoredo::{ApplyDelta, Delta, FlushDelta};
|
||||
|
|
@ -18,9 +18,9 @@ pub struct Board {
|
|||
}
|
||||
|
||||
impl Board {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(boundary: Vec<[i64; 2]>) -> Self {
|
||||
Self {
|
||||
layout: Layout::new(),
|
||||
layout: Layout::new(boundary),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
|
|
@ -103,6 +103,8 @@ pub struct Via {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Layout {
|
||||
boundary: Vec<[i64; 2]>,
|
||||
place_boundary: Vec<[i64; 2]>,
|
||||
joints: Recorder<StableVec<Joint>>,
|
||||
segments: Recorder<StableVec<Segment>>,
|
||||
arcs: Recorder<StableVec<Arc>>,
|
||||
|
|
@ -110,8 +112,10 @@ pub struct Layout {
|
|||
}
|
||||
|
||||
impl Layout {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(boundary: Vec<[i64; 2]>) -> Self {
|
||||
Self {
|
||||
boundary: boundary.clone(),
|
||||
place_boundary: boundary,
|
||||
joints: Recorder::new(StableVec::new()),
|
||||
segments: Recorder::new(StableVec::new()),
|
||||
arcs: Recorder::new(StableVec::new()),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
mod board;
|
||||
mod layout;
|
||||
mod specctra;
|
||||
|
||||
pub use crate::board::Board;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use specctra::structure::DsnFile;
|
||||
|
||||
use crate::board::Board;
|
||||
|
||||
impl Board {
|
||||
pub fn from_specctra(dsn: DsnFile) -> Self {
|
||||
Board::new(
|
||||
dsn.pcb
|
||||
.structure
|
||||
.boundary
|
||||
.coords()
|
||||
.into_owned()
|
||||
.into_iter()
|
||||
.map(|p| [p.x as i64, p.y as i64])
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue