mirror of https://codeberg.org/topola/topola.git
Compare commits
3 Commits
d589266df5
...
f4a8bee711
| Author | SHA1 | Date |
|---|---|---|
|
|
f4a8bee711 | |
|
|
5f7f8fe14e | |
|
|
e9778fd653 |
|
|
@ -9,7 +9,7 @@ resolver = "2"
|
|||
|
||||
[workspace.dependencies]
|
||||
derive-getters = "0.5"
|
||||
derive_more = { version = "2.1", features = ["from"] }
|
||||
derive_more = { version = "2.1", features = ["add", "constructor", "from", "into"] }
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
thiserror = "2.0"
|
||||
undoredo = { version = "0.8", features = ["stable-vec"] }
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ impl App {
|
|||
Board::from_specctra(data.unwrap()),
|
||||
&self.translator,
|
||||
));
|
||||
self.viewport.scheduled_zoom_to_fit = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,18 +43,15 @@ impl Displayer {
|
|||
y: p[1] as f32,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
egui::Stroke::new(20.0 / viewport.scale_factor(), egui::Color32::WHITE),
|
||||
egui::Stroke::new(5.0 / viewport.scale_factor(), egui::Color32::WHITE),
|
||||
);
|
||||
ui.painter().line(
|
||||
vec![
|
||||
Pos2::new(0.0, 0.0),
|
||||
Pos2::new(100.0, 100.0),
|
||||
Pos2::new(100.0, 500.0),
|
||||
],
|
||||
egui::Stroke::new(2.0, egui::Color32::GOLD),
|
||||
);
|
||||
ui.painter()
|
||||
.circle_filled(egui::pos2(0.0, 0.0), 2.0, egui::Color32::RED);
|
||||
//workspace.board.layout().boundary()
|
||||
|
||||
for (_, joint) in workspace.board.layout().joints().collection() {
|
||||
ui.painter().circle_filled(
|
||||
egui::Pos2::new(joint.position[0] as f32, joint.position[1] as f32),
|
||||
joint.radius as f32,
|
||||
egui::Color32::RED,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,24 +2,22 @@
|
|||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use egui::Pos2;
|
||||
|
||||
use crate::{displayer::Displayer, workspace::Workspace};
|
||||
|
||||
pub struct Viewport {
|
||||
pub scene_rect: egui::Rect,
|
||||
pub ref_scene_rect: egui::Rect,
|
||||
pub scheduled_zoom_to_fit: bool,
|
||||
}
|
||||
|
||||
impl Viewport {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
scene_rect: egui::Rect::from_min_max(
|
||||
egui::pos2(-10000.0, -10000.0),
|
||||
egui::pos2(10000.0, 10000.0),
|
||||
),
|
||||
ref_scene_rect: egui::Rect::from_min_max(
|
||||
egui::pos2(-10000.0, -10000.0),
|
||||
egui::pos2(10000.0, 10000.0),
|
||||
),
|
||||
scene_rect: egui::Rect::from_min_max(egui::pos2(-1.0, -1.0), egui::pos2(1.0, 1.0)),
|
||||
ref_scene_rect: egui::Rect::from_min_max(egui::pos2(-1.0, -1.0), egui::pos2(1.0, 1.0)),
|
||||
scheduled_zoom_to_fit: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -28,18 +26,61 @@ impl Viewport {
|
|||
let mut scene_rect = self.scene_rect.clone();
|
||||
|
||||
egui::Scene::new()
|
||||
.zoom_range(0.0001..=10000.0)
|
||||
.zoom_range(0.00001..=10000.0)
|
||||
.show(ui, &mut scene_rect, |ui| {
|
||||
if let Some(workspace) = workspace {
|
||||
if let Some(ref workspace) = workspace {
|
||||
let mut displayer = Displayer::new();
|
||||
displayer.update(ctx, ui, &self, workspace);
|
||||
}
|
||||
});
|
||||
|
||||
self.scene_rect = scene_rect;
|
||||
|
||||
if let Some(workspace) = workspace {
|
||||
self.zoom_to_fit_if_scheduled(workspace);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn zoom_to_fit_if_scheduled(&mut self, workspace: &Workspace) {
|
||||
if self.scheduled_zoom_to_fit {
|
||||
self.scene_rect = Self::boundary_bounding_box(workspace);
|
||||
self.ref_scene_rect = self.scene_rect.clone();
|
||||
}
|
||||
|
||||
self.scheduled_zoom_to_fit = false;
|
||||
}
|
||||
|
||||
fn boundary_bounding_box(workspace: &Workspace) -> egui::Rect {
|
||||
let first = workspace.board.layout().boundary()[0];
|
||||
|
||||
let mut min_x = first[0];
|
||||
let mut max_x = first[0];
|
||||
let mut min_y = first[1];
|
||||
let mut max_y = first[1];
|
||||
|
||||
for point in workspace.board.layout().boundary()[1..].iter() {
|
||||
if point[0] < min_x {
|
||||
min_x = point[0];
|
||||
}
|
||||
if point[0] > max_x {
|
||||
max_x = point[0];
|
||||
}
|
||||
if point[1] < min_y {
|
||||
min_y = point[1];
|
||||
}
|
||||
if point[1] > max_y {
|
||||
max_y = point[1];
|
||||
}
|
||||
}
|
||||
|
||||
egui::Rect::from_min_max(
|
||||
Pos2::new(min_x as f32, min_y as f32),
|
||||
Pos2::new(max_x as f32, max_y as f32),
|
||||
)
|
||||
.scale_from_center(1.05)
|
||||
}
|
||||
|
||||
pub fn scale_factor(&self) -> f32 {
|
||||
self.ref_scene_rect.width() / self.scene_rect.width()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
dearcut = { version = "0.1", features = ["undoredo"] }
|
||||
derive-getters.workspace = true
|
||||
derive_more.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
|
|||
|
|
@ -5,14 +5,16 @@
|
|||
use derive_getters::{Dissolve, Getters};
|
||||
use undoredo::{ApplyDelta, Delta, FlushDelta};
|
||||
|
||||
use crate::layout::{Layout, LayoutHalfDelta};
|
||||
use crate::layout::{
|
||||
Arc, ArcId, Joint, JointId, Layout, LayoutHalfDelta, Segment, SegmentId, Via, ViaId,
|
||||
};
|
||||
|
||||
struct Layer {
|
||||
name: String,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
#[derive(Getters)]
|
||||
#[derive(Clone, Debug, Getters)]
|
||||
pub struct Board {
|
||||
layout: Layout,
|
||||
}
|
||||
|
|
@ -23,6 +25,22 @@ impl Board {
|
|||
layout: Layout::new(boundary),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_joint(&mut self, joint: Joint) -> JointId {
|
||||
self.layout.add_joint(joint)
|
||||
}
|
||||
|
||||
pub fn add_segment(&mut self, segment: Segment) -> SegmentId {
|
||||
self.layout.add_segment(segment)
|
||||
}
|
||||
|
||||
pub fn add_arc(&mut self, arc: Arc) -> ArcId {
|
||||
self.layout.add_arc(arc)
|
||||
}
|
||||
|
||||
pub fn add_via(&mut self, via: Via) -> ViaId {
|
||||
self.layout.add_via(via)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Dissolve)]
|
||||
|
|
|
|||
|
|
@ -25,10 +25,11 @@ impl JointId {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Joint {
|
||||
position: [i64; 2],
|
||||
radius: u64,
|
||||
pub position: [i64; 2],
|
||||
pub layer: usize,
|
||||
pub radius: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
|
|
@ -48,10 +49,11 @@ impl SegmentId {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Segment {
|
||||
endpoints: [JointId; 2],
|
||||
half_width: u64,
|
||||
pub endpoints: [JointId; 2],
|
||||
pub layer: usize,
|
||||
pub half_width: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
|
|
@ -71,11 +73,12 @@ impl ArcId {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Arc {
|
||||
endpoints: [JointId; 2],
|
||||
focus: [i64; 2],
|
||||
half_width: u64,
|
||||
pub endpoints: [JointId; 2],
|
||||
pub focus: [i64; 2],
|
||||
pub layer: usize,
|
||||
pub half_width: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
|
|
@ -95,10 +98,11 @@ impl ViaId {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Via {
|
||||
endpoints: [JointId; 2],
|
||||
radius: u64,
|
||||
pub endpoints: [JointId; 2],
|
||||
pub layer: usize,
|
||||
pub radius: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Getters)]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
|
||||
mod board;
|
||||
mod layout;
|
||||
mod math;
|
||||
mod navmesher;
|
||||
mod specctra;
|
||||
|
||||
pub use crate::board::Board;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
// SPDX-FileCopyrightText: 2026 Topola contributors
|
||||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use dearcut::RecordingTriangulator;
|
||||
use derive_getters::Getters;
|
||||
|
||||
use crate::{
|
||||
Board,
|
||||
layout::{Arc, ArcId, Joint, JointId, Segment, SegmentId, Via, ViaId},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Getters)]
|
||||
pub struct LayerNavmesher {
|
||||
navmeshes: Vec<RecordingTriangulator<i64>>,
|
||||
inflation_factors: Vec<f64>,
|
||||
}
|
||||
|
||||
impl LayerNavmesher {
|
||||
pub fn insert_polygon(&mut self, polygon: impl IntoIterator<Item = [i64; 2]>) {
|
||||
let polygon: Vec<[i64; 2]> = polygon.into_iter().collect();
|
||||
|
||||
for i in 0..self.navmeshes.len() {
|
||||
self.navmeshes[i].insert_polygon(Self::inflate_polygon(
|
||||
polygon.clone(),
|
||||
self.inflation_factors[i],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn inflate_polygon(
|
||||
polygon: impl IntoIterator<Item = [i64; 2]>,
|
||||
inflation_factor: f64,
|
||||
) -> impl IntoIterator<Item = [i64; 2]> {
|
||||
let polygon: Vec<[i64; 2]> = polygon.into_iter().collect();
|
||||
|
||||
// Centroid.
|
||||
let cx = polygon.iter().map(|p| p[0] as f64).sum::<f64>() / polygon.len() as f64;
|
||||
let cy = polygon.iter().map(|p| p[1] as f64).sum::<f64>() / polygon.len() as f64;
|
||||
|
||||
polygon.into_iter().map(move |[px, py]| {
|
||||
// Delta.
|
||||
let dx = px as f64 - cx;
|
||||
let dy = py as f64 - cy;
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
|
||||
// Normalize delta.
|
||||
let nx = dx / d;
|
||||
let ny = dy / d;
|
||||
|
||||
// Shift away from centroid.
|
||||
let fx = px as f64 + nx * inflation_factor;
|
||||
let fy = py as f64 + ny * inflation_factor;
|
||||
|
||||
// Round away from centroid.
|
||||
let rx = if fx >= cx { fx.ceil() } else { fx.floor() };
|
||||
let ry = if fy >= cy { fy.ceil() } else { fy.floor() };
|
||||
|
||||
[rx as i64, ry as i64]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Getters)]
|
||||
pub struct Navmesher {
|
||||
layers: Vec<LayerNavmesher>,
|
||||
}
|
||||
|
||||
impl Navmesher {
|
||||
pub fn insert_polygon(&mut self, layer: usize, polygon: impl IntoIterator<Item = [i64; 2]>) {
|
||||
self.layers[layer].insert_polygon(polygon);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Getters)]
|
||||
pub struct NavmesherBoard {
|
||||
navmesher: Navmesher,
|
||||
board: Board,
|
||||
}
|
||||
|
||||
impl NavmesherBoard {
|
||||
pub fn insert_joint(&mut self, joint: Joint) -> JointId {
|
||||
self.navmesher
|
||||
.insert_polygon(joint.layer, Self::joint_circumscribed_octagon(joint));
|
||||
self.board.add_joint(joint)
|
||||
}
|
||||
|
||||
fn joint_circumscribed_octagon(joint: Joint) -> [[i64; 2]; 8] {
|
||||
let cx = joint.position[0];
|
||||
let cy = joint.position[1];
|
||||
let r = joint.radius as i64;
|
||||
|
||||
// 1.082392... = 1 / cos(π/8)
|
||||
// 0.414213... = tan(π/8)
|
||||
|
||||
// Approximate multipliers as fractions.
|
||||
let r1 = (r * 277 + 128) / 256; // round(r * 1.0823922)
|
||||
let r2 = (r * 106 + 128) / 256; // round(r * 0.41421356)
|
||||
|
||||
[
|
||||
[cx + r1, cy], // right
|
||||
[cx + r2, cy + r2], // top-right
|
||||
[cx, cy + r1], // top
|
||||
[cx - r2, cy + r2], // top-left
|
||||
[cx - r1, cy], // left
|
||||
[cx - r2, cy - r2], // bottom-left
|
||||
[cx, cy - r1], // bottom
|
||||
[cx + r2, cy - r2], // bottom-right
|
||||
]
|
||||
}
|
||||
|
||||
pub fn insert_segment(&mut self, segment: Segment) -> SegmentId {
|
||||
// TODO: Insert into navmesh.
|
||||
self.board.add_segment(segment)
|
||||
}
|
||||
|
||||
pub fn insert_arc(&mut self, arc: Arc) -> ArcId {
|
||||
// TODO: Insert into navmesh.
|
||||
self.board.add_arc(arc)
|
||||
}
|
||||
|
||||
pub fn insert_via(&mut self, via: Via) -> ViaId {
|
||||
// TODO: Insert into navmesh.
|
||||
self.board.add_via(via)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,13 +2,16 @@
|
|||
//
|
||||
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
|
||||
use specctra::structure::DsnFile;
|
||||
use specctra::{
|
||||
math::PointWithRotation,
|
||||
structure::{DsnFile, Shape},
|
||||
};
|
||||
|
||||
use crate::board::Board;
|
||||
use crate::{board::Board, layout::Joint, math::Vector2};
|
||||
|
||||
impl Board {
|
||||
pub fn from_specctra(dsn: DsnFile) -> Self {
|
||||
Board::new(
|
||||
let mut board = Board::new(
|
||||
dsn.pcb
|
||||
.structure
|
||||
.boundary
|
||||
|
|
@ -17,6 +20,75 @@ impl Board {
|
|||
.into_iter()
|
||||
.map(|p| [p.x as i64, p.y as i64])
|
||||
.collect(),
|
||||
)
|
||||
);
|
||||
|
||||
// add pins from components
|
||||
for component in &dsn.pcb.placement.components {
|
||||
let image = dsn
|
||||
.pcb
|
||||
.library
|
||||
.images
|
||||
.iter()
|
||||
.find(|image| image.name == component.name)
|
||||
.unwrap();
|
||||
|
||||
for place in &component.places {
|
||||
/*let place_side_is_front = place.side == "front";
|
||||
let get_layer = |board: &Board, name: &str| {
|
||||
Self::layer(board, &dsn.pcb.structure.layers, name, place_side_is_front)
|
||||
};*/
|
||||
|
||||
for pin in &image.pins {
|
||||
let padstack = dsn.pcb.library.find_padstack_by_name(&pin.name).unwrap();
|
||||
|
||||
for shape in padstack.shapes.iter() {
|
||||
match shape {
|
||||
Shape::Circle(circle) => Self::place_circle(
|
||||
&mut board,
|
||||
place.point_with_rotation(),
|
||||
pin.point_with_rotation(),
|
||||
(circle.diameter / 2.0) as u64,
|
||||
false,
|
||||
),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
board
|
||||
}
|
||||
|
||||
pub fn place_circle(
|
||||
board: &mut Board,
|
||||
place: PointWithRotation,
|
||||
pin: PointWithRotation,
|
||||
radius: u64,
|
||||
flip: bool,
|
||||
) {
|
||||
let pos = Self::pos(place, pin, 0.0, 0.0, flip);
|
||||
|
||||
board.add_joint(Joint {
|
||||
position: [pos.x, pos.y],
|
||||
layer: 0,
|
||||
radius,
|
||||
});
|
||||
}
|
||||
|
||||
fn pos(
|
||||
place: PointWithRotation,
|
||||
pin: PointWithRotation,
|
||||
x: f64,
|
||||
y: f64,
|
||||
flip: bool,
|
||||
) -> Vector2<i64> {
|
||||
let pos = (Vector2::new(x, y) + Vector2::new(pin.pos.x(), pin.pos.y()))
|
||||
.rotate_around_point_degrees(pin.rot, Vector2::new(pin.pos.x(), pin.pos.y()));
|
||||
let pos = (Vector2::new(place.pos.x(), place.pos.y())
|
||||
+ flip.then_some(Vector2::new(-pos.x, pos.y)).unwrap_or(pos))
|
||||
.rotate_around_point_degrees(place.rot, Vector2::new(place.pos.x(), place.pos.y()));
|
||||
|
||||
Vector2::new(pos.x as i64, pos.y as i64)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue