PDF follow-ups: import layers, arcs and joins, names, background, page placement, snaps

- Unlayered raster images go with the PDF layer drawn last before them.
- Circular Bezier arcs import as arcs: bulged polyline segments with Join,
  ARC entities without it; straight chains still join.
- Imported PNGs go to "PDF Images" next to the PDF; import blocks are
  numbered imp31, imp32, ...
- Background darkness is its HSL lightness below one half.
- All chosen pages attach at the same insertion point.
- PDF geometry snaps as separate pieces: centre from the rim, intersections
  and perpendiculars.
- Automation: snap query with an optional base point.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-09-25 10:33:48 +03:00
1 parent 857f2f2e58
commit edbbc9d135
10 files changed
+347 -63

No files matched your search

+7 -10
View File
@@ -82,13 +82,8 @@ impl OpenCADStudio {
};
let mut result = pdf_import::convert(&content, &underlay, &area, &naming);
let images = std::mem::take(&mut result.images);
let image_dir = self.tabs[i]
.current_path
.as_deref()
.and_then(|p| p.parent())
.map(|p| p.to_path_buf())
.or_else(|| std::path::Path::new(&path).parent().map(|p| p.to_path_buf()))
.map(|dir| dir.join("PDF Images"));
// PDFIMPORTIMAGEPATH "PDF Images": a folder next to the PDF.
let image_dir = std::path::Path::new(&path).parent().map(|dir| dir.join("PDF Images"));
self.push_undo_snapshot(i, "PDFIMPORT");
let scene = &mut self.tabs[i].scene;
@@ -101,7 +96,7 @@ impl OpenCADStudio {
let _ = scene.document.line_types.add(lt);
}
// Raster images: each written as a PNG under "PDF Images" next to the
// drawing (or the PDF while the drawing is unsaved), then referenced.
// PDF, named after it with 8 hex digits, then referenced.
let stem = std::path::Path::new(&path.replace('\\', "/"))
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
@@ -128,7 +123,9 @@ impl OpenCADStudio {
raster.u_vector = image.u;
raster.v_vector = image.v;
raster.flags = codec::entities::ImageDisplayFlags::SHOW_IMAGE
| codec::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY;
| codec::entities::ImageDisplayFlags::SHOW_NOT_ALIGNED
| codec::entities::ImageDisplayFlags::USE_CLIPPING_BOUNDARY
| codec::entities::ImageDisplayFlags::TRANSPARENCY_ON;
raster.common.layer = image.layer.clone();
result.entities.push(codec::EntityType::RasterImage(raster));
}
@@ -154,8 +151,8 @@ impl OpenCADStudio {
let mut name = stem.clone();
let mut n = 1;
while scene.document.block_records.get(&name).is_some() {
name = format!("{stem}{n}");
n += 1;
name = format!("{stem}({n})");
}
if scene
.define_block_from_owned_entities(result.entities, &name, glam::DVec3::ZERO)
+23
View File
@@ -459,6 +459,7 @@ impl OpenCADStudio {
| "commands"
| "properties"
| "measure"
| "snap"
| "query"
| "records"
| "record_schema"
@@ -705,6 +706,28 @@ impl OpenCADStudio {
let response = match op {
"properties" => self.control_properties(),
"measure" => self.control_measure(&req),
// The object snap a cursor over `point` (world) would get;
// `from` is the base for perpendicular and tangent.
"snap" => {
let p = &req["point"];
let world = glam::DVec3::new(
p[0].as_f64().unwrap_or(0.0),
p[1].as_f64().unwrap_or(0.0),
p[2].as_f64().unwrap_or(0.0),
);
let from = req["from"].as_array().map(|f| {
glam::DVec3::new(
f.first().and_then(|v| v.as_f64()).unwrap_or(0.0),
f.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0),
f.get(2).and_then(|v| v.as_f64()).unwrap_or(0.0),
)
});
let i = self.active_tab;
match self.snap_query(i, world, from) {
Some(hit) => json!({"ok":true,"snap":format!("{:?}", hit.snap_type),"world":[hit.world.x,hit.world.y,hit.world.z]}),
None => json!({"ok":true,"snap":null}),
}
}
"xdata_get" => self.xdata_read(&req),
"get_selection" => self.control_get_selection(),
"history" => {
+43
View File
@@ -2872,6 +2872,49 @@ impl OpenCADStudio {
)
}
/// The object snap the cursor would get over `world` in the model view,
/// through the same candidate gathering and snapper as a cursor move
/// (automation's `snap` query).
pub(in crate::app) fn snap_query(
&mut self,
i: usize,
world: glam::DVec3,
from: Option<glam::DVec3>,
) -> Option<crate::snap::SnapResult> {
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
if vw <= 1.0 || vh <= 1.0 {
return None;
}
let tile_b = self.tabs[i].scene.active_model_tile_bounds(vw, vh);
let bounds = iced::Rectangle {
x: 0.0,
y: 0.0,
width: tile_b.width,
height: tile_b.height,
};
let (view_rot, eye, px) = {
let cam = self.tabs[i].scene.camera.borrow();
(cam.view_proj_rte(bounds), cam.eye(), cam.project(world, bounds)?)
};
let p = Point::new(px.x, px.y);
let raw = self.cursor_model_point(i, &None, p, bounds);
let all_wires = self.tabs[i].scene.hit_test_wires();
let candidates = self.tabs[i].scene.interaction_candidates_near(
all_wires,
raw,
view_rot,
eye,
bounds,
self.snapper.osnap_radius_px,
);
// Perpendicular and tangent measure from `from`, else the running
// command's last point.
self.snapper.from_point = from.or(self.last_point).map(|p| p.as_vec3());
let (go, gr) = self.drafting_grid_basis(i);
self.snapper
.snap(raw, p, &candidates, view_rot, eye, bounds, go, gr, None)
}
/// Apply one frame of a UCS-icon grip drag: map the cursor onto the active
/// UCS plane (so the move stays in-plane) and either slide the origin there
/// or rotate the chosen axis to point at it, keeping a right-handed frame