diff --git a/CHANGELOG.md b/CHANGELOG.md index f68e00c..145dff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,12 @@ glTF profile has its own version (`profileVersion`, [spec §11](spec/README.md#1 same drills share one sheet, which is cut and triangulated once and moved to each layer's heights; copper layers crossed by the same drills share one hole index. The output is unchanged. testcase1-RevC-full (11 dielectrics) converts about 10 % faster in the browser. +- **Smaller files for boards with several dielectric layers (profile 0.10, still a draft):** + a dielectric whose sheet repeats an earlier dielectric's shares that layer's mesh, scaled and + moved in Z by its node transform, and its feature table ([spec §4](spec/README.md#4-scene-structure), + [ADR 0016](docs/adr/0016-shared-dielectric-sheets.md)). testcase1-RevC-full shrinks from + 277 MB to 203 MB. `boardui validate` accepts exactly this transform on layer nodes; files of + profile 0.9 and older load as before. New sample `stacked-sheets`. - **Component attributes (profile 0.8, still a draft):** the converter reads the BOM, the approved vendor list and the exporting software. Each component gets its BOM attributes diff --git a/crates/boardui-convert/src/pipeline.rs b/crates/boardui-convert/src/pipeline.rs index 6871911..65360b4 100644 --- a/crates/boardui-convert/src/pipeline.rs +++ b/crates/boardui-convert/src/pipeline.rs @@ -442,32 +442,40 @@ pub(crate) fn run( let mut layers = Vec::with_capacity(stack.layers.len()); { let _step = timings.step("extrude"); - let mut meshes = par::map(&finals, |i, (_, regions)| { - let layer = &stack.layers[i]; - extrude(&layer.name, regions, layer.z_min, layer.z_max) - }); - // Repeated dielectric sheets: the earlier sheet's mesh at their own heights. - let repeats: Vec<(usize, usize)> = (0..finals.len()) - .filter_map(|i| Some((i, same_sheet[i]?))) + // A dielectric that repeats an earlier one's sheet, in the same colour, shares its + // mesh, scaled and moved in Z (spec §4). + let extrudes = |i: usize| { + let (z_min, z_max) = (stack.layers[i].z_min, stack.layers[i].z_max); + z_min.is_finite() && z_max.is_finite() && z_min < z_max + }; + let mut repeats: Vec> = (0..finals.len()) + .map(|i| { + same_sheet[i].filter(|&j| colours[i] == colours[j] && extrudes(i) && extrudes(j)) + }) .collect(); - let repeated = par::map(&repeats, |_, &(i, j)| { - let (layer, earlier) = (&stack.layers[i], &stack.layers[j]); - let (mesh, messages) = &meshes[j]; - let heights = (layer.z_min, layer.z_max); - // A sheet that failed to mesh fails again, with this layer's warning. - let restacked = messages - .is_empty() - .then(|| mesh.restacked((earlier.z_min, earlier.z_max), heights)); - match restacked.flatten() { - Some(mesh) => (mesh, Vec::new()), - None => extrude(&layer.name, &finals[j].1, heights.0, heights.1), + let mut meshes = par::map(&finals, |i, _| { + let layer = &stack.layers[i]; + // A repeat that doesn't share has no sheet of its own: it extrudes the earlier one. + let regions = &finals[same_sheet[i].unwrap_or(i)].1; + match repeats[i] { + Some(_) => (LayerMesh::default(), Vec::new()), + None => extrude(&layer.name, regions, layer.z_min, layer.z_max), } }); - for ((i, _), mesh) in repeats.into_iter().zip(repeated) { - meshes[i] = mesh; + // A sheet that failed to mesh fails again for each repeat, with its own warning. + for i in 0..finals.len() { + if let Some(j) = repeats[i] + && !meshes[j].1.is_empty() + { + let layer = &stack.layers[i]; + meshes[i] = extrude(&layer.name, &finals[j].1, layer.z_min, layer.z_max); + repeats[i] = None; + } } - let sources = stack.layers.iter().zip(colours); - for (((layer, color), (rows, _)), (mesh, messages)) in sources.zip(finals).zip(meshes) { + let sources = stack.layers.iter().zip(colours).zip(repeats); + for ((((layer, color), repeats), (rows, _)), (mesh, messages)) in + sources.zip(finals).zip(meshes) + { messages.into_iter().for_each(|m| warnings.push(m)); layers.push(LayerAsset { name: layer.name.clone(), @@ -482,6 +490,7 @@ pub(crate) fn run( color, mesh, features: rows, + repeats, }); } } @@ -523,9 +532,10 @@ pub(crate) fn run( pins_misplaced: components.pins_misplaced, ..Stats::default() }; + // The board's geometry: a shared mesh counts for each layer that shows it. let meshes = layers .iter() - .map(|l| (&l.mesh, l.features.len())) + .map(|l| (&l.repeats.map_or(l, |j| &layers[j]).mesh, l.features.len())) .chain(drill_assets.iter().map(|d| (&d.mesh, d.features.len()))); for (mesh, rows) in meshes { stats.features += rows; diff --git a/crates/boardui-convert/src/validate.rs b/crates/boardui-convert/src/validate.rs index e30d8b7..8ec7b7f 100644 --- a/crates/boardui-convert/src/validate.rs +++ b/crates/boardui-convert/src/validate.rs @@ -5,11 +5,11 @@ use std::fmt; use boardui_geom::{DVec2, GRID_STEP, Region}; use boardui_gltf::buffer::{read_u32s, read_vec3, view_bytes}; -use boardui_gltf::json::{FLOAT, Root, UNSIGNED_SHORT}; +use boardui_gltf::json::{FLOAT, Node, Root, UNSIGNED_SHORT}; use boardui_gltf::metadata::{NO_ROW, PropertyTable, SCHEMA_JSON}; use boardui_gltf::{ - Board, ComponentExtras, EXTENSIONS, FeatureKind, Fiducial, Mount, PROFILE_VERSION, Role, Side, - encode_id_segment, glb, layer_id, populate, + Board, BoardLayer, ComponentExtras, EXTENSIONS, FeatureKind, Fiducial, Mount, PROFILE_VERSION, + Role, Side, encode_id_segment, glb, layer_id, populate, }; /// How bad an [`Issue`] is. @@ -465,18 +465,30 @@ impl Validator<'_> { ); } let mut ids = HashSet::new(); + let repeats = self.repeats(board); let entries = board .layers .iter() - .map(|l| (&l.id, &l.name, l.node, l.synthesized)) + .zip(&repeats) + .map(|(l, &r)| (&l.id, &l.name, l.node, l.synthesized, Some((l, r)))) // A synthesized drill layer (spec §6.3) shows in its ID's unencoded `@`. .chain( board .drills .iter() - .map(|d| (&d.id, &d.name, d.node, d.id.starts_with("layer/@"))), + .map(|d| (&d.id, &d.name, d.node, d.id.starts_with("layer/@"), None)), ); - for (id, layer_name, index, synthesized) in entries { + // Nodes per mesh: a layer or drill mesh is used by no other node, except by later + // dielectrics that repeat its sheet. + let mut users: HashMap = HashMap::new(); + for mesh in root.nodes.iter().filter_map(|n| n.mesh) { + *users.entry(mesh).or_default() += 1; + } + let mut layer_users: HashMap = HashMap::new(); + for mesh in board.layers.iter().filter_map(|l| node(l.node)?.mesh) { + *layer_users.entry(mesh).or_default() += 1; + } + for (id, layer_name, index, synthesized, layer) in entries { if *id != layer_id(layer_name, synthesized) || !well_formed(id) { self.report.error(format!( "layer ID `{id}` is not the encoded ID of layer `{layer_name}`" @@ -491,10 +503,24 @@ impl Validator<'_> { self.report .error(format!("the node of `{id}` must be named `{id}`")); } - if !n.is_identity() { - self.report.error(format!( + match layer { + Some((layer, Some(earlier))) => { + self.repeat(&board.layers[earlier], layer, n); + } + _ if !n.is_identity() => self.report.error(format!( "the node of `{id}` must have an identity transform" - )); + )), + _ => {} + } + if let Some(mesh) = n.mesh { + let allowed = match layer { + Some(_) => layer_users[&mesh], + None => 1, + }; + if users[&mesh] > allowed { + self.report + .error(format!("the mesh of `{id}` is used by another node")); + } } if !n.children.is_empty() { self.report @@ -523,6 +549,54 @@ impl Validator<'_> { self.z_order(board); } + /// For each of `board.layers`, the first earlier layer whose node has the same mesh: the + /// layer repeats that layer's geometry (spec §4). + fn repeats(&self, board: &Board) -> Vec> { + let mut first = HashMap::new(); + let nodes = &self.root.nodes; + (board.layers.iter().enumerate()) + .map(|(i, l)| { + let mesh = nodes.get(l.node as usize)?.mesh?; + let earlier = *first.entry(mesh).or_insert(i); + (earlier != i).then_some(earlier) + }) + .collect() + } + + /// Spec §4: a dielectric may share the mesh of an earlier dielectric, with a node transform + /// that only scales and moves along Y, from that layer's Z range onto its own. + fn repeat(&mut self, earlier: &BoardLayer, layer: &BoardLayer, node: &Node) { + let (id, other) = (&layer.id, &earlier.id); + if layer.role != Role::Dielectric || earlier.role != Role::Dielectric { + self.report.error(format!( + "`{id}` shares the mesh of `{other}`, but only dielectric layers may share one" + )); + return; + } + let [x, t, z] = node.translation.unwrap_or_default(); + let [sx, s, sz] = node.scale.unwrap_or([1.0; 3]); + let rotated = node.rotation.is_some_and(|r| r != [0.0, 0.0, 0.0, 1.0]); + // The Z ranges map onto each other within float32 precision. + let largest = [earlier.z_min, earlier.z_max, layer.z_min, layer.z_max] + .iter() + .fold(0.0f64, |m, z| m.max(z.abs())); + let tolerance = 2.0 * f32_spacing(largest) + 1e-12; + let maps = |from: f64, to: f64| (s * from + t - to).abs() <= tolerance; + let moved = node.matrix.is_none() + && !rotated + && [x, z] == [0.0; 2] + && [sx, sz] == [1.0; 2] + && s > 0.0 + && maps(earlier.z_min, layer.z_min) + && maps(earlier.z_max, layer.z_max); + if !moved { + self.report.error(format!( + "the node of `{id}` shares the mesh of `{other}`: its transform must scale and \ + move it along Y only, from the Z range of `{other}` onto its own" + )); + } + } + fn z_order(&mut self, board: &Board) { let layers = &board.layers; for l in layers { @@ -899,10 +973,24 @@ impl Validator<'_> { .structural_metadata .as_ref() .expect("checked"); + // A layer that repeats another has that layer's table and geometry, checked there. + let repeats = self.repeats(board); + for (layer, earlier) in board.layers.iter().zip(&repeats) { + if let Some(earlier) = earlier.map(|e| &board.layers[e]) + && layer.feature_table != earlier.feature_table + { + self.report.error(format!( + "`{}` shares the mesh of `{}` but not its feature table", + layer.id, earlier.id + )); + } + } let entries = board .layers .iter() - .map(|l| (l.id.as_str(), l.node, l.feature_table, Some(l))) + .zip(&repeats) + .filter(|(_, earlier)| earlier.is_none()) + .map(|(l, _)| (l.id.as_str(), l.node, l.feature_table, Some(l))) .chain( board .drills @@ -1444,6 +1532,78 @@ mod tests { assert_eq!(check(&root, &bin), Vec::::new()); } + /// The `stacked-sheets` sample: PREPREG_2 repeats the core's sheet. + fn stacked() -> (Root, Vec) { + let xml = + include_bytes!("../../../spec/samples/hand-written/stacked-sheets/stacked-sheets.xml"); + let glb = crate::convert(xml, &crate::Options::default()).unwrap().glb; + let (root, bin) = glb::read(&glb).unwrap(); + (root, bin.to_vec()) + } + + fn layer<'b>(board: &'b Board, name: &str) -> &'b BoardLayer { + board.layers.iter().find(|l| l.name == name).unwrap() + } + + #[test] + fn dielectrics_may_share_a_mesh_moved_in_z() { + let (root, bin) = stacked(); + assert_eq!(check(&root, &bin), Vec::::new()); + let b = board(&root); + let (core, prepreg) = (layer(&b, "CORE"), layer(&b, "PREPREG_2")); + let (core_node, prepreg_node) = ( + &root.nodes[core.node as usize], + &root.nodes[prepreg.node as usize], + ); + assert!(core_node.is_identity()); + assert_eq!(prepreg_node.mesh, core_node.mesh); + assert_eq!(prepreg.feature_table, core.feature_table); + let [_, s, _] = prepreg_node.scale.unwrap(); + assert!((s - 0.2).abs() < 1e-9, "{s}"); + let other = layer(&b, "PREPREG_1"); + assert_ne!(root.nodes[other.node as usize].mesh, core_node.mesh); + } + + #[test] + fn shared_meshes_are_checked() { + let (root, bin) = stacked(); + let b = board(&root); + let (core, prepreg) = (layer(&b, "CORE"), layer(&b, "PREPREG_2")); + + let mut moved = root.clone(); + moved.nodes[prepreg.node as usize].translation = Some([0.0, 0.0, 0.0]); + assert_error(&check(&moved, &bin), "must scale and move it along Y only"); + let mut rotated = root.clone(); + rotated.nodes[prepreg.node as usize].rotation = Some([0.0, 0.0, 1.0, 0.0]); + assert_error( + &check(&rotated, &bin), + "must scale and move it along Y only", + ); + + let mut copper = root.clone(); + copper.nodes[layer(&b, "IN2").node as usize].mesh = root.nodes[core.node as usize].mesh; + assert_error( + &check(&copper, &bin), + "only dielectric layers may share one", + ); + + let mut tables = root.clone(); + let mut changed = b.clone(); + let top = layer(&b, "TOP").feature_table; + changed + .layers + .iter_mut() + .find(|l| l.name == "PREPREG_2") + .unwrap() + .feature_table = top; + set_board(&mut tables, &changed); + assert_error(&check(&tables, &bin), "but not its feature table"); + + let mut drill = root.clone(); + drill.nodes[b.drills[0].node as usize].mesh = root.nodes[core.node as usize].mesh; + assert_error(&check(&drill, &bin), "is used by another node"); + } + #[test] fn extensions_must_be_declared_but_not_required() { let (mut root, bin) = sample(); @@ -1664,6 +1824,7 @@ mod tests { color: None, mesh: mesh.finish(), features: vec![row(0), row(1)], + repeats: None, }], drills: Vec::new(), nets: Vec::new(), diff --git a/crates/boardui-convert/tests/conformance.rs b/crates/boardui-convert/tests/conformance.rs index 591eafa..d7e5bd3 100644 --- a/crates/boardui-convert/tests/conformance.rs +++ b/crates/boardui-convert/tests/conformance.rs @@ -12,7 +12,7 @@ //! with a message if its file is missing, or fails if `BOARDUI_REQUIRE_IPC_TESTCASES` is set //! (CI sets it). -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::Write as _; use std::io::Write as _; use std::path::{Path, PathBuf}; @@ -187,6 +187,7 @@ samples! { pad_stacks => "pad-stacks", pad_stacks_drill_layer => "pad-stacks-drill-layer", conductor_core => "conductor-core", + stacked_sheets => "stacked-sheets", bottom_placement => "bottom-placement", paste_layer => "paste-layer", drawing_layers => "drawing-layers", @@ -864,13 +865,19 @@ fn summary(conversion: &Conversion, detailed: bool) -> String { } } let strings = |table: Option, name: &str| strings(&root, bin, table, name); + // Spec §4: a dielectric that shares the mesh of an earlier one. + let mut first_with_mesh = HashMap::new(); let entries = board .layers .iter() .map(|l| { + let shared = root.nodes[l.node as usize].mesh.and_then(|mesh| { + let first = *first_with_mesh.entry(mesh).or_insert(&l.id); + (first != &l.id).then(|| format!(" mesh of {first}")) + }); ( format!( - "layer {} {:?} {:?} z {:.4}..{:.4} mm {:?}{}{}{}", + "layer {} {:?} {:?} z {:.4}..{:.4} mm {:?}{}{}{}{}", l.id, l.role, l.side, @@ -879,7 +886,8 @@ fn summary(conversion: &Conversion, detailed: bool) -> String { l.thickness_source, if l.synthesized { " synthesized" } else { "" }, if l.visible { "" } else { " hidden" }, - source_colors(&root, l.node) + source_colors(&root, l.node), + shared.unwrap_or_default() ), l.node, l.feature_table, diff --git a/crates/boardui-geom/src/mesh.rs b/crates/boardui-geom/src/mesh.rs index 83a47fb..55b9bef 100644 --- a/crates/boardui-geom/src/mesh.rs +++ b/crates/boardui-geom/src/mesh.rs @@ -78,41 +78,6 @@ pub struct LayerMesh { pub primitives: Vec, } -impl LayerMesh { - /// The mesh of regions extruded from board height `from.0` to `from.1` - /// ([`Region::extrude`](crate::Region::extrude)), moved to the heights `to`: the same - /// mesh as extruding the regions from `to.0` to `to.1`, without triangulating them again. - /// - /// Vertices at height `from.0` move to `to.0`, all others to `to.1`. Returns `None` when - /// extrusion rejects either range, or `from.0` and `from.1` are the same in `f32` so that - /// bottom and top vertices can't be told apart. - pub fn restacked(&self, from: (f64, f64), to: (f64, f64)) -> Option { - let valid = - |(z_min, z_max): (f64, f64)| z_min.is_finite() && z_max.is_finite() && z_min < z_max; - let (bottom, top) = (from.0 as f32, from.1 as f32); - if !valid(from) || !valid(to) || bottom == top { - return None; - } - // Board z is glTF y (spec §3). - let z = |y: f32| { - if y == bottom { - to.0 as f32 - } else { - to.1 as f32 - } - }; - let mut mesh = self.clone(); - for p in &mut mesh.primitives { - p.positions.iter_mut().for_each(|v| v[1] = z(v[1])); - let ranges = p.features.iter_mut().map(|f| (&mut f.min, &mut f.max)); - for (min, max) in ranges.chain([(&mut p.min, &mut p.max)]) { - (min[1], max[1]) = (z(min[1]), z(max[1])); - } - } - Some(mesh) - } -} - /// Builds a [`LayerMesh`] from the prisms of a layer's features (spec §4, §8.1). /// /// Features are packed greedily into primitives of at most [`MAX_PRIMITIVE_VERTICES`] @@ -340,45 +305,6 @@ mod tests { assert!(LayerMeshBuilder::new().finish().primitives.is_empty()); } - /// A plate with a hole, extruded from `z_min` to `z_max` as one feature. - fn plate(z_min: f64, z_max: f64) -> LayerMesh { - let region = rect(0.0, 0.0, 3e-3, 2e-3).difference(&rect(1e-3, 5e-4, 2e-3, 1.5e-3)); - let mut builder = LayerMeshBuilder::new(); - builder - .push(0, ®ion.extrude(z_min, z_max).unwrap()) - .unwrap(); - builder.finish() - } - - #[test] - fn restacking_needs_valid_distinct_heights() { - let mesh = plate(0.0, 1e-4); - assert_eq!( - mesh.restacked((0.0, 1e-4), (1e-3, 1.2e-3)), - Some(plate(1e-3, 1.2e-3)) - ); - assert_eq!(mesh.restacked((0.0, 1e-4), (1e-3, 1e-3)), None); - assert_eq!(mesh.restacked((0.0, 1e-4), (0.0, f64::NAN)), None); - assert_eq!(mesh.restacked((1e-4, 0.0), (0.0, 1e-4)), None); - let thin = plate(1.0, 1.0 + 1e-12); - assert_eq!(thin.restacked((1.0, 1.0 + 1e-12), (0.0, 1e-4)), None); - } - - proptest! { - /// Moving an extruded mesh to other heights gives exactly the mesh extruded there. - #[test] - fn restacking_equals_extruding_again( - z in 0.0..3e-3f64, - thickness in 1e-6..1e-3f64, - to in 0.0..3e-3f64, - to_thickness in 1e-9..1e-3f64, - ) { - let restacked = plate(z, z + thickness) - .restacked((z, z + thickness), (to, to + to_thickness)); - prop_assert_eq!(restacked, Some(plate(to, to + to_thickness))); - } - } - proptest! { /// Spec §8.1: within each primitive, features are contiguous in vertices and in /// indices, in ascending ID order, and reference only their own vertices. diff --git a/crates/boardui-gltf/src/board.rs b/crates/boardui-gltf/src/board.rs index 0a573ee..2432796 100644 --- a/crates/boardui-gltf/src/board.rs +++ b/crates/boardui-gltf/src/board.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; /// The profile version written by this crate (spec §11). -pub const PROFILE_VERSION: &str = "0.9"; +pub const PROFILE_VERSION: &str = "0.10"; /// Root extension `BOARDUI_board`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/crates/boardui-gltf/src/writer.rs b/crates/boardui-gltf/src/writer.rs index 7aa0ae8..8b50bcf 100644 --- a/crates/boardui-gltf/src/writer.rs +++ b/crates/boardui-gltf/src/writer.rs @@ -179,6 +179,17 @@ impl Transform { } } +/// The node transform that moves geometry from the board heights `from` to `to`: a scale and +/// a translation along glTF Y, which is board Z (spec §3, §4). +fn restack(from: (f64, f64), to: (f64, f64)) -> Transform { + let scale = (to.1 - to.0) / (from.1 - from.0); + Transform { + translation: [0.0, to.0 - scale * from.0, 0.0], + scale: [1.0, scale, 1.0], + ..Transform::default() + } +} + /// A row of a layer's or drill's feature table (spec §8.2). #[derive(Debug, Clone, PartialEq, Eq)] pub struct FeatureRow { @@ -224,10 +235,17 @@ pub struct LayerAsset { /// Base colour from the source, sRGB; `None` for the role's default (spec §6.10). A /// colour equal to the default is written as the default material. pub color: Option<[u8; 3]>, - /// Geometry; feature IDs are rows of `features`. + /// Geometry; feature IDs are rows of `features`. Not written when the layer shares the + /// mesh of a layer it [`repeats`](Self::repeats). pub mesh: LayerMesh, /// The feature table. pub features: Vec, + /// For a dielectric whose sheet is that of an earlier dielectric at other heights: the + /// index of that layer in [`BoardAsset::layers`]. If that layer has a mesh, this layer + /// shares it, scaled and moved in Z by its node transform, and shares its feature table + /// (spec §4); else this layer's own (empty) mesh is written. Both layers must have the + /// same features and material, and the earlier one must repeat no other layer. + pub repeats: Option, } /// A drill layer: the barrels of one drill span (spec §6.3). @@ -396,6 +414,10 @@ pub fn layer_id(name: &str, synthesized: bool) -> String { impl BoardAsset { /// Writes the asset as GLB. + /// + /// # Panics + /// + /// Panics if a layer [`repeats`](LayerAsset::repeats) a layer it can't repeat. pub fn to_glb(&self) -> Vec { Writer::default().write(self) } @@ -437,14 +459,49 @@ impl Writer { attributes: self.attributes_table(asset), }; - let mut board_layers = Vec::new(); - for layer in &asset.layers { + let mut board_layers: Vec = Vec::new(); + for (i, layer) in asset.layers.iter().enumerate() { let id = layer_id(&layer.name, layer.synthesized); - let table = self.feature_table(&id, &layer.features); let builtin = BuiltinMaterial::for_role(layer.role); let color = layer.color.filter(|&c| c != builtin.base_color()); let material = self.colored_material(builtin, color); - let node = self.layer_node(&id, &layer.mesh, material, table, layer.features.len()); + // A repeated sheet shares the earlier layer's mesh, if it has one, and its table. + let shared = layer.repeats.and_then(|j| { + let earlier = &asset.layers[j]; + let earlier_color = earlier.color.filter(|&c| c != builtin.base_color()); + assert!( + j < i + && earlier.repeats.is_none() + && layer.role == Role::Dielectric + && earlier.role == Role::Dielectric + && earlier.features == layer.features + && earlier_color == color, + "layer `{}` can't repeat layer `{}` (spec §4)", + layer.name, + earlier.name + ); + Some((j, self.out.root.nodes[board_layers[j].node as usize].mesh?)) + }); + let (node, table) = match shared { + Some((j, mesh)) => { + let earlier = &asset.layers[j]; + let mut node = Node { + mesh: Some(mesh), + ..named(&id) + }; + restack((earlier.z_min, earlier.z_max), (layer.z_min, layer.z_max)) + .apply(&mut node); + (self.node(node), board_layers[j].feature_table) + } + None => { + let table = self.feature_table(&id, &layer.features); + let rows = layer.features.len(); + ( + self.layer_node(&id, &layer.mesh, material, table, rows), + table, + ) + } + }; self.out.root.nodes[layers as usize].children.push(node); board_layers.push(BoardLayer { id, diff --git a/docs/adr/0016-shared-dielectric-sheets.md b/docs/adr/0016-shared-dielectric-sheets.md new file mode 100644 index 0000000..2c5cfcf --- /dev/null +++ b/docs/adr/0016-shared-dielectric-sheets.md @@ -0,0 +1,43 @@ +# 0016 Shared dielectric sheets + +**Status:** Accepted, 2026-10-07. Amends [ADR 0008](0008-hybrid-metadata.md) (profile 0.10). + +## Context + +A dielectric layer is one sheet: the board outline minus the holes that cross it (spec §6.7). +On a board whose holes all go through, every dielectric has the same sheet at a different +height. IPC consortium test case 1 (full) has 11 of them: 7.0 MB each, 77 MB of a 277 MB GLB, +4.2 M of 17.25 M triangles. Spec §4 required identity transforms on layer nodes and each layer's +primitives to name that layer's feature table, so every sheet was stored again. + +## Decision + +- **A repeated sheet shares the mesh of the first dielectric with that sheet.** Its node uses + the same mesh with a transform that scales and moves it along Y only (`translation [0, t, 0]`, + `scale [1, s, 1]`), mapping the first layer's Z range onto its own. The first layer keeps an + identity transform and the mesh stays in board coordinates at its height, so the asset still + reads as before for everything but the repeats, and the rule is easy to check. + - Rejected: a mesh in a unit Z range with a transform on every user. No node would hold board + coordinates, and the shared case would differ from every other layer. +- **The repeat shares the first layer's feature table too.** A primitive names exactly one + `propertyTable`, and a shared primitive can't name each user's own. Sharing the table keeps + `EXT_mesh_features` consistent: every primitive's table is the `featureTable` of every layer + that shows it. The tables would be identical anyway (one `SHEET` row), and feature IDs derive + from the layer, not from the table (spec §5). + - Rejected: keeping a table per layer and letting `BOARDUI_board.layers[].featureTable` + override the primitive's. Generic `EXT_mesh_features` readers would see the other layer's + table. +- **Only dielectric sheets.** They are the only layers that repeat in practice, and the converter + knows they repeat without comparing geometry: the same drills cross them. Soldermask sheets + differ by their openings, copper, paste and drawing layers never match. Widening the rule later + is a compatible change; narrowing it would not be. +- Layers share only with the same colour (the material is the mesh's), and a sheet without + geometry shares nothing. + +## Consequences + +- testcase1-RevC-full: 277 MB → 203 MB; conversion skips extruding and writing 10 sheets. +- Readers that merge layer meshes must apply node transforms. The boardui viewer always did + (`mergeLayer` uses `matrixWorld`); generic glTF viewers do anyway. A reader that assumed + identity transforms on layer nodes draws a repeat at the first layer's height. +- The profile is still a draft, so the relaxed rule goes into a minor version, 0.10 (spec §11). diff --git a/docs/adr/README.md b/docs/adr/README.md index d85d660..de79de3 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -19,3 +19,4 @@ Each record captures one decision from the 3D rewrite design. Format: context, d | [0013](0013-panels.md) | Panels: root step by default, instances with their own IDs and nets, baked geometry | Accepted | | [0014](0014-runtime-model-sources.md) | Runtime model sources: models loaded by the viewer from servers (KiCad via GitLab), STEP in `@boardui/models` | Accepted | | [0015](0015-agpl-plus-commercial-licence.md) | Licence: AGPL-3.0-only plus a commercial licence; contributions under a CLA | Accepted | +| [0016](0016-shared-dielectric-sheets.md) | Shared dielectric sheets: repeats use the first sheet's mesh, moved in Z, and its feature table (profile 0.10) | Accepted | diff --git a/docs/architecture.md b/docs/architecture.md index cf64612..d705319 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -79,7 +79,7 @@ That keeps every dependency pure Rust, so the WASM build needs no C/C++ toolchai - Apply negative polarity in document order. - Resolve overlaps by priority (spec §6.2). Features go in priority order; each one subtracts the already-claimed area near it (found via the R-tree) and then claims its own. 4. **Holes and outline.** Subtract the holes from every layer they cross, and build barrels. The board outline, for the dielectric and soldermask sheets, is the union of the step profiles; a profile without `Cutout`s loses the closed contours that a `BOARD_OUTLINE` layer draws inside it, as KiCad writes them (spec §6.7). -5. **Extrude and triangulate.** Triangulate each region, emit caps and side walls, write features contiguously, and split into primitives of at most 65,535 vertices. Dielectrics crossed by the same drills have the same sheet: it is cut and triangulated once, and its mesh is moved to the other dielectrics' heights. +5. **Extrude and triangulate.** Triangulate each region, emit caps and side walls, write features contiguously, and split into primitives of at most 65,535 vertices. Dielectrics crossed by the same drills have the same sheet: it is cut and triangulated once, and the other dielectrics share its mesh, scaled and moved in Z by their node transforms (spec §4). 6. **Components.** Build placeholder bodies, or user models from the mapping file, with one shared mesh per package or model. 7. **Write.** Emit materials, nodes, meshes, `EXT_mesh_features`, the `EXT_structural_metadata` tables and `BOARDUI_board`, then pack the GLB. diff --git a/packages/viewer/test/runtime-models.test.ts b/packages/viewer/test/runtime-models.test.ts index f293574..d7c7d1e 100644 --- a/packages/viewer/test/runtime-models.test.ts +++ b/packages/viewer/test/runtime-models.test.ts @@ -196,7 +196,7 @@ describe('runtime models', () => { MPN: 'RC0603FR-0710KL', LCSC: 'C25744', }); - expect(boards.at(-1)?.profileVersion).toBe('0.9'); + expect(boards.at(-1)?.profileVersion).toBe('0.10'); }); it('replace placeholder bodies and pin-1 markers on the same state texels, and revert', async () => { diff --git a/packages/viewer/test/samples.test.ts b/packages/viewer/test/samples.test.ts index 94974d9..cc44142 100644 --- a/packages/viewer/test/samples.test.ts +++ b/packages/viewer/test/samples.test.ts @@ -1,10 +1,12 @@ import { readdirSync, readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { Ray, Vector3 } from 'three'; import { describe, expect, it } from 'vitest'; import type { BoardLayerJson } from '../src/board-extension.js'; import { BoardModel } from '../src/board-model.js'; import { loadGltf } from '../src/load.js'; +import { Picker } from '../src/picking.js'; // The converter's expected outputs (spec/samples/hand-written/*/*.glb), so the viewer is // tested on real assets, including ones that omit empty tables (spec §8.2). @@ -289,3 +291,39 @@ describe('BOM attributes (spec §8.2–§8.4)', () => { expect(model.describe('board')?.properties.source).not.toHaveProperty('software'); }); }); + +describe('repeated sheets (spec §4)', () => { + it('draws a sheet that shares a mesh at its own heights', async () => { + const model = await load('stacked-sheets'); + for (const name of ['PREPREG_1', 'CORE', 'PREPREG_2']) { + const layer = model.layer(`layer/${name}`); + if (!layer) throw new Error(`layer/${name} is missing`); + const { zMin, zMax } = layer.info as BoardLayerJson; + const geometry = layer.meshes[0]?.geometry; + expect(geometry?.boundingBox?.min.y).toBeCloseTo(zMin, 8); + expect(geometry?.boundingBox?.max.y).toBeCloseTo(zMax, 8); + const box = model.resolve(`feat/${name}/0`)?.box; + expect(box?.min.y).toBeCloseTo(zMin, 8); + expect(box?.max.y).toBeCloseTo(zMax, 8); + expect(model.describe(`feat/${name}/0`)?.properties).toMatchObject({ kind: 'SHEET' }); + } + // The prepreg shares the core's mesh and feature table; the upper prepreg has its own. + const table = (name: string) => model.layer(`layer/${name}`)?.table; + expect(table('PREPREG_2')).toBe(table('CORE')); + expect(table('PREPREG_1')).not.toBe(table('CORE')); + }); + + it('picks a shared sheet at its own heights', async () => { + const model = await load('stacked-sheets'); + const picker = new Picker(model); + for (const name of ['PREPREG_1', 'CORE', 'PREPREG_2']) { + const info = model.layer(`layer/${name}`)?.info as BoardLayerJson | undefined; + const z = info ? (info.zMin + info.zMax) / 2 : Number.NaN; + // Towards the board edge at x = 8 mm, from the side, between two vias. + const ray = new Ray(new Vector3(0.05, z, -1e-3), new Vector3(-1, 0, 0)); + const hit = picker.pick(ray); + expect(hit && model.idOfTexel(hit.texel)).toBe(`feat/${name}/0`); + expect(hit?.point.x).toBeCloseTo(8e-3, 8); + } + }); +}); diff --git a/spec/README.md b/spec/README.md index 4c5da76..0f6bdac 100644 --- a/spec/README.md +++ b/spec/README.md @@ -1,6 +1,6 @@ # boardui glTF profile -**Version 0.9 — draft** +**Version 0.10 — draft** This document specifies how boardui represents a printed circuit board as a glTF 2.0 asset. The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as described in [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119). @@ -53,8 +53,13 @@ board root node ``` - The root node MUST be named `board`, with child group nodes `layers`, `drills` and `components`. Group nodes MAY be empty. -- Layer and drill nodes MUST be named by their layer ID (§5), have identity transforms and hold geometry in board coordinates. -- Each layer and drill node with geometry has one mesh. A layer without geometry has no mesh (glTF meshes can't be empty). +- Layer and drill nodes MUST be named by their layer ID (§5), have identity transforms and hold geometry in board coordinates. The one exception is a repeated sheet (below), whose node scales and moves a shared mesh in Z. +- Each layer and drill node with geometry has one mesh. A layer without geometry has no mesh (glTF meshes can't be empty). A layer or drill mesh MUST NOT be used by any other node, except by repeated sheets. +- **Repeated sheets** (since 0.10). Dielectric layers crossed by the same holes have the same sheet (§6.7) at different heights. A dielectric layer whose sheet is that of an earlier dielectric layer (in `layers` order) MAY share that layer's mesh instead of having its own ([ADR 0016](../docs/adr/0016-shared-dielectric-sheets.md)): + - The mesh belongs to the first layer whose node uses it. That node has an identity transform, and the mesh is in board coordinates at its Z range. Only dielectric layers MAY use the mesh of another layer. + - The repeating layer's node uses the same mesh, with a `translation` of `[0, t, 0]` and a `scale` of `[1, s, 1]`, `s > 0` (either is omitted when it is the identity), and no `rotation` or `matrix`. The transform MUST map the first layer's Z range `[zMin₁, zMax₁]` onto the layer's own, within float32 precision: `s · zMin₁ + t = zMin` and `s · zMax₁ + t = zMax`, so `s = (zMax − zMin) / (zMax₁ − zMin₁)`. Its geometry in board coordinates is the mesh transformed by the node. + - The repeating layer shares the first layer's feature table too (§8.1, §8.2): both have one row, the sheet. The material is the mesh's, so the two layers have the same colour (§6.10). + - Other layers don't repeat in practice (soldermask sheets differ by their openings), so only dielectric sheets are shared. Viewers that merge layer meshes in board coordinates apply the node transform; generic glTF viewers do so anyway. - A mesh MAY have several primitives, for example chunks of the same material. Converters SHOULD keep each primitive at or below 65,535 vertices so indices fit `UNSIGNED_SHORT`. A feature MUST NOT span two primitives. ## 5. Element identifiers @@ -181,7 +186,7 @@ A feature whose region becomes empty keeps its metadata row (§8.2) and has no v ### 6.7 Dielectric and outline -Each dielectric layer is one sheet feature (kind `SHEET`): the step profile, including its cutouts, minus holes. +Each dielectric layer is one sheet feature (kind `SHEET`): the step profile, including its cutouts, minus the holes that cross it. Dielectrics crossed by the same holes have the same sheet, which they MAY share (§4, since 0.10). **Cut-outs on a board outline layer.** KiCad writes the `Profile` as its outer polygon alone and draws the board's inner contours (holes, slots, the gaps of a panel) only on its `BOARD_OUTLINE` layer, `Edge.Cuts`, as separate `Line`s and `Arc`s. When a step's profile has no `Cutout`, the converter takes them from the step's `BOARD_OUTLINE` layers: @@ -331,7 +336,7 @@ This section describes how the boardui viewer adds models when it shows an asset Every primitive of a layer or drill mesh MUST carry exactly one `featureIds` entry with: - `attribute: 0` (vertex attribute `_FEATURE_ID_0`); -- `propertyTable`: that layer's feature table; +- `propertyTable`: that layer's feature table (for a repeated sheet, the table it shares, §4); - `featureCount`: the number of distinct feature IDs in the primitive. Feature IDs are row indices into the layer's feature table: @@ -355,7 +360,7 @@ Feature IDs are row indices into the layer's feature table: | `attributes` | `attribute` | attribute of a component (since 0.8) | | `` | `feature` | source feature of that layer (or drill layer) | -- Feature tables are named by their layer ID. +- Feature tables are named by their layer ID. A repeated sheet's table is the first layer's, with that layer's name (§4); its feature IDs still derive from its own layer (§5). - **Empty tables.** `EXT_structural_metadata` property tables need at least one row, and glTF buffer views at least one byte. A table without rows is therefore omitted (and so is its index in `BOARDUI_board`), and so is an optional `STRING` property whose values are all empty (its `noData` is `""`; for example `pin.name` on a board without pin names). - **References.** References between tables are row indices (`UINT32`); `4294967295` means none. For example, `feature.net` is a row in `nets`, and `pin.component` is a row in `components`. - **Feature ID strings** are not stored. They derive from the layer name and `feature.source` (§5). @@ -382,7 +387,7 @@ This is a root-level extension ([`schema/BOARDUI_board.schema.json`](schema/BOAR ```json "BOARDUI_board": { - "profileVersion": "0.9", + "profileVersion": "0.10", "source": { "format": "IPC-2581", "revision": "C", @@ -416,6 +421,7 @@ This is a root-level extension ([`schema/BOARDUI_board.schema.json`](schema/BOAR - `layers` is ordered top to bottom. `thickness` is the copper-to-copper thickness. - `tables.*` and `featureTable` are absent for tables without rows (§8.2). `tables.instances` is present only for panels (§6.14). +- `node` and `featureTable` of a repeated sheet (since 0.10) are its own node, which uses the shared mesh, and the shared table (§4). Two layers have the same `featureTable` only then. - `source.software` (since 0.8) is the software that wrote the source file: the `name`, `revision` and `vendor` of `HistoryRecord/FileRevision/SoftwarePackage`, as written. `revision` and `vendor` are absent when empty, and `software` when the file has no `SoftwarePackage`. - `role` is one of `COPPER`, `DIELECTRIC`, `SOLDERMASK`, `SILKSCREEN`, `PASTE`, `COURTYARD`, `ASSEMBLY`, `DOCUMENTATION`. `ipcFunction` keeps the source `layerFunction`, and is absent for synthesized layers. - `visible` is the suggested default visibility. Inner copper layers and the optional layers (paste and drawings, §6.11, §6.12) default to `false`, all other layers to `true`. Dielectric layers stay visible so that the board is opaque like a real one: with them hidden, the translucent soldermask (§7) would show the other side's copper and components through the board. @@ -458,7 +464,7 @@ Anchors (for example top-centre of the bounding box) are computed from these. `boardui validate ` checks the rules of this profile: - the extensions are declared as in §2; -- the scene structure follows §4, and IDs are unique and well-formed; +- the scene structure follows §4, and IDs are unique and well-formed; layer and drill nodes have identity transforms, except repeated sheets, whose transforms map their first layer's Z range onto their own and who share its feature table; - feature IDs are contiguous and ascending within primitives; - metadata references are in range, and component `extras` match the `components` and `attributes` tables; - the `attributes` table is in component order, without empty values or repeated names per component; @@ -472,7 +478,7 @@ It runs the Khronos validator too, when the `gltf_validator` binary is on `PATH` ## 11. Versioning -`profileVersion` is `major.minor`. Minor versions only add optional data. Readers MUST reject an unknown major version and SHOULD accept an unknown minor version. +`profileVersion` is `major.minor`; versions compare as integers, so 0.10 follows 0.9. Minor versions only add optional data; while the profile is a draft (0.x), a minor version may also relax a rule, as 0.10 does for the transforms of repeated sheets (§4). Readers MUST reject an unknown major version and SHOULD accept an unknown minor version. ## 12. Open questions diff --git a/spec/samples/README.md b/spec/samples/README.md index d5765d8..e5f48b1 100644 --- a/spec/samples/README.md +++ b/spec/samples/README.md @@ -22,6 +22,7 @@ The IPC consortium test cases (`ipc-testcases/`) aren't in the repository: the c | [`pad-stacks`](hand-written/pad-stacks/) | hand-written | `minimal-2layer` as revision B `PadStack`s, plus a mounting hole: pads with their pins, via lands, holes on a synthesized `@drill-TOP-BOTTOM`; §6.3 | | [`pad-stacks-drill-layer`](hand-written/pad-stacks-drill-layer/) | hand-written | revision A `PadStack` holes and an Altium-style `Drill Guide` layer: a hole on both is drilled once and takes the padstack's net, a hole only in a `PadStack` is added to the drill layer; §6.3 | | [`conductor-core`](hand-written/conductor-core/) | hand-written | Altium's core: a `CONDUCTOR` layer with `side="NONE"` and `materialType="FR-4"` (revision A) is a dielectric; §6.4 | +| [`stacked-sheets`](hand-written/stacked-sheets/) | hand-written | four copper layers, two through vias and a blind via: the lower prepreg repeats the core's sheet and shares its mesh, scaled and moved in Z, and its feature table; the upper prepreg has its own sheet; §4, §6.7 | | [`bottom-placement`](hand-written/bottom-placement/) | hand-written | an asymmetric package on the bottom side at 0°, 30°, 90° and 270°, and one on top; §6.8 | | [`paste-layer`](hand-written/paste-layer/) | hand-written | paste on both sides, one thickness from the stack-up and one by default, standing on the copper through mask and silkscreen; a pasted through-hole land cut by its hole; §6.11 | | [`drawing-layers`](hand-written/drawing-layers/) | hand-written | courtyard, assembly and documentation layers stacked outside the board, an empty documentation layer and a glue layer left out, and a package assembly drawing not repeated because the assembly layer has content; §6.12, §6.13 | diff --git a/spec/samples/expected/blind-buried-vias.snap b/spec/samples/expected/blind-buried-vias.snap index bf6899f..3455c36 100644 --- a/spec/samples/expected/blind-buried-vias.snap +++ b/spec/samples/expected/blind-buried-vias.snap @@ -26,83 +26,83 @@ layer layer/DIELECTRIC_2 Dielectric Internal z 1.0056..1.0378 mm File 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In2.Cu Copper Internal z 0.9356..1.0056 mm File hidden 4 features {"Via": 4}, 206 vertices, 416 triangles, top area 15.3537 mm² -layer layer/DIELECTRIC_3 Dielectric Internal z 0.9035..0.9356 mm File +layer layer/DIELECTRIC_3 Dielectric Internal z 0.9035..0.9356 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In3.Cu Copper Internal z 0.8335..0.9035 mm File hidden 4 features {"Via": 4}, 216 vertices, 436 triangles, top area 27.9407 mm² -layer layer/DIELECTRIC_4 Dielectric Internal z 0.8013..0.8335 mm File +layer layer/DIELECTRIC_4 Dielectric Internal z 0.8013..0.8335 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In4.Cu Copper Internal z 0.7313..0.8013 mm File hidden 4 features {"Via": 4}, 206 vertices, 416 triangles, top area 15.3537 mm² -layer layer/DIELECTRIC_5 Dielectric Internal z 0.6991..0.7313 mm File +layer layer/DIELECTRIC_5 Dielectric Internal z 0.6991..0.7313 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In5.Cu Copper Internal z 0.6291..0.6991 mm File hidden 4 features {"Via": 4}, 216 vertices, 436 triangles, top area 27.9407 mm² -layer layer/DIELECTRIC_6 Dielectric Internal z 0.5970..0.6291 mm File +layer layer/DIELECTRIC_6 Dielectric Internal z 0.5970..0.6291 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In6.Cu Copper Internal z 0.5270..0.5970 mm File hidden 4 features {"Via": 4}, 206 vertices, 416 triangles, top area 15.3537 mm² -layer layer/DIELECTRIC_7 Dielectric Internal z 0.4948..0.5270 mm File +layer layer/DIELECTRIC_7 Dielectric Internal z 0.4948..0.5270 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In7.Cu Copper Internal z 0.4248..0.4948 mm File hidden 4 features {"Via": 4}, 216 vertices, 436 triangles, top area 27.9407 mm² -layer layer/DIELECTRIC_8 Dielectric Internal z 0.3926..0.4248 mm File +layer layer/DIELECTRIC_8 Dielectric Internal z 0.3926..0.4248 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In8.Cu Copper Internal z 0.3226..0.3926 mm File hidden 4 features {"Via": 4}, 206 vertices, 416 triangles, top area 15.3537 mm² -layer layer/DIELECTRIC_9 Dielectric Internal z 0.2904..0.3226 mm File +layer layer/DIELECTRIC_9 Dielectric Internal z 0.2904..0.3226 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In9.Cu Copper Internal z 0.2204..0.2904 mm File hidden 4 features {"Via": 4}, 216 vertices, 436 triangles, top area 27.9407 mm² -layer layer/DIELECTRIC_10 Dielectric Internal z 0.1883..0.2204 mm File +layer layer/DIELECTRIC_10 Dielectric Internal z 0.1883..0.2204 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In10.Cu Copper Internal z 0.1183..0.1883 mm File hidden 4 features {"Via": 4}, 206 vertices, 416 triangles, top area 15.3537 mm² -layer layer/DIELECTRIC_11 Dielectric Internal z 0.0861..0.1183 mm File +layer layer/DIELECTRIC_11 Dielectric Internal z 0.0861..0.1183 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In11.Cu Copper Internal z 0.0161..0.0861 mm File hidden 4 features {"Via": 4}, 216 vertices, 436 triangles, top area 27.9407 mm² -layer layer/DIELECTRIC_12 Dielectric Internal z -0.0161..0.0161 mm File +layer layer/DIELECTRIC_12 Dielectric Internal z -0.0161..0.0161 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In12.Cu Copper Internal z -0.0861..-0.0161 mm File hidden 4 features {"Via": 4}, 206 vertices, 416 triangles, top area 15.3537 mm² -layer layer/DIELECTRIC_13 Dielectric Internal z -0.1183..-0.0861 mm File +layer layer/DIELECTRIC_13 Dielectric Internal z -0.1183..-0.0861 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In13.Cu Copper Internal z -0.1883..-0.1183 mm File hidden 4 features {"Via": 4}, 216 vertices, 436 triangles, top area 27.9407 mm² -layer layer/DIELECTRIC_14 Dielectric Internal z -0.2204..-0.1883 mm File +layer layer/DIELECTRIC_14 Dielectric Internal z -0.2204..-0.1883 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In14.Cu Copper Internal z -0.2904..-0.2204 mm File hidden 4 features {"Via": 4}, 206 vertices, 416 triangles, top area 15.3537 mm² -layer layer/DIELECTRIC_15 Dielectric Internal z -0.3226..-0.2904 mm File +layer layer/DIELECTRIC_15 Dielectric Internal z -0.3226..-0.2904 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In15.Cu Copper Internal z -0.3926..-0.3226 mm File hidden 4 features {"Via": 4}, 216 vertices, 436 triangles, top area 27.9407 mm² -layer layer/DIELECTRIC_16 Dielectric Internal z -0.4248..-0.3926 mm File +layer layer/DIELECTRIC_16 Dielectric Internal z -0.4248..-0.3926 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In16.Cu Copper Internal z -0.4948..-0.4248 mm File hidden 4 features {"Via": 4}, 206 vertices, 416 triangles, top area 15.3537 mm² -layer layer/DIELECTRIC_17 Dielectric Internal z -0.5270..-0.4948 mm File +layer layer/DIELECTRIC_17 Dielectric Internal z -0.5270..-0.4948 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In17.Cu Copper Internal z -0.5970..-0.5270 mm File hidden 4 features {"Via": 4}, 216 vertices, 436 triangles, top area 27.9407 mm² -layer layer/DIELECTRIC_18 Dielectric Internal z -0.6291..-0.5970 mm File +layer layer/DIELECTRIC_18 Dielectric Internal z -0.6291..-0.5970 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In18.Cu Copper Internal z -0.6991..-0.6291 mm File hidden 4 features {"Via": 4}, 206 vertices, 416 triangles, top area 15.3537 mm² -layer layer/DIELECTRIC_19 Dielectric Internal z -0.7313..-0.6991 mm File +layer layer/DIELECTRIC_19 Dielectric Internal z -0.7313..-0.6991 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In19.Cu Copper Internal z -0.8013..-0.7313 mm File hidden 4 features {"Via": 4}, 216 vertices, 436 triangles, top area 27.9407 mm² -layer layer/DIELECTRIC_20 Dielectric Internal z -0.8335..-0.8013 mm File +layer layer/DIELECTRIC_20 Dielectric Internal z -0.8335..-0.8013 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In20.Cu Copper Internal z -0.9035..-0.8335 mm File hidden 4 features {"Via": 4}, 206 vertices, 416 triangles, top area 15.3537 mm² -layer layer/DIELECTRIC_21 Dielectric Internal z -0.9356..-0.9035 mm File +layer layer/DIELECTRIC_21 Dielectric Internal z -0.9356..-0.9035 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In21.Cu Copper Internal z -1.0056..-0.9356 mm File hidden 4 features {"Via": 4}, 216 vertices, 436 triangles, top area 27.9407 mm² -layer layer/DIELECTRIC_22 Dielectric Internal z -1.0378..-1.0056 mm File +layer layer/DIELECTRIC_22 Dielectric Internal z -1.0378..-1.0056 mm File mesh of layer/DIELECTRIC_2 1 features {"Sheet": 1}, 98 vertices, 204 triangles, top area 1325.4466 mm² layer layer/In22.Cu Copper Internal z -1.1078..-1.0378 mm File hidden 6 features {"Via": 6}, 266 vertices, 544 triangles, top area 15.0448 mm² diff --git a/spec/samples/expected/royalblue54l-feather.snap b/spec/samples/expected/royalblue54l-feather.snap index 19bd375..b408bfa 100644 --- a/spec/samples/expected/royalblue54l-feather.snap +++ b/spec/samples/expected/royalblue54l-feather.snap @@ -24,27 +24,27 @@ layer layer/DIELECTRIC_1 Dielectric Internal z 0.6550..0.7550 mm File 1 features {"Sheet": 1}, 6610 vertices, 14212 triangles, top area 1266.5260 mm² layer layer/In1.Cu Copper Internal z 0.6200..0.6550 mm File hidden 270 features {"Pad": 86, "Via": 184}, 24804 vertices, 50496 triangles, top area 1097.1125 mm² -layer layer/DIELECTRIC_2 Dielectric Internal z 0.3200..0.6200 mm File +layer layer/DIELECTRIC_2 Dielectric Internal z 0.3200..0.6200 mm File mesh of layer/DIELECTRIC_1 1 features {"Sheet": 1}, 6610 vertices, 14212 triangles, top area 1266.5260 mm² layer layer/In2.Cu Copper Internal z 0.2850..0.3200 mm File hidden 283 features {"Fill": 6, "Pad": 86, "Trace": 6, "Via": 185}, 34108 vertices, 68580 triangles, top area 1014.8800 mm² -layer layer/DIELECTRIC_3 Dielectric Internal z 0.1850..0.2850 mm File +layer layer/DIELECTRIC_3 Dielectric Internal z 0.1850..0.2850 mm File mesh of layer/DIELECTRIC_1 1 features {"Sheet": 1}, 6610 vertices, 14212 triangles, top area 1266.5260 mm² layer layer/In3.Cu Copper Internal z 0.1500..0.1850 mm File hidden 270 features {"Pad": 86, "Via": 184}, 24804 vertices, 50496 triangles, top area 1097.1125 mm² -layer layer/DIELECTRIC_4 Dielectric Internal z -0.1500..0.1500 mm File +layer layer/DIELECTRIC_4 Dielectric Internal z -0.1500..0.1500 mm File mesh of layer/DIELECTRIC_1 1 features {"Sheet": 1}, 6610 vertices, 14212 triangles, top area 1266.5260 mm² layer layer/In4.Cu Copper Internal z -0.1850..-0.1500 mm File hidden 299 features {"Fill": 13, "Pad": 86, "Trace": 16, "Via": 184}, 34658 vertices, 69400 triangles, top area 988.0248 mm² -layer layer/DIELECTRIC_5 Dielectric Internal z -0.2850..-0.1850 mm File +layer layer/DIELECTRIC_5 Dielectric Internal z -0.2850..-0.1850 mm File mesh of layer/DIELECTRIC_1 1 features {"Sheet": 1}, 6610 vertices, 14212 triangles, top area 1266.5260 mm² layer layer/In5.Cu Copper Internal z -0.3200..-0.2850 mm File hidden 289 features {"Fill": 9, "Pad": 86, "Trace": 10, "Via": 184}, 26680 vertices, 53996 triangles, top area 1024.6027 mm² -layer layer/DIELECTRIC_6 Dielectric Internal z -0.6200..-0.3200 mm File +layer layer/DIELECTRIC_6 Dielectric Internal z -0.6200..-0.3200 mm File mesh of layer/DIELECTRIC_1 1 features {"Sheet": 1}, 6610 vertices, 14212 triangles, top area 1266.5260 mm² layer layer/In6.Cu Copper Internal z -0.6550..-0.6200 mm File hidden 270 features {"Pad": 86, "Via": 184}, 24804 vertices, 50496 triangles, top area 1097.1125 mm² -layer layer/DIELECTRIC_7 Dielectric Internal z -0.7550..-0.6550 mm File +layer layer/DIELECTRIC_7 Dielectric Internal z -0.7550..-0.6550 mm File mesh of layer/DIELECTRIC_1 1 features {"Sheet": 1}, 6610 vertices, 14212 triangles, top area 1266.5260 mm² layer layer/B.Cu Copper Bottom z -0.7900..-0.7550 mm File 344 features {"Fill": 23, "Pad": 108, "Trace": 28, "Via": 185}, 31092 vertices, 61796 triangles, top area 964.5025 mm² diff --git a/spec/samples/expected/stacked-sheets.snap b/spec/samples/expected/stacked-sheets.snap new file mode 100644 index 0000000..38e35d1 --- /dev/null +++ b/spec/samples/expected/stacked-sheets.snap @@ -0,0 +1,48 @@ +--- +source: crates/boardui-convert/tests/conformance.rs +--- +source: IPC-2581 rev C, step board, FABRICATION +thickness: 1.5400 mm +counts: 18 features, 1178 vertices, 2384 triangles, 1 nets, 0 components, 0 pins (0 checked against pads) +warnings: 0 from the reader, 0 from the converter +layer layer/@soldermask-top Soldermask Top z 0.7350..0.7900 mm Default synthesized + 1 features {"Sheet": 1}, 92 vertices, 192 triangles, top area 39.7210 mm² + row 0: Sheet net - area 39.7210 mm² +layer layer/TOP Copper Top z 0.7350..0.7700 mm File + 3 features {"Via": 3}, 192 vertices, 384 triangles, top area 0.5521 mm² + row 0: Via net 0 area 0.1840 mm² + row 1: Via net 0 area 0.1840 mm² + row 2: Via net 0 area 0.1840 mm² +layer layer/PREPREG_1 Dielectric Internal z 0.5350..0.7350 mm File + 1 features {"Sheet": 1}, 92 vertices, 192 triangles, top area 39.7210 mm² + row 0: Sheet net - area 39.7210 mm² +layer layer/IN1 Copper Internal z 0.5000..0.5350 mm File hidden + 3 features {"Via": 3}, 192 vertices, 384 triangles, top area 0.5521 mm² + row 0: Via net 0 area 0.1840 mm² + row 1: Via net 0 area 0.1840 mm² + row 2: Via net 0 area 0.1840 mm² +layer layer/CORE Dielectric Internal z -0.5000..0.5000 mm File + 1 features {"Sheet": 1}, 64 vertices, 132 triangles, top area 39.8140 mm² + row 0: Sheet net - area 39.8140 mm² +layer layer/IN2 Copper Internal z -0.5350..-0.5000 mm File hidden + 2 features {"Via": 2}, 128 vertices, 256 triangles, top area 0.3680 mm² + row 0: Via net 0 area 0.1840 mm² + row 1: Via net 0 area 0.1840 mm² +layer layer/PREPREG_2 Dielectric Internal z -0.7350..-0.5350 mm File mesh of layer/CORE + 1 features {"Sheet": 1}, 64 vertices, 132 triangles, top area 39.8140 mm² + row 0: Sheet net - area 39.8140 mm² +layer layer/BOTTOM Copper Bottom z -0.7700..-0.7350 mm File + 2 features {"Via": 2}, 128 vertices, 256 triangles, top area 0.3680 mm² + row 0: Via net 0 area 0.1840 mm² + row 1: Via net 0 area 0.1840 mm² +layer layer/@soldermask-bottom Soldermask Bottom z -0.7900..-0.7350 mm Default synthesized + 1 features {"Sheet": 1}, 64 vertices, 132 triangles, top area 39.8140 mm² + row 0: Sheet net - area 39.8140 mm² +drill layer/DRILL_TOP-BOTTOM layer/TOP..layer/BOTTOM + 2 features {"Barrel": 2}, 108 vertices, 216 triangles, top area 0.0501 mm² + row 0: Barrel net 0 area 0.0250 mm² + row 1: Barrel net 0 area 0.0250 mm² +drill layer/DRILL_TOP-IN1 layer/TOP..layer/IN1 + 1 features {"Barrel": 1}, 54 vertices, 108 triangles, top area 0.0250 mm² + row 0: Barrel net 0 area 0.0250 mm² +components: 0 diff --git a/spec/samples/expected/testcase11-rdgflx-RevC-full.snap b/spec/samples/expected/testcase11-rdgflx-RevC-full.snap index af3c1f1..2dece41 100644 --- a/spec/samples/expected/testcase11-rdgflx-RevC-full.snap +++ b/spec/samples/expected/testcase11-rdgflx-RevC-full.snap @@ -44,15 +44,15 @@ layer layer/DIELECTRIC_INDEX_14 Dielectric Internal z 0.0686..0.1956 mm File 1 features {"Sheet": 1}, 6208 vertices, 13136 triangles, top area 10766.8032 mm² layer layer/FLEX_1 Copper Internal z 0.0381..0.0686 mm File hidden 240 features {"Fill": 6, "Pad": 6, "Trace": 51, "Via": 177}, 34196 vertices, 68444 triangles, top area 3014.9127 mm² -layer layer/DIELECTRIC_INDEX_16 Dielectric Internal z -0.0381..0.0381 mm File +layer layer/DIELECTRIC_INDEX_16 Dielectric Internal z -0.0381..0.0381 mm File mesh of layer/DIELECTRIC_INDEX_14 1 features {"Sheet": 1}, 6208 vertices, 13136 triangles, top area 10766.8032 mm² layer layer/FLEX_2 Copper Internal z -0.0686..-0.0381 mm File hidden 569 features {"Fill": 3, "Pad": 38, "Trace": 351, "Via": 177}, 43538 vertices, 80988 triangles, top area 6081.0809 mm² -layer layer/DIELECTRIC_INDEX_18 Dielectric Internal z -0.1956..-0.0686 mm File +layer layer/DIELECTRIC_INDEX_18 Dielectric Internal z -0.1956..-0.0686 mm File mesh of layer/DIELECTRIC_INDEX_14 1 features {"Sheet": 1}, 6208 vertices, 13136 triangles, top area 10766.8032 mm² layer layer/INT_4 Copper Internal z -0.2261..-0.1956 mm File hidden 271 features {"Fill": 1, "Pad": 34, "Trace": 59, "Via": 177}, 24098 vertices, 48128 triangles, top area 3984.4022 mm² -layer layer/DIELECTRIC_INDEX_20 Dielectric Internal z -0.4293..-0.2261 mm File +layer layer/DIELECTRIC_INDEX_20 Dielectric Internal z -0.4293..-0.2261 mm File mesh of layer/DIELECTRIC_INDEX_12 1 features {"Sheet": 1}, 3576 vertices, 7496 triangles, top area 10775.7880 mm² layer layer/BOTTOM Copper Bottom z -0.4597..-0.4293 mm File 89 features {"Fill": 1, "Pad": 4, "Trace": 1, "Via": 83}, 12526 vertices, 25384 triangles, top area 3927.1961 mm² diff --git a/spec/samples/hand-written/assembly-drawing/assembly-drawing.glb b/spec/samples/hand-written/assembly-drawing/assembly-drawing.glb index 7c9cc28..777cbe6 100644 Binary files a/spec/samples/hand-written/assembly-drawing/assembly-drawing.glb and b/spec/samples/hand-written/assembly-drawing/assembly-drawing.glb differ diff --git a/spec/samples/hand-written/board-sized-package/board-sized-package.glb b/spec/samples/hand-written/board-sized-package/board-sized-package.glb index 10853e3..0bd8845 100644 Binary files a/spec/samples/hand-written/board-sized-package/board-sized-package.glb and b/spec/samples/hand-written/board-sized-package/board-sized-package.glb differ diff --git a/spec/samples/hand-written/bom-attributes/bom-attributes.glb b/spec/samples/hand-written/bom-attributes/bom-attributes.glb index 47bccfb..f6d4427 100644 Binary files a/spec/samples/hand-written/bom-attributes/bom-attributes.glb and b/spec/samples/hand-written/bom-attributes/bom-attributes.glb differ diff --git a/spec/samples/hand-written/bottom-placement/bottom-placement.glb b/spec/samples/hand-written/bottom-placement/bottom-placement.glb index 1364cda..048639f 100644 Binary files a/spec/samples/hand-written/bottom-placement/bottom-placement.glb and b/spec/samples/hand-written/bottom-placement/bottom-placement.glb differ diff --git a/spec/samples/hand-written/colours/colours.glb b/spec/samples/hand-written/colours/colours.glb index 39dc847..7aaaceb 100644 Binary files a/spec/samples/hand-written/colours/colours.glb and b/spec/samples/hand-written/colours/colours.glb differ diff --git a/spec/samples/hand-written/conductor-core/conductor-core.glb b/spec/samples/hand-written/conductor-core/conductor-core.glb index e610e4b..bd20369 100644 Binary files a/spec/samples/hand-written/conductor-core/conductor-core.glb and b/spec/samples/hand-written/conductor-core/conductor-core.glb differ diff --git a/spec/samples/hand-written/drawing-layers/drawing-layers.glb b/spec/samples/hand-written/drawing-layers/drawing-layers.glb index 7b29671..30a9c25 100644 Binary files a/spec/samples/hand-written/drawing-layers/drawing-layers.glb and b/spec/samples/hand-written/drawing-layers/drawing-layers.glb differ diff --git a/spec/samples/hand-written/fiducials/fiducials.glb b/spec/samples/hand-written/fiducials/fiducials.glb index 0abb4f4..d5c7110 100644 Binary files a/spec/samples/hand-written/fiducials/fiducials.glb and b/spec/samples/hand-written/fiducials/fiducials.glb differ diff --git a/spec/samples/hand-written/generate.py b/spec/samples/hand-written/generate.py index 0209915..6d0b887 100644 --- a/spec/samples/hand-written/generate.py +++ b/spec/samples/hand-written/generate.py @@ -301,6 +301,48 @@ def conductor_core(): return d +def stacked_sheets(): + """Four copper layers with two through vias and a blind via from TOP to IN1: the core and + the lower prepreg are crossed by the same drills, so the prepreg shares the core's sheet + mesh, scaled and moved in Z (spec §4); the upper prepreg has its own (spec §6.7).""" + d = Doc() + d.primitive("VIA", f'') + d.layer("TOP", "CONDUCTOR", "TOP") + d.layer("PREPREG_1", "DIELPREG", "INTERNAL") + d.layer("IN1", "CONDUCTOR", "INTERNAL") + d.layer("CORE", "DIELCORE", "INTERNAL") + d.layer("IN2", "CONDUCTOR", "INTERNAL") + d.layer("PREPREG_2", "DIELPREG", "INTERNAL") + d.layer("BOTTOM", "CONDUCTOR", "BOTTOM") + d.layer("DRILL_TOP-BOTTOM", "DRILL", "ALL", span=("TOP", "BOTTOM")) + d.layer("DRILL_TOP-IN1", "DRILL", "INTERNAL", span=("TOP", "IN1")) + stack = [("TOP", 0.035), ("PREPREG_1", 0.2), ("IN1", 0.035), ("CORE", 1.0)] + stack += [("IN2", 0.035), ("PREPREG_2", 0.2), ("BOTTOM", 0.035)] + rows = "".join( + f'' + for i, (name, t) in enumerate(stack) + ) + d.layers.append(f'{rows}') + profile(d, 8, 5) + vias = { + "DRILL_TOP-BOTTOM": [(2, 2.5), (6, 2.5)], + "DRILL_TOP-IN1": [(4, 1.5)], + } + lands = {"TOP": vias["DRILL_TOP-BOTTOM"] + vias["DRILL_TOP-IN1"]} + lands["IN1"] = lands["TOP"] + lands["IN2"] = lands["BOTTOM"] = vias["DRILL_TOP-BOTTOM"] + for layer, points in lands.items(): + sets = "".join(f'{pad(d, x, y, "VIA")}' for x, y in points) + d.step.append(f'{sets}') + for (layer, points), prefix in zip(vias.items(), "TB"): + holes = "".join( + f'' + for k, (x, y) in enumerate(points) + ) + d.step.append(f'{holes}') + return d + + def overlap_priority(): """Pad over trace over plane, and ties in document order (spec §6.2).""" d = Doc() @@ -1609,6 +1651,7 @@ def main(): "pad-stacks": pad_stacks(), "pad-stacks-drill-layer": pad_stacks_drill_layer(), "conductor-core": conductor_core(), + "stacked-sheets": stacked_sheets(), "bottom-placement": bottom_placement(), "paste-layer": paste_layer(), "drawing-layers": drawing_layers(), diff --git a/spec/samples/hand-written/hatch-fill/hatch-fill.glb b/spec/samples/hand-written/hatch-fill/hatch-fill.glb index 6da9317..02e8e96 100644 Binary files a/spec/samples/hand-written/hatch-fill/hatch-fill.glb and b/spec/samples/hand-written/hatch-fill/hatch-fill.glb differ diff --git a/spec/samples/hand-written/hexagon-moire/hexagon-moire.glb b/spec/samples/hand-written/hexagon-moire/hexagon-moire.glb index c6c3baf..c2b9637 100644 Binary files a/spec/samples/hand-written/hexagon-moire/hexagon-moire.glb and b/spec/samples/hand-written/hexagon-moire/hexagon-moire.glb differ diff --git a/spec/samples/hand-written/line-styles/line-styles.glb b/spec/samples/hand-written/line-styles/line-styles.glb index 98e755b..8e8f2b1 100644 Binary files a/spec/samples/hand-written/line-styles/line-styles.glb and b/spec/samples/hand-written/line-styles/line-styles.glb differ diff --git a/spec/samples/hand-written/minimal-2layer/minimal-2layer.glb b/spec/samples/hand-written/minimal-2layer/minimal-2layer.glb index 382d0ab..df64e57 100644 Binary files a/spec/samples/hand-written/minimal-2layer/minimal-2layer.glb and b/spec/samples/hand-written/minimal-2layer/minimal-2layer.glb differ diff --git a/spec/samples/hand-written/negative-polarity/negative-polarity.glb b/spec/samples/hand-written/negative-polarity/negative-polarity.glb index bfa9482..9aad1e9 100644 Binary files a/spec/samples/hand-written/negative-polarity/negative-polarity.glb and b/spec/samples/hand-written/negative-polarity/negative-polarity.glb differ diff --git a/spec/samples/hand-written/outline-cutouts/outline-cutouts.glb b/spec/samples/hand-written/outline-cutouts/outline-cutouts.glb index 87e5fb9..c2084e9 100644 Binary files a/spec/samples/hand-written/outline-cutouts/outline-cutouts.glb and b/spec/samples/hand-written/outline-cutouts/outline-cutouts.glb differ diff --git a/spec/samples/hand-written/overlap-priority/overlap-priority.glb b/spec/samples/hand-written/overlap-priority/overlap-priority.glb index e41787f..98ed613 100644 Binary files a/spec/samples/hand-written/overlap-priority/overlap-priority.glb and b/spec/samples/hand-written/overlap-priority/overlap-priority.glb differ diff --git a/spec/samples/hand-written/package-silkscreen/package-silkscreen.glb b/spec/samples/hand-written/package-silkscreen/package-silkscreen.glb index ef58c47..7710d5f 100644 Binary files a/spec/samples/hand-written/package-silkscreen/package-silkscreen.glb and b/spec/samples/hand-written/package-silkscreen/package-silkscreen.glb differ diff --git a/spec/samples/hand-written/pad-stacks-drill-layer/pad-stacks-drill-layer.glb b/spec/samples/hand-written/pad-stacks-drill-layer/pad-stacks-drill-layer.glb index fdc2f4f..b81c75e 100644 Binary files a/spec/samples/hand-written/pad-stacks-drill-layer/pad-stacks-drill-layer.glb and b/spec/samples/hand-written/pad-stacks-drill-layer/pad-stacks-drill-layer.glb differ diff --git a/spec/samples/hand-written/pad-stacks/pad-stacks.glb b/spec/samples/hand-written/pad-stacks/pad-stacks.glb index 17a7fc5..ce69dca 100644 Binary files a/spec/samples/hand-written/pad-stacks/pad-stacks.glb and b/spec/samples/hand-written/pad-stacks/pad-stacks.glb differ diff --git a/spec/samples/hand-written/panel/panel.glb b/spec/samples/hand-written/panel/panel.glb index ab6308b..0304644 100644 Binary files a/spec/samples/hand-written/panel/panel.glb and b/spec/samples/hand-written/panel/panel.glb differ diff --git a/spec/samples/hand-written/paste-layer/paste-layer.glb b/spec/samples/hand-written/paste-layer/paste-layer.glb index daae8d1..9703791 100644 Binary files a/spec/samples/hand-written/paste-layer/paste-layer.glb and b/spec/samples/hand-written/paste-layer/paste-layer.glb differ diff --git a/spec/samples/hand-written/pinless-package/pinless-package.glb b/spec/samples/hand-written/pinless-package/pinless-package.glb index 013ae57..9861831 100644 Binary files a/spec/samples/hand-written/pinless-package/pinless-package.glb and b/spec/samples/hand-written/pinless-package/pinless-package.glb differ diff --git a/spec/samples/hand-written/polygon-xform/polygon-xform.glb b/spec/samples/hand-written/polygon-xform/polygon-xform.glb index 92326f3..0d4386a 100644 Binary files a/spec/samples/hand-written/polygon-xform/polygon-xform.glb and b/spec/samples/hand-written/polygon-xform/polygon-xform.glb differ diff --git a/spec/samples/hand-written/primitive-xform/primitive-xform.glb b/spec/samples/hand-written/primitive-xform/primitive-xform.glb index f6e55ec..8087d19 100644 Binary files a/spec/samples/hand-written/primitive-xform/primitive-xform.glb and b/spec/samples/hand-written/primitive-xform/primitive-xform.glb differ diff --git a/spec/samples/hand-written/slots/slots.glb b/spec/samples/hand-written/slots/slots.glb index b5475aa..d082039 100644 Binary files a/spec/samples/hand-written/slots/slots.glb and b/spec/samples/hand-written/slots/slots.glb differ diff --git a/spec/samples/hand-written/stacked-sheets/stacked-sheets.glb b/spec/samples/hand-written/stacked-sheets/stacked-sheets.glb new file mode 100644 index 0000000..ffb20d6 Binary files /dev/null and b/spec/samples/hand-written/stacked-sheets/stacked-sheets.glb differ diff --git a/spec/samples/hand-written/stacked-sheets/stacked-sheets.xml b/spec/samples/hand-written/stacked-sheets/stacked-sheets.xml new file mode 100644 index 0000000..b9c6644 --- /dev/null +++ b/spec/samples/hand-written/stacked-sheets/stacked-sheets.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spec/samples/hand-written/text/text.glb b/spec/samples/hand-written/text/text.glb index acefc65..de4a712 100644 Binary files a/spec/samples/hand-written/text/text.glb and b/spec/samples/hand-written/text/text.glb differ diff --git a/spec/samples/hand-written/units-inch/units-inch.glb b/spec/samples/hand-written/units-inch/units-inch.glb index fdb2784..daa6a3a 100644 Binary files a/spec/samples/hand-written/units-inch/units-inch.glb and b/spec/samples/hand-written/units-inch/units-inch.glb differ diff --git a/spec/samples/hand-written/units-micron/units-micron.glb b/spec/samples/hand-written/units-micron/units-micron.glb index e21753c..bbb80f8 100644 Binary files a/spec/samples/hand-written/units-micron/units-micron.glb and b/spec/samples/hand-written/units-micron/units-micron.glb differ diff --git a/spec/samples/hand-written/user-models/user-models.glb b/spec/samples/hand-written/user-models/user-models.glb index 2fbe60f..ead90b7 100644 Binary files a/spec/samples/hand-written/user-models/user-models.glb and b/spec/samples/hand-written/user-models/user-models.glb differ diff --git a/spec/samples/hand-written/zero-width-lines/zero-width-lines.glb b/spec/samples/hand-written/zero-width-lines/zero-width-lines.glb index 3af0dbf..8766dd9 100644 Binary files a/spec/samples/hand-written/zero-width-lines/zero-width-lines.glb and b/spec/samples/hand-written/zero-width-lines/zero-width-lines.glb differ diff --git a/spec/schema/BOARDUI_board.schema.json b/spec/schema/BOARDUI_board.schema.json index b2012f8..9b4cd3b 100644 --- a/spec/schema/BOARDUI_board.schema.json +++ b/spec/schema/BOARDUI_board.schema.json @@ -123,7 +123,7 @@ "visible": { "type": "boolean" }, "node": { "$ref": "#/$defs/index" }, "featureTable": { - "description": "The layer's feature table; absent when the layer has no features.", + "description": "The layer's feature table; absent when the layer has no features. A dielectric that shares an earlier dielectric's mesh shares its feature table too (spec §4).", "$ref": "#/$defs/index" } }, diff --git a/spec/schema/structural-metadata.json b/spec/schema/structural-metadata.json index e672a74..c2051f9 100644 --- a/spec/schema/structural-metadata.json +++ b/spec/schema/structural-metadata.json @@ -2,7 +2,7 @@ "id": "boardui", "name": "boardui PCB metadata", "description": "EXT_structural_metadata schema embedded in every boardui asset. See spec/README.md §8.2. References between tables are UINT32 row indices; 4294967295 means none.", - "version": "0.9", + "version": "0.10", "enums": { "FeatureKind": { "valueType": "UINT8",