Compare commits

..

57 Commits

Author SHA1 Message Date
bherbruck d4f923a3e6 Release 0.40.0-rc.1 2026-11-03 02:20:23 -05:00
bherbruck bbb98928fb Release 0.40.0-rc.1 2026-11-03 01:00:12 -05:00
bherbruck 8e2b0cd60d Release 0.30.1 2026-11-02 11:11:46 -05:00
bherbruck afd81380ab Release 0.30.0 2026-11-01 21:55:18 -05:00
bherbruck 3a5195b0a0 Release 0.29.0 2026-10-31 19:20:30 -04:00
bherbruck ee6b85605a Release 0.28.0 2026-10-30 19:50:14 -04:00
bherbruck aa1fe28ddf Release 0.27.1 2026-10-29 23:34:31 -04:00
bherbruck 5b0eb08473 Release 0.27.0 2026-10-29 19:53:24 -04:00
bherbruck 9df8837d43 Release 0.26.1 2026-10-29 06:33:09 -04:00
bherbruck 409ab1ed45 Release 0.26.0 (zip 9.1.3) 2026-10-28 19:41:38 -04:00
bherbruck 5e03fe2f4b Release 0.26.0 2026-10-28 19:32:17 -04:00
bherbruck 37a586dd61 Release 0.25.1 2026-10-27 20:58:30 -04:00
bherbruck a79f40a711 Release 0.25.0 2026-10-27 18:05:39 -04:00
bherbruck b5728f284b Release 0.24.1 2026-10-27 08:34:27 -04:00
bherbruck 5965439a15 Release 0.24.0 2026-10-26 19:23:21 -04:00
bherbruck 586a358ed4 Release 0.23.1 2026-10-26 07:07:21 -04:00
bherbruck 7770f8982d Release 0.23.0 2026-10-25 18:18:39 -04:00
bherbruck 72ad6383ef Release 0.22.1 2026-10-25 10:38:39 -04:00
bherbruck 0537623a75 Release 0.22.0 2026-10-24 18:53:43 -04:00
bherbruck 65d288227e Release 0.21.0 (rc.2's code, version only)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 20:51:08 -04:00
bherbruck fedb3bcdb1 Release 0.21.0-rc.2 2026-10-23 17:14:38 -04:00
bherbruck 097b8b7550 Release 0.21.0-rc.1
The tree is main at f120285c9 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 13:48:05 -04:00
bherbruck af7ca3a8a2 Release 0.21.0-rc.1
The tree is main at b31fddb98 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 13:37:43 -04:00
bherbruck 27ddb725c7 Release 0.21.0-rc.1
The tree is main at 2bf3d4e11 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-23 12:10:02 -04:00
bherbruck a06bdf5cfc Release 0.20.0
The tree is main at 030187452 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-22 18:34:40 -04:00
bherbruck 33fdcdb3a0 Release 0.19.0
The tree is main at 5899e5b6c exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-21 21:13:33 -04:00
bherbruck c07ab5f8f9 Release 0.18.4
The tree is main at 728f753b8 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-21 12:26:34 -04:00
bherbruck fea32f8aac Release 0.18.3
The tree is main at 034aa60d4 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-21 08:46:37 -04:00
bherbruck 4927214976 Release 0.18.2
The tree is main at 57bcc4083 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-20 22:09:18 -04:00
bherbruck b621819c0c Release 0.18.1
The tree is main at 522ae7755 exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-20 17:39:11 -04:00
bherbruck ec552a5300 Release 0.18.0
The tree is main at c3151fb7f exactly; the previous release commit is kept as a parent so release only moves forward.
2026-10-20 16:28:59 -04:00
bherbruck e0cc740cc3 Release 0.17.0
The tree is main at 511faedcf exactly; the 0.16.x release commits are kept as a parent so release only moves forward.
2026-10-20 11:41:48 -04:00
bherbruck 5de48e916d Release 0.16.2 2026-10-19 00:39:30 +00:00
bherbruck 80af7bbe3e Release 0.16.2
Version 0.16.2: 0.16.1 with the rigid orbit fix (#2753). Same design format (27).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 00:38:53 +00:00
bherbruck 191e983423 Fix orbit about the picked point (regression in 0.16.0) (#2753)
Orbiting about the point under the cursor (or the origin) turned the camera
about its target and then panned to put the pivot back. The pan works at the
target's depth with the parallel-view scale, so in a perspective view the
pivot slid off its pixel and the model swung about the camera.

The camera now turns rigidly about the pivot: Camera::orbit_about keeps the
pivot's coordinates in the camera's frame and rebuilds the target from the
new orientation, so the pivot stays exactly on its pixel in both projections.
The Shift snap spring and view.navigate's pivot orbit use the same.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e88779218)
2026-10-19 00:38:52 +00:00
bherbruck 2b9445b049 Release 0.16.1 2026-10-18 20:16:21 +00:00
bherbruck 7ddf48ca16 Release 0.16.1
Version 0.16.1: 0.16.0 with the orbit fix (reverts #2142). Same design format (27).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:12:04 +00:00
bherbruck d2f6848377 Revert #2142: orbit about the picked point like 0.15.0
#2142 made orbiting about the point under the cursor turn the camera in place instead of turning the part about that point. Restore the 0.15.0 behaviour.

This reverts commit cac4bccea8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:12:00 +00:00
bherbruck 0b6d6fecd8 Release 0.16.0 2026-10-18 18:16:11 +00:00
bherbruck fa691707c2 Release 0.15.0 2026-10-17 13:54:24 +00:00
bherbruck b9a55c74e2 Release 0.15.0
Version 0.15.0; the pending design-format changes are folded into format 26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:53:44 +00:00
bherbruck 2bb9c81069 Release 0.14.0 2026-10-16 09:19:14 +00:00
bherbruck cedb51b8ea Release 0.14.0
Version 0.14.0; the pending design-format changes are folded into format 25.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:19:10 +00:00
bherbruck 66d13059dd Release 0.13.1 2026-10-15 21:56:43 +00:00
bherbruck 800ea40c0d Release 0.13.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:56:35 +00:00
bherbruck 20442eecd3 Agent chat on Windows: start npm agents through their .cmd shim
npm puts a Unix shell script with the bare name (codex-acp,
claude-code-acp, gemini) beside the .cmd shim. The lookup tried the bare
name first, so every npm-installed agent failed with "not a valid Win32
application (os error 193)". Windows now only considers .exe/.cmd/.bat,
and a command saved with the bare path runs the shim beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 60ea8c69d2)
2026-10-15 21:51:01 +00:00
bherbruck 22d1cd7032 Release 0.13.0 2026-10-15 20:34:13 +00:00
bherbruck 6a0dc18655 Fix the Windows 32-bit and macOS release builds
On 32-bit Windows SetWindowLongPtrW is SetWindowLongW, which takes and
returns i32, so the window-procedure hook didn't compile for i686.
The macOS app bundle now signs the bundled solidcraft-cli before the
bundle itself; codesign refuses a bundle with an unsigned helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 94e76cd97e)
2026-10-15 20:34:12 +00:00
bherbruck e006dbf1af Release 0.13.0
The tree is the 0.13.0 version commit (8241a750fa) on main exactly; the previous release is kept as a parent so release only moves forward.
2026-10-15 19:59:50 +00:00
bherbruck 8241a750fa Release 0.13.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:59:42 +00:00
bherbruck 41779cf7bb Release 0.12.0: release branch follows main again after the 0.11.1 patch
The tree is main at c36a6f9f exactly; the 0.11.1 patch commit is kept as a parent so release only moves forward.
2026-10-15 13:32:01 +00:00
bherbruck b01b9689c8 Release 0.11.1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:41:41 +00:00
bherbruck 4426da6f5d Windows: start Direct3D 12 first, and Vulkan only without a D3D12 GPU (#1190)
* Windows: start Direct3D 12 first, and Vulkan only without a D3D12 GPU

wgpu started Vulkan, D3D12 and OpenGL together and preferred Vulkan, so
every Windows start loaded all installed Vulkan drivers. An old Intel
Vulkan driver (igvk64.dll) crashed 0.10.1 at startup inside the driver.

The app now probes D3D12, then Vulkan, then OpenGL, one backend at a
time, and starts only the first with a hardware adapter (WARP when none
has one). WGPU_BACKEND still wins; graphics.json's "backend" sets the
order by hand; the software renderer (safe mode, the retry) uses D3D12
alone. A new Windows CI job starts the real app under several settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CI: a test filter YAML parses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CI: the startup smoke test writes graphics.json where the app reads it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CI: run the WARP shader tests one at a time

WARP's shader JIT (d3d10warp.dll) crashed twice in 124 runs of the gpu
tests with four test threads, with a null read inside its own optimizer
while several devices compiled compute pipelines at once. Serial runs
did not crash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 95ea05e7c8)
2026-10-14 20:41:37 +00:00
bherbruck 5ead82e627 Release 0.11.0: release branch follows main again after the 0.10.1 patch
The tree is main at cccbe125 exactly; c29b0d2f (0.10.1) is kept as a parent so release only moves forward.
2026-10-14 20:07:41 +00:00
bherbruck c29b0d2f13 Release 0.10.1 2026-10-14 14:41:19 +00:00
bherbruck 480fd8d517 Fix the startup crash on D3D12 with FXC: CPU fallback for the tessellation pipeline, FXC-friendly shader (#1098) (#1099)
* GPU tessellation: a pipeline the device cannot build falls back to CPU meshing instead of crashing (#1098)

Build the tessellation compute pipeline inside error scopes; TessGpu::new returns an error,
and the viewport logs it and meshes bodies on the CPU. A Windows CI job builds the pipeline
with D3D12's FXC on the WARP adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tessellation shader: basis functions in one-dimensional local arrays, so D3D12's FXC compiles it (#1098)

FXC has to unroll loops that write an array inside a struct (Basis.n0/n1) or a 2-D array
(ndu) by a counter, and cannot with run-time trip counts (X3511). A2.2 basis functions in a
local 1-D array returned whole; derivatives from the degree p-1 functions; the pole step
offsets computed rather than indexed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:41:16 +00:00
bherbruck 4c09fa5355 Hovered and selected faces light solidly again over GPU-drawn bodies (#1064)
Since the model's mesh went back to the reference mesher (#940), the GPU viewport drew
opaque bodies from their display plans while hover and selection lit faces with the
reference mesh's triangles. Two triangulations of the same face at equal depth fought, so
a hovered face shimmered in wedges instead of lighting whole.

Faces lit over a body drawn from its plan now use the plan's own triangles
(ModelBody::plan_mesh, evaluated once on the CPU), and the highlight pass gets a small
depth bias toward the eye so f32 rounding between the GPU and CPU evaluations of the same
triangles cannot speckle it either.

Test: every face of a box with a pocket and a round boss, hovered and rendered offscreen
through the viewport's real pipelines on the GPU path, lights the same pixels as on the
CPU path (skipped without a GPU adapter).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:56:47 +00:00
841 changed files with 6520 additions and 6037 deletions
+2 -6
View File
@@ -1,6 +1,5 @@
# The documentation site on GitHub Pages: the book (docs/book) at https://docs.solidcraft.io/
# and the web app at /app/ (the latest release), /app-rc/ (the newest release candidate) and
# /app-edge/ (this commit). Needs Settings → Pages → Source = GitHub Actions; skipped while the
# and the bundled web app at /app/. Needs Settings → Pages → Source = GitHub Actions; skipped while the
# repository is private.
name: Pages
@@ -67,10 +66,7 @@ jobs:
mkdir -p site
cp -r target/book/. site/
bash docs/book/seo.sh site # canonical links, robots.txt, sitemap.xml
- name: Add the web app channels (stable and release candidate from their releases, edge from this build)
env:
GH_TOKEN: ${{ github.token }}
run: bash packaging/web-channels.sh site dist/web
cp -r dist/web site/app
- uses: actions/configure-pages@v5
id: pages
- name: Install Flatpak repository tools
Generated
-23
View File
@@ -4059,16 +4059,6 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "solidcore-record"
version = "0.1.0"
source = "git+https://github.com/tangency-labs/solidcore?rev=f5e8e9a29e0c9b7cf6c4040a5bc8878382b3c62f#f5e8e9a29e0c9b7cf6c4040a5bc8878382b3c62f"
dependencies = [
"serde",
"serde_json",
"thiserror 2.0.21",
]
[[package]]
name = "solidcraft"
version = "0.40.0-rc.1"
@@ -4117,7 +4107,6 @@ dependencies = [
"image",
"log",
"serde_json",
"solidcore-record",
"solidcraft-engine",
"solidcraft-gpu",
"solidcraft-kernel",
@@ -4141,7 +4130,6 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-derive",
"solidcraft-geom",
"solidcraft-kernel",
@@ -4162,7 +4150,6 @@ dependencies = [
"serde",
"serde_json",
"serde_path_to_error",
"solidcore-record",
"solidcraft-doc",
"solidcraft-geom",
"solidcraft-io",
@@ -4181,7 +4168,6 @@ version = "0.40.0-rc.1"
dependencies = [
"serde",
"serde_json",
"solidcore-record",
"thiserror 2.0.21",
]
@@ -4196,7 +4182,6 @@ dependencies = [
"robust",
"serde",
"serde_json",
"solidcore-record",
"solidcraft-engine",
"solidcraft-geom",
"solidcraft-io",
@@ -4223,7 +4208,6 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-doc",
"solidcraft-geom",
"solidcraft-kernel",
@@ -4242,7 +4226,6 @@ dependencies = [
"rayon",
"serde",
"serde_json",
"solidcore-record",
"solidcraft-geom",
"solidcraft-profile",
"solidcraft-smlib",
@@ -4280,7 +4263,6 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-geom",
"web-time",
"weezl",
@@ -4293,7 +4275,6 @@ dependencies = [
"harfrust",
"serde",
"serde_json",
"solidcore-record",
"solidcraft-geom",
"thiserror 2.0.21",
"ttf-parser",
@@ -4308,7 +4289,6 @@ dependencies = [
"log",
"serde_json",
"solidcore",
"solidcore-record",
"solidcraft-geom",
"solidcraft-profile",
]
@@ -4324,7 +4304,6 @@ dependencies = [
name = "solidcraft-step"
version = "0.40.0-rc.1"
dependencies = [
"solidcore-record",
"solidcraft-geom",
"solidcraft-profile",
]
@@ -4350,7 +4329,6 @@ dependencies = [
"serde",
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-engine",
"solidcraft-gpu",
"solidcraft-space-mouse",
@@ -5776,7 +5754,6 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
name = "xtask"
version = "0.40.0-rc.1"
dependencies = [
"flate2",
"proc-macro2",
"quick-xml",
"serde",
-3
View File
@@ -36,9 +36,6 @@ solidcraft-3dx-client = { path = "crates/threedx-client" }
# local checkout, add `[patch."https://github.com/tangency-labs/solidcore"] solidcore = { path =
# "../solidcore/crates/solidcore" }` to a `.cargo/config.toml` outside the repository.
solidcore = { git = "https://github.com/tangency-labs/solidcore", rev = "8ddce901671f1cebd9214a31d21895d725e886c8" }
# Geometry as data (solidcore's layers §1, "Record"): the B-rep record, its structural validation
# and the native body file. Pinned like solidcore; for a local checkout, patch it as above.
solidcore-record = { git = "https://github.com/tangency-labs/solidcore", rev = "f5e8e9a29e0c9b7cf6c4040a5bc8878382b3c62f" }
# test tooling: a dev-dependency only (xtask/src/layers.rs)
solidcraft-testkit = { path = "crates/testkit" }
-1
View File
@@ -28,7 +28,6 @@ raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
profile = ["solidcraft-engine/profile"]
[dependencies]
solidcore-record = { workspace = true }
solidcraft-engine = { workspace = true }
solidcraft-gpu = { workspace = true }
solidcraft-mcp = { workspace = true }
+12 -57
View File
@@ -5,7 +5,6 @@
//! are never compared with each other. See tests/kernel-cases/README.md.
use serde_json::{Value, json};
use solidcraft_engine::geom::prelude::*;
use crate::oracle::{ABS_FRAC, Checks, REL_TOL, totals, v3};
use crate::{read_json, recipe};
@@ -63,10 +62,10 @@ const MESH_TOL: f64 = 0.01;
/// A mesh's edges used by one triangle only (open) and by more than two (over-shared). Vertices
/// closer than `weld` (mm) are one vertex: a mesh repeats a vertex for each face, and faces
/// meeting at an edge may place it a rounding error apart.
fn open_edges(positions: &[solidcore_record::Vec3], triangles: &[[u32; 3]], weld: f64) -> (usize, usize) {
fn open_edges(positions: &[solidcraft_engine::geom::Vec3], triangles: &[[u32; 3]], weld: f64) -> (usize, usize) {
use std::collections::HashMap;
let weld = if weld.is_finite() && weld > 0.0 { weld } else { 1e-4 };
let cell = |p: &solidcore_record::Vec3| [p.x, p.y, p.z].map(|x| (x / weld).floor() as i64);
let cell = |p: &solidcraft_engine::geom::Vec3| [p.x, p.y, p.z].map(|x| (x / weld).floor() as i64);
// Union-find over positions, joining each vertex with any within `weld` in the 27 cells round it.
let mut parent: Vec<usize> = (0..positions.len()).collect();
fn find(parent: &mut [usize], mut i: usize) -> usize {
@@ -126,7 +125,7 @@ const SEAM_ANGLE_DEG: f64 = 1.0;
/// the patch's boundary, where its neighbours' own G1 seams end and no surface is G2 to both),
/// with how many seams and stations clear there were. An error where the seams can't be measured.
fn curvature_jumps(body: &solidcraft_engine::kernel::Body, near: [f64; 3], within: f64, clear: f64) -> Result<Value, String> {
use solidcore_record::Vec3;
use solidcraft_engine::geom::Vec3;
let tol =
solidcraft_engine::kernel::ContinuityTolerances { gap_mm: 1e-3, angle_deg: SEAM_ANGLE_DEG, curvature_per_mm: 0.0, ..Default::default() };
let report = solidcraft_engine::kernel::seam_diagnostics(body, &[], SEAM_SAMPLES, tol).map_err(|e| e.to_string())?;
@@ -183,7 +182,7 @@ fn continuity_seams(b: &solidcraft_engine::doc::ModelBody, edges: &Value) -> Res
if let Some(points) = edges.as_array() {
let picks = points
.iter()
.map(|p| v3(p).map(|[x, y, z]| solidcraft_engine::kernel::EdgePick::Point(solidcore_record::Vec3::new(x, y, z))))
.map(|p| v3(p).map(|[x, y, z]| solidcraft_engine::kernel::EdgePick::Point(solidcraft_engine::geom::Vec3::new(x, y, z))))
.collect::<Option<Vec<_>>>()
.ok_or("continuity `edges` points must each be [x,y,z]")?;
let picked = solidcraft_engine::kernel::pick_edges(&b.body, &picks).map_err(|e| e.to_string())?;
@@ -481,8 +480,8 @@ fn output_checks(c: &mut Checks, want: &Value, out: &str, results: &[Value], rel
/// The word a `surface_types` check names a face's surface by, as the record holds it: an
/// analytic kind, `ball_patch` (a corner's patch on a sphere), `bspline` (a B-spline or NURBS
/// surface) or `revolution` (a revolved curve).
fn surface_word(s: &solidcore_record::brep::Surface) -> &'static str {
use solidcore_record::brep::Surface as S;
fn surface_word(s: &solidcraft_engine::geom::brep::Surface) -> &'static str {
use solidcraft_engine::geom::brep::Surface as S;
match s {
S::Plane { .. } | S::PlaneThrough { .. } => "plane",
S::Cylinder { .. } => "cylinder",
@@ -498,8 +497,8 @@ fn surface_word(s: &solidcore_record::brep::Surface) -> &'static str {
/// A B-spline face's two directions as `[degree, spans]` (spans: the distinct knot intervals of
/// the modelled range), the lower first, so a surface parameterised the other way round reads the
/// same. `None` for a surface that is not a B-spline.
fn spline_shape(s: &solidcore_record::brep::Surface) -> Option<[[u64; 2]; 2]> {
use solidcore_record::brep::Surface as S;
fn spline_shape(s: &solidcraft_engine::geom::brep::Surface) -> Option<[[u64; 2]; 2]> {
use solidcraft_engine::geom::brep::Surface as S;
let (u, v, nu, nv) = match s {
S::Spline { u_knots, v_knots, points } => (u_knots, v_knots, points.len(), points.first().map_or(0, Vec::len)),
S::RationalSpline { u_knots, v_knots, homogeneous } => (u_knots, v_knots, homogeneous.len(), homogeneous.first().map_or(0, Vec::len)),
@@ -554,7 +553,7 @@ const CONSTRUCT_DIR_TOL: f64 = 1e-6;
/// parallel to `normal` (either way) and which passes within `tol` of `through`. An axis or plane
/// is graded as the line or plane it is, never by the origin it happens to keep.
fn check_construction(s: &solidcraft_engine::Session, want: &Value, tol: f64, c: &mut Checks) {
use solidcore_record::Vec3;
use solidcraft_engine::geom::Vec3;
use solidcraft_engine::view::{construction_axes, construction_planes, construction_points};
let Some(named) = want.as_object() else {
c.push("construction", json!("`construction` must map names to {point} | {axis} | {plane}"), json!("an object"), false);
@@ -658,34 +657,7 @@ fn flat_pattern_checks(c: &mut Checks, want: &Value, results: &[Value], rel: f64
}
}
/// [`check_case_in`], and with `export` the case's corpus entry written under it
/// ([`crate::kernel_cases_export`]).
fn check_case(
dir: &str,
dumps: Option<&std::path::Path>,
validation: solidcraft_engine::kernel::Validation,
export: Option<&std::path::Path>,
) -> Value {
let mut capture = export.map(|_| crate::kernel_cases_export::Capture::default());
let out = check_case_in(dir, dumps, validation, capture.as_mut());
if let (Some(root), Some(capture)) = (export, capture)
&& let Err(e) = crate::kernel_cases_export::write(root, dir, &out, capture)
{
let mut out = out;
out["export_error"] = json!(e);
return out;
}
out
}
/// One case run and graded. With `capture`, the kernel operations its commands make are traced
/// and kept there with the bodies the case ends with.
fn check_case_in(
dir: &str,
dumps: Option<&std::path::Path>,
validation: solidcraft_engine::kernel::Validation,
mut capture: Option<&mut crate::kernel_cases_export::Capture>,
) -> Value {
fn check_case(dir: &str, dumps: Option<&std::path::Path>, validation: solidcraft_engine::kernel::Validation) -> Value {
let (area, name) = case_name(dir);
let mut out = json!({"case": name, "area": area, "kernel": kernel()});
let want = match read_json(&format!("{dir}/expected.json")) {
@@ -739,18 +711,7 @@ fn check_case_in(
let mut s = solidcraft_engine::Session::default();
s.set_validation(validation);
let clock = std::time::Instant::now();
if capture.is_some() {
solidcraft_engine::kernel::smlib_trace::start();
}
let ran = s.run_script(&commands);
// The trace ends with the commands: grading reads the bodies through the kernel too.
if let Some(c) = &mut capture {
c.events = solidcraft_engine::kernel::smlib_trace::finish();
c.ran = Some(match &ran {
Ok(_) => s.world_state().map(|w| w.bodies.iter().map(|b| (b.name.clone(), b.body.record().clone())).collect()).map_err(|e| e.to_string()),
Err(e) => Err(e.to_string()),
});
}
// The commands' own time, without process start or grading: the ladder's timing golden.
out["seconds"] = json!(clock.elapsed().as_secs_f64());
let answers = match (ran, may_refuse) {
@@ -1015,13 +976,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
.ok_or_else(|| format!("kernel-cases: --validation= must be each_step, each_feature or off, not `{m}`"))?,
None => solidcraft_engine::kernel::Validation::default(),
};
// `--export=DIR`: each case's kernel operations, their input records and its expected values
// go to DIR/<area>/<case>.json, the corpus for solidcore's rig.
let export = args.iter().find_map(|a| a.strip_prefix("--export=")).map(std::path::PathBuf::from);
if export.as_ref().is_some_and(|d| d.as_os_str().is_empty()) {
return Err("kernel-cases: --export= needs a folder".into());
}
let results: Vec<Value> = dirs.iter().map(|d| check_case(d, dumps.as_deref(), validation, export.as_deref())).collect();
let results: Vec<Value> = dirs.iter().map(|d| check_case(d, dumps.as_deref(), validation)).collect();
if dumps.is_some() {
solidcraft_engine::kernel::smlib_dump_dir(None);
}
@@ -1072,7 +1027,7 @@ mod tests {
/// either order of directions, spans only where given, and every face must be accounted for.
#[test]
fn splines_match_degrees_and_spans_in_either_order() {
use solidcore_record::brep::Surface;
use solidcraft_engine::geom::brep::Surface;
let knots = |p: usize, spans: usize| -> Vec<f64> {
let mut k = vec![0.0; p + 1];
k.extend((1..spans).map(|i| i as f64));
@@ -1,299 +0,0 @@
//! `kernel-cases --export=DIR`: one kernel rig case as a corpus entry for solidcore's rig. The
//! case's commands run as the rig runs them while the kernel's operations are traced
//! (`smlib_trace`); the entry holds the operation (or the chain of operations) that made the
//! bodies the case ends with, each operand that came in as a record given as that record (the
//! B-rep record, solidcore-record's JSON), the fix set, the case's expected values and the rig's
//! own result. A case whose result no kernel operation alone accounts for is written too, with
//! the reasons, so nothing is dropped silently.
//!
//! An entry's operation is expressible (`operation_expressible`) when the run failed in a kernel
//! operation (the last one traced failed), or when every body the case ends with is the record a
//! traced operation's output was written as, or an operand some traced operation took, as its
//! record. The entry is wholly expressible (`expressible`) when, besides, every value it grades is
//! the kernel's: none of `continuity`, `watertight`, `files`, `results`, `construction` or
//! `flat_pattern` (SolidCraft computes them), no face, edge or vertex count that the rig's merging
//! across analytic surfaces (`merged_topology`) changes, and no refusal graded on SolidCraft's
//! wording of the kernel's error. Those are listed in `ungraded`.
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use solidcraft_engine::geom::brep::Brep;
use solidcraft_engine::kernel::smlib_trace::{self, Event};
use crate::read_json;
/// What a traced case leaves for its entry: the trace of its commands, and the bodies it ends
/// with by name or the engine's error.
#[derive(Default)]
pub struct Capture {
pub events: Vec<Event>,
pub ran: Option<Result<Vec<(String, Brep)>, String>>,
}
/// The expected values SolidCraft computes, never the kernel.
const GRADER_ONLY: [&str; 6] = ["continuity", "watertight", "files", "results", "construction", "flat_pattern"];
/// The case directory's area (its path under `tests/kernel-cases/`, a ladder case's tier under
/// `ladder/`) and its id.
fn area_and_id(dir: &str) -> Result<(String, String), String> {
let p = Path::new(dir);
let parts: Vec<String> = p.components().map(|c| c.as_os_str().to_string_lossy().to_string()).collect();
let at = parts.iter().rposition(|c| c == "kernel-cases").ok_or_else(|| format!("{dir}: not under tests/kernel-cases"))?;
let rest = parts.get(at + 1..).unwrap_or_default();
match rest.split_last() {
Some((id, area)) if !area.is_empty() => Ok((area.join("/"), id.clone())),
_ => Err(format!("{dir}: no area")),
}
}
/// The case's line in its area's `KNOWN_FAILING`: why it fails today; `None` when not listed.
fn known_failing(dir: &str, id: &str) -> Option<String> {
let list = Path::new(dir).parent()?.join("KNOWN_FAILING");
let text = std::fs::read_to_string(list).ok()?;
text.lines().filter(|l| !l.trim_start().starts_with('#')).find_map(|l| {
let (case, why) = l.split_once(char::is_whitespace).unwrap_or((l, ""));
(case == id).then(|| why.trim().to_string())
})
}
/// The trace read as operations and the records that went in and came out.
struct Trace<'a> {
/// Each operation: its name, operands, parameters and outputs or error.
calls: Vec<(&'static str, &'a [u64], &'a Value, &'a Result<Vec<u64>, String>)>,
/// The record each body made from a record was made from.
imports: HashMap<u64, (&'a Brep, bool, bool)>,
/// The records bodies were written as, in order.
exports: Vec<(u64, &'a Brep)>,
/// The operation that made each body.
producer: HashMap<u64, usize>,
}
impl<'a> Trace<'a> {
fn of(events: &'a [Event]) -> Trace<'a> {
let mut t = Trace { calls: Vec::new(), imports: HashMap::new(), exports: Vec::new(), producer: HashMap::new() };
for e in events {
match e {
Event::Import { id, record, trusted, keeping_splines } => {
t.imports.insert(*id, (record, *trusted, *keeping_splines));
}
Event::Export { id, record } => t.exports.push((*id, record)),
Event::Call { op, inputs, params, outputs } => {
if let Ok(made) = outputs {
for id in made {
t.producer.insert(*id, t.calls.len());
}
}
t.calls.push((*op, inputs, params, outputs));
}
}
}
t
}
/// Bodies some operation took as an operand.
fn operands(&self) -> BTreeSet<u64> {
self.calls.iter().flat_map(|c| c.1.iter().copied()).collect()
}
}
/// The operations `ends` need, in the order they ran: each body's maker, and its operands' makers
/// in turn, back to bodies made from records. Operands made by nothing traced are listed in
/// `unknown`.
fn chain(t: &Trace<'_>, ends: &[u64], unknown: &mut BTreeSet<u64>) -> Vec<usize> {
let mut need = BTreeSet::new();
let mut todo: Vec<u64> = ends.to_vec();
while let Some(id) = todo.pop() {
if t.imports.contains_key(&id) {
continue;
}
match t.producer.get(&id) {
Some(&i) => {
if need.insert(i)
&& let Some(c) = t.calls.get(i)
{
todo.extend(c.1.iter().copied());
}
}
None => {
unknown.insert(id);
}
}
}
need.into_iter().collect()
}
/// The entry of case `dir`, run with result `out`, into `root/<area>/<id>.json`, and the fix set
/// into `root/fixes.json`.
pub fn write(root: &Path, dir: &str, out: &Value, capture: Capture) -> Result<(), String> {
let (area, id) = area_and_id(dir)?;
let want = read_json(&format!("{dir}/expected.json"))?;
let t = Trace::of(&capture.events);
// Why the operation is not the kernel's alone, and what the entry cannot grade without
// SolidCraft.
let mut why_not: Vec<String> = Vec::new();
let mut ungraded: Vec<String> = Vec::new();
let has = |k: &str| want.get(k).is_some_and(|v| !v.is_null());
if let Some(missing) = want["missing"].as_str() {
why_not.push(format!("SolidCraft has no command for it yet: {missing}"));
}
for k in GRADER_ONLY.iter().filter(|k| has(k)) {
ungraded.push(format!("{k}: SolidCraft computes it"));
}
let mut result: Vec<Value> = Vec::new();
let mut ends: Vec<u64> = Vec::new();
let mut failed_call: Option<usize> = None;
let mut outcome = json!({"ran": true});
match (&capture.ran, has("missing")) {
(_, true) => {}
(None, false) => why_not.push("the case did not run".into()),
(Some(Err(e)), false) => {
// The run stopped. It stopped in the kernel when the last operation that failed is
// followed by no operation but copies (a copy or a tagged copy, which build nothing):
// nothing was built after it, so the engine's error reports that failure in its own
// words. An operation that ran after it means the engine went on and failed itself.
outcome = json!({"ran": false, "engine_error": e});
let last_failed = t.calls.iter().rposition(|c| c.3.is_err());
let built_after = |i: usize| t.calls.iter().skip(i + 1).any(|c| !matches!(c.0, "copy" | "tagged"));
match last_failed.filter(|&i| !built_after(i)).and_then(|i| t.calls.get(i).map(|c| (i, c))) {
Some((i, c)) => {
failed_call = Some(i);
ends.extend(c.1.iter().copied());
if let Err(k) = c.3 {
outcome["kernel_error"] = json!(k);
if let Some(why) = want["refusal"].as_str().filter(|w| !k.contains(*w)) {
ungraded.push(format!("refusal {why:?}: SolidCraft's wording of the kernel's error"));
}
}
}
None if t.calls.is_empty() => why_not.push(format!("no kernel operation ran before the failure: {e}")),
None => why_not.push(format!("the failure is SolidCraft's, not a kernel operation's: {e}")),
}
}
(Some(Ok(world)), false) => {
let operands = t.operands();
let mut made = 0;
for (name, record) in world {
let from_op = t.exports.iter().rev().find(|(id, r)| *r == record && t.producer.contains_key(id)).map(|(id, _)| *id);
let operand = t.imports.iter().filter(|(i, (r, _, _))| *r == record && operands.contains(i)).map(|(i, _)| *i).min();
match (from_op, operand) {
(Some(id), _) => {
made += 1;
ends.push(id);
result.push(json!({"body": name, "made": format!("b{id}")}));
}
(None, Some(id)) => {
ends.push(id);
result.push(json!({"body": name, "operand": format!("b{id}")}));
}
(None, None) => why_not.push(format!(
"body {name:?} is no kernel operation's output nor an operand: SolidCraft built it or changed the kernel's record"
)),
}
}
if world.is_empty() {
why_not.push("the case ends with no body".into());
} else if made == 0 && why_not.is_empty() {
why_not.push("no kernel operation made a body the case ends with".into());
}
// Counts graded as SolidCraft merges them, where that differs from the records'.
let raw = |k: &str| -> u64 {
world
.iter()
.map(|(_, r)| match k {
"faces" => r.faces.len(),
"edges" => r.edges.len(),
_ => r.vertices.len(),
} as u64)
.sum()
};
for k in ["faces", "edges", "vertices"] {
if let Some(merged) = out["measured"][k].as_u64().filter(|&m| has(k) && m != raw(k)) {
ungraded.push(format!("{k}: graded merged across analytic surfaces ({merged}), the records have {}", raw(k)));
}
}
outcome["raw_counts"] = json!({"faces": raw("faces"), "edges": raw("edges"), "vertices": raw("vertices")});
}
}
let mut unknown = BTreeSet::new();
let mut order = chain(&t, &ends, &mut unknown);
if !unknown.is_empty() {
why_not.push(format!("operands made by nothing traced: {unknown:?}"));
}
if let Some(i) = failed_call.filter(|i| !order.contains(i)) {
order.push(i);
}
// Operand records: every body the chain takes, or the case ends with, that came in as a record.
let mut bodies = BTreeMap::new();
let given = |id: u64, bodies: &mut BTreeMap<String, Value>| {
if let Some((record, trusted, splines)) = t.imports.get(&id) {
bodies.insert(format!("b{id}"), json!({"trusted": trusted, "keeping_splines": splines, "record": record}));
}
};
let mut calls: Vec<Value> = Vec::new();
for (op, inputs, params, outputs) in order.iter().filter_map(|&i| t.calls.get(i)) {
for &id in inputs.iter() {
given(id, &mut bodies);
}
let mut c = json!({"op": op, "inputs": inputs.iter().map(|i| format!("b{i}")).collect::<Vec<_>>(), "params": params});
match outputs {
Ok(made) => c["outputs"] = json!(made.iter().map(|i| format!("b{i}")).collect::<Vec<_>>()),
Err(e) => c["error"] = json!(e),
}
calls.push(c);
}
for r in &result {
if let Some(id) = r["operand"].as_str().and_then(|b| b.strip_prefix('b')).and_then(|n| n.parse().ok()) {
given(id, &mut bodies);
}
}
let rig = json!({
"status": out["status"], "error": out["error"], "refused_with": out["refused_with"], "measured": out["measured"],
"known_failing": known_failing(dir, &id),
});
let operation_expressible = why_not.is_empty();
let entry = json!({
"case": id,
"area": area,
"sources": want["sources"],
"what": want["what"],
"expressible": operation_expressible && ungraded.is_empty(),
"operation_expressible": operation_expressible,
"why_not": why_not,
"ungraded": ungraded,
"expected": want,
"rig": rig,
"outcome": outcome,
"calls": calls,
"bodies": bodies,
"result": result,
"traced_ops": t.calls.iter().map(|c| c.0).collect::<Vec<_>>(),
});
let folder: PathBuf = root.join(&area);
std::fs::create_dir_all(&folder).map_err(|e| format!("{}: {e}", folder.display()))?;
let path = folder.join(format!("{id}.json"));
let text = serde_json::to_string(&entry).map_err(|e| e.to_string())?;
std::fs::write(&path, text).map_err(|e| format!("{}: {e}", path.display()))?;
// Cases run in parallel processes: each writes its own file and renames it over the shared
// one, so no reader sees a torn file (every process writes the same fix set).
let fixes = serde_json::to_string_pretty(&smlib_trace::fixes()).map_err(|e| e.to_string())?;
let own = root.join(format!("fixes.json.{}", std::process::id()));
std::fs::write(&own, fixes).map_err(|e| format!("{}: {e}", own.display()))?;
std::fs::rename(&own, root.join("fixes.json")).map_err(|e| format!("{}: {e}", root.display()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_area_is_the_path_under_kernel_cases() {
assert_eq!(area_and_id("tests/kernel-cases/booleans/1894-x").unwrap(), ("booleans".into(), "1894-x".into()));
assert_eq!(area_and_id("/r/tests/kernel-cases/ladder/07-fillets/003-y").unwrap(), ("ladder/07-fillets".into(), "003-y".into()));
assert!(area_and_id("/r/other/x").is_err());
}
}
+1 -2
View File
@@ -35,7 +35,6 @@ mod bench_assistant;
mod crashes;
mod fuzz;
mod kernel_cases;
mod kernel_cases_export;
mod logger;
mod oracle;
mod preview;
@@ -86,7 +85,7 @@ const USAGE_RECIPE: &str = " solidcraft-cli recipe <recipe.json>\n";
/// `oracle`'s lines in the usage.
const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
/// `kernel-cases`'s lines in the usage.
const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases <case-dir>... [--json] [--dumps=DIR] [--export=DIR] [--validation=each_step|each_feature|off]\n (the kernel regression rig, tests/kernel-cases, on SMLib; --dumps: each case's\n failing SMLib calls leave their inputs for replay in DIR/<area>-<case>; --export: each case's\n kernel operations, input records and expected values for solidcore's rig, in DIR/<area>/<case>.json; --validation: when SMLib's\n full check runs (diagnostics.validation), default each_feature)\n";
const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases <case-dir>... [--json] [--dumps=DIR] [--validation=each_step|each_feature|off]\n (the kernel regression rig, tests/kernel-cases, on SMLib; --dumps: each case's\n failing SMLib calls leave their inputs for replay in DIR/<area>-<case>; --validation: when SMLib's\n full check runs (diagnostics.validation), default each_feature)\n";
/// `fuzz`'s lines in the usage.
const USAGE_FUZZ: &str =
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
+1 -1
View File
@@ -5,7 +5,7 @@
#![allow(clippy::needless_range_loop)]
use serde_json::{Value, json};
use solidcore_record::Vec3;
use solidcraft_engine::geom::Vec3;
use crate::spatial::{self, Tri};
use crate::{read_json, recipe};
+2 -3
View File
@@ -21,7 +21,6 @@
//! Each design runs in a child process (this test binary, [`action_matrix_worker`]) so an action
//! that hangs or aborts is recorded as such and the rest of the design still runs.
use solidcraft_engine::geom::prelude::*;
use std::collections::{BTreeMap, BTreeSet};
use std::io::{BufRead, Write};
use std::path::{Path, PathBuf};
@@ -29,10 +28,10 @@ use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use serde_json::{Value, json};
use solidcore_record::Vec3;
use solidcraft_engine::Session;
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
use solidcraft_engine::doc::{FaceAttachmentStatus, ModelBody, naming};
use solidcraft_engine::geom::Vec3;
/// One in this many actions runs on pull requests (by a stable hash of its key).
const PR_SAMPLE: u64 = 40;
@@ -1657,7 +1656,7 @@ impl TipConstruction {
other => panic!("expected an axis, got {other:?}"),
}
}
fn plane(&self) -> solidcore_record::Plane {
fn plane(&self) -> solidcraft_engine::geom::Plane {
match &self.0 {
solidcraft_engine::doc::construct::ConstructGeom::Plane(p) => *p,
other => panic!("expected a plane, got {other:?}"),
+1 -1
View File
@@ -4,7 +4,7 @@
use std::f64::consts::PI;
use serde_json::Value;
use solidcore_record::Vec3;
use solidcraft_engine::geom::Vec3;
use solidcraft_engine::{Session, io, kernel};
fn assert_two_through_holes(session: &Session, stage: &str) {
@@ -2,7 +2,6 @@
//! the current format and evaluate (what they rebuild to today is today's, not what the build
//! that saved them made); damaged and hostile design files never panic.
use solidcraft_engine::geom::prelude::*;
use std::path::{Path, PathBuf};
use serde_json::{Value, json};
+1 -2
View File
@@ -16,14 +16,13 @@
//! designs, the engine's design fixtures and the older designs kept by the hole matrix, taper
//! and projection tests.
use solidcraft_engine::geom::prelude::*;
use std::path::{Path, PathBuf};
use std::process::Command;
use serde_json::{Value, json};
use solidcore_record::Vec3;
use solidcraft_engine::Session;
use solidcraft_engine::doc::{FaceAttachmentStatus, Feature, FeatureKind, ModelBody, ModelState, PlaneRef, naming};
use solidcraft_engine::geom::Vec3;
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap()
@@ -9,7 +9,6 @@ use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use solidcraft_engine::Session;
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
use solidcraft_engine::geom::prelude::*;
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap()
+1 -1
View File
@@ -288,7 +288,7 @@ fn a_snapshot_shows_the_part_where_the_camera_puts_it() {
let vp = c.view_proj(f64::from(SIZE.0) / f64::from(SIZE.1), 1000.0);
let (mut elo, mut ehi) = ([f64::MAX; 2], [f64::MIN; 2]);
for k in 0..8 {
let p = solidcore_record::Vec3::new(
let p = solidcraft_engine::geom::Vec3::new(
corner[0] + size[0] * f64::from(k & 1),
corner[1] + size[1] * f64::from((k >> 1) & 1),
corner[2] + size[2] * f64::from((k >> 2) & 1),
@@ -5,14 +5,13 @@
//! vertices nothing else used, so an edge that is one straight line was drawn (and lit on hover)
//! as many pieces, and a sketch on such a face projected every piece.
use solidcraft_engine::geom::prelude::*;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde_json::json;
use solidcore_record::Vec3;
use solidcraft_engine::Session;
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
use solidcraft_engine::geom::Vec3;
fn design() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/action_matrix/designs/crazy-geom-cuts.sc")
+1 -1
View File
@@ -36,7 +36,7 @@ js-sys = "0.3"
serde_json = { workspace = true }
web-sys = { version = "0.3", features = [
"Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "Location", "Navigator", "StorageManager",
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemGetDirectoryOptions", "FileSystemWritableFileStream",
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage", "StorageEvent",
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
"Headers", "ReadableStream", "ReadableStreamDefaultReader",
-2
View File
@@ -1,2 +0,0 @@
This build keeps its designs and settings apart per release channel (src/channel.rs).
packaging/web-channels.sh only offers a release candidate that has this file.
-1
View File
@@ -34,7 +34,6 @@
<link rel="icon" type="image/png" sizes="32x32" href="icons/32x32/io.tangencylabs.solidcraft.png">
<link rel="apple-touch-icon" href="icons/256x256/io.tangencylabs.solidcraft.png">
<link data-trunk rel="copy-file" href="manifest.webmanifest" />
<link data-trunk rel="copy-file" href="channel-storage.txt" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/32x32/apps/io.tangencylabs.solidcraft.png" data-target-path="icons/32x32" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/256x256/apps/io.tangencylabs.solidcraft.png" data-target-path="icons/256x256" />
<link data-trunk rel="copy-file" href="../../assets/icons/app/linux/exports/hicolor/512x512/apps/io.tangencylabs.solidcraft.png" data-target-path="icons/512x512" />
-61
View File
@@ -1,61 +0,0 @@
//! Release channels. The site serves the stable web app at `/app/`, the release candidate at
//! `/app-rc/` and the bleeding-edge build at `/app-edge/`: one origin, so one set of browser
//! storage. A channel that kept its designs and settings in the same places could save a design
//! in a file format the stable app can't open, or overwrite stable's settings. So each
//! pre-release channel keeps everything under its own name, and stable (and any copy served from
//! another path: the release zip, `trunk serve`) keeps the names it has always used, so what
//! people have saved at `/app/` is untouched.
//!
//! The channel comes from the directory the app is served from, not from the build, so a build
//! taken from a release zip is as separate as one built on the spot.
/// The channel the app is served as, from the page's path: `"rc"`, `"edge"`, or `""` (stable).
pub fn of_path(pathname: &str) -> &'static str {
match pathname.trim_end_matches("index.html").trim_end_matches('/').rsplit('/').next() {
Some("app-rc") => "rc",
Some("app-edge") => "edge",
_ => "",
}
}
/// `name` as this channel stores it: unchanged for stable, `rc:name` and `edge:name` otherwise.
pub fn name_in(channel: &str, name: &str) -> String {
if channel.is_empty() { name.to_owned() } else { format!("{channel}:{name}") }
}
#[cfg(target_arch = "wasm32")]
pub fn current() -> &'static str {
web_sys::window().and_then(|w| w.location().pathname().ok()).map_or("", |p| of_path(&p))
}
/// A storage key (local storage, IndexedDB database name) of the running channel.
#[cfg(target_arch = "wasm32")]
pub fn key(name: &str) -> String {
name_in(current(), name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn channel_follows_the_directory() {
assert_eq!(of_path("/app/"), "");
assert_eq!(of_path("/app/index.html"), "");
assert_eq!(of_path("/app-rc/"), "rc");
assert_eq!(of_path("/app-rc"), "rc");
assert_eq!(of_path("/app-rc/index.html"), "rc");
assert_eq!(of_path("/app-edge/"), "edge");
assert_eq!(of_path("/"), "");
assert_eq!(of_path("/solidcraft-web-0.30.1/"), "");
assert_eq!(of_path("/app/app-rc/"), "rc", "only the directory's own name counts");
assert_eq!(of_path("/app-rc/app/"), "");
}
#[test]
fn stable_keeps_its_names() {
assert_eq!(name_in("", "solidcraft.prefs"), "solidcraft.prefs");
assert_eq!(name_in("rc", "solidcraft.prefs"), "rc:solidcraft.prefs");
assert_ne!(name_in("rc", "x"), name_in("edge", "x"));
}
}
+1 -1
View File
@@ -467,7 +467,7 @@ mod web {
async fn open_db() -> Result<IdbDatabase, String> {
let factory = web_sys::window().ok_or("no window")?.indexed_db().map_err(err)?.ok_or("this browser has no IndexedDB")?;
let request: IdbOpenDbRequest = factory.open_with_u32(&crate::channel::key(DB), 1).map_err(err)?;
let request: IdbOpenDbRequest = factory.open_with_u32(DB, 1).map_err(err)?;
let target = request.clone();
let upgrade = Closure::once_into_js(move || {
if let Ok(db) = target.result().and_then(|r| r.dyn_into::<IdbDatabase>()) {
+6 -21
View File
@@ -16,9 +16,7 @@ use solidcraft_engine::io::vfs;
use crate::disk;
use wasm_bindgen::{JsCast as _, JsValue, closure::Closure};
use wasm_bindgen_futures::{JsFuture, spawn_local};
use web_sys::{
FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions, FileSystemGetFileOptions, FileSystemWritableFileStream,
};
use web_sys::{FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetFileOptions, FileSystemWritableFileStream};
/// Where OPFS files appear to the engine.
pub const OPFS: &str = disk::OPFS_PREFIX;
@@ -113,7 +111,7 @@ thread_local! {
static LOCAL: Cell<bool> = const { Cell::new(false) };
}
/// Local storage keys of stored files (under the channel's name, see `channel.rs`).
/// Local storage keys of stored files.
const LOCAL_PREFIX: &str = "solidcraft.file.";
/// Local-storage files kept by earlier releases (compatibility shim, docs/compatibility.md):
/// still listed and opened; removing one removes either key.
@@ -171,17 +169,7 @@ fn has(sm: &web_sys::StorageManager, method: &str) -> bool {
async fn root() -> Result<FileSystemDirectoryHandle, String> {
let sm = storage_manager().filter(|sm| has(sm, "getDirectory")).ok_or("this page has no private file storage (it needs https or localhost)")?;
let dir = JsFuture::from(sm.get_directory()).await.map_err(err)?;
let dir = dir.dyn_into::<FileSystemDirectoryHandle>().map_err(|_| "this browser has no private file storage (OPFS)".to_string())?;
// Stable keeps its files in the root, where they have always been; a pre-release channel has a folder of its own.
match crate::channel::current() {
"" => Ok(dir),
channel => {
let opts = FileSystemGetDirectoryOptions::new();
opts.set_create(true);
let sub = JsFuture::from(dir.get_directory_handle_with_options(&format!("channel-{channel}"), &opts)).await.map_err(err)?;
sub.dyn_into::<FileSystemDirectoryHandle>().map_err(|_| "not a folder".to_string())
}
}
dir.dyn_into::<FileSystemDirectoryHandle>().map_err(|_| "this browser has no private file storage (OPFS)".to_string())
}
pub(crate) async fn read_handle(h: &FileSystemFileHandle) -> Result<Vec<u8>, String> {
@@ -203,7 +191,7 @@ async fn file_handle(dir: &FileSystemDirectoryHandle, name: &str, create: bool)
async fn write_file(name: &str, bytes: &[u8]) -> Result<(), String> {
if local_only() {
return local()?
.set_item(&crate::channel::key(&format!("{LOCAL_PREFIX}{name}")), &encode(bytes))
.set_item(&format!("{LOCAL_PREFIX}{name}"), &encode(bytes))
.map_err(|e| format!("local storage is full or blocked: {}", err(e)));
}
let dir = root().await?;
@@ -219,7 +207,7 @@ async fn remove_file(name: &str) -> Result<(), String> {
if local_only() {
let st = local()?;
st.remove_item(&format!("{LEGACY_LOCAL_PREFIX}{name}")).map_err(err)?;
return st.remove_item(&crate::channel::key(&format!("{LOCAL_PREFIX}{name}"))).map_err(err);
return st.remove_item(&format!("{LOCAL_PREFIX}{name}")).map_err(err);
}
let dir = root().await?;
JsFuture::from(dir.remove_entry(name)).await.map_err(err)?;
@@ -232,10 +220,7 @@ fn read_local() -> Result<Vec<(String, Vec<u8>, f64)>, String> {
let mut out = Vec::new();
for i in 0..st.length().unwrap_or(0).min(10_000) {
let Some(k) = st.key(i).ok().flatten() else { continue };
let own = crate::channel::key(LOCAL_PREFIX);
// The old names belong to stable; a pre-release channel has none.
let legacy = crate::channel::current().is_empty().then_some(LEGACY_LOCAL_PREFIX);
let Some(name) = k.strip_prefix(own.as_str()).or_else(|| legacy.and_then(|l| k.strip_prefix(l))) else { continue };
let Some(name) = k.strip_prefix(LOCAL_PREFIX).or_else(|| k.strip_prefix(LEGACY_LOCAL_PREFIX)) else { continue };
if let Some(v) = st.get_item(&k).ok().flatten() {
out.push((name.to_string(), decode(&v), 0.0));
}
+16 -26
View File
@@ -9,8 +9,6 @@
#[cfg(target_arch = "wasm32")]
mod api;
#[cfg(any(target_arch = "wasm32", test))]
mod channel;
#[cfg(any(target_arch = "wasm32", test))]
mod disk;
#[cfg(target_arch = "wasm32")]
mod easyprint;
@@ -154,15 +152,8 @@ impl PrefsWriter {
/// tab of the page when one tab writes). Other keys, and settings taken away (clearing the site's
/// data), leave this tab's settings as they are.
#[cfg(any(target_arch = "wasm32", test))]
fn settings_written(own_key: &str, key: Option<&str>, value: Option<String>) -> Option<String> {
// Tabs of another channel write to the same storage, under their own key.
value.filter(|_| key == Some(own_key))
}
/// The settings key of `channel` (see [`channel`]).
#[cfg(target_arch = "wasm32")]
fn prefs_key() -> String {
channel::key(solidcraft_ui_egui::prefs::STORAGE_KEY)
fn settings_written(key: Option<&str>, value: Option<String>) -> Option<String> {
value.filter(|_| key == Some(solidcraft_ui_egui::prefs::STORAGE_KEY))
}
#[cfg(target_arch = "wasm32")]
@@ -271,7 +262,7 @@ mod web {
fn hear_settings_from_other_tabs(window: &web_sys::Window, ctx: &egui::Context, inbox: PrefsInbox) {
let ctx = ctx.clone();
let on_storage = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::StorageEvent)>::new(move |e: web_sys::StorageEvent| {
if let Some(prefs) = crate::settings_written(&crate::prefs_key(), e.key().as_deref(), e.new_value()) {
if let Some(prefs) = crate::settings_written(e.key().as_deref(), e.new_value()) {
inbox.set(Some(prefs));
ctx.request_repaint();
}
@@ -343,7 +334,8 @@ mod web {
}
let prefs = self.persisted_prefs();
let Some(prefs) = self.prefs_writer.take(prefs) else { return };
let stored = web_sys::window().and_then(|w| w.local_storage().ok().flatten()).map(|s| s.set_item(&crate::prefs_key(), &prefs));
let stored =
web_sys::window().and_then(|w| w.local_storage().ok().flatten()).map(|s| s.set_item(solidcraft_ui_egui::prefs::STORAGE_KEY, &prefs));
if !matches!(stored, Some(Ok(()))) {
log::warn!("settings: the browser's storage did not take them; they are saved again later");
self.prefs_writer = crate::PrefsWriter::default();
@@ -842,15 +834,14 @@ mod web {
self.store_prefs(&ctx);
}
fn persist_egui_memory(&self) -> bool {
// eframe keeps this under one fixed key: only stable uses it, so a pre-release never rewrites it.
self.launch.persists() && crate::channel::current().is_empty()
self.launch.persists()
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if !self.launch.persists() {
return;
}
let prefs = self.persisted_prefs();
storage.set_string(&crate::prefs_key(), prefs);
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, prefs);
}
}
@@ -860,7 +851,7 @@ mod web {
/// Where `?webgl` is remembered. The installed app starts from its manifest's start URL, which
/// has no query, so the choice has to outlive the page it was made on.
const WEBGL_KEY: &str = "solidcraft.webgl"; // stored under crate::channel::key
const WEBGL_KEY: &str = "solidcraft.webgl";
/// Render with WebGL2: asked for now (`?webgl`, remembered) or before (until `?webgpu`).
fn webgl() -> bool {
@@ -868,17 +859,17 @@ mod web {
let q = query();
if crate::query_flag(&q, "webgl") {
if let Some(s) = &storage {
let _ = s.set_item(&crate::channel::key(WEBGL_KEY), "1");
let _ = s.set_item(WEBGL_KEY, "1");
}
return true;
}
if crate::query_flag(&q, "webgpu") {
if let Some(s) = &storage {
let _ = s.remove_item(&crate::channel::key(WEBGL_KEY));
let _ = s.remove_item(WEBGL_KEY);
}
return false;
}
storage.and_then(|s| s.get_item(&crate::channel::key(WEBGL_KEY)).ok().flatten()).is_some()
storage.and_then(|s| s.get_item(WEBGL_KEY).ok().flatten()).is_some()
}
/// Fetch font bytes asynchronously through the browser.
@@ -977,7 +968,7 @@ mod web {
if launch.persists() {
hear_settings_from_other_tabs(&window, &cc.egui_ctx, other_tab_prefs.clone());
}
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(&crate::channel::key(k)))) {
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
app.load_prefs(&p);
}
if let Some(rs) = &cc.wgpu_render_state {
@@ -1180,10 +1171,9 @@ mod tests {
#[test]
fn only_settings_another_tab_wrote_are_loaded() {
let key = solidcraft_ui_egui::prefs::STORAGE_KEY;
assert_eq!(settings_written(key, Some(key), Some("{}".into())).as_deref(), Some("{}"));
assert_eq!(settings_written(key, Some("solidcraft.webgl"), Some("1".into())), None);
assert_eq!(settings_written(key, Some(key), None), None, "removed settings leave this tab's");
assert_eq!(settings_written(key, None, None), None, "clearing all storage leaves this tab's settings");
assert_eq!(settings_written(key, Some("rc:solidcraft.prefs"), Some("{}".into())), None, "another channel's settings are not loaded");
assert_eq!(settings_written(Some(key), Some("{}".into())).as_deref(), Some("{}"));
assert_eq!(settings_written(Some("solidcraft.webgl"), Some("1".into())), None);
assert_eq!(settings_written(Some(key), None), None, "removed settings leave this tab's");
assert_eq!(settings_written(None, None), None, "clearing all storage leaves this tab's settings");
}
}
+1 -2
View File
@@ -1,8 +1,7 @@
//! Navigation drawing definitions. Camera projection and picking remain in the UI widgets.
use crate::theme::Tokens;
use egui::{Color32, FontId, Pos2, Rect, Shape, Stroke, pos2, vec2};
use solidcore_record::Vec3;
use solidcraft_engine::geom::Ring;
use solidcraft_engine::geom::{Ring, Vec3};
pub(crate) struct Facet {
pub(crate) direction: Vec3,
pub(crate) points: Ring<Pos2>,
-1
View File
@@ -9,7 +9,6 @@ license.workspace = true
workspace = true
[dependencies]
solidcore-record = { workspace = true }
solidcraft-geom = { workspace = true }
solidcraft-sketch = { workspace = true }
solidcraft-kernel = { workspace = true }
+1 -2
View File
@@ -3,11 +3,10 @@
//! a component's over the physical material's look; a body with none of these keeps the colour
//! it was imported with.
use solidcraft_geom::prelude::*;
use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
use crate::Document;
+1 -2
View File
@@ -9,8 +9,7 @@
//! and the engine maps a new feature's geometry into the active component's frame.
use serde::{Deserialize, Serialize};
use solidcore_record::{Plane, Vec3};
use solidcraft_geom::prelude::*;
use solidcraft_geom::{Plane, Vec3};
use crate::{DocError, Document, FeatureKind, PlaneRef, Result};
+1 -2
View File
@@ -1,6 +1,5 @@
//! World-frame metadata at a timeline body ownership boundary.
use solidcore_record::Plane;
use solidcraft_geom::prelude::*;
use solidcraft_geom::Plane;
use crate::{DocError, Mat, ModelState, Result, apply_point, apply_vector};
+1 -2
View File
@@ -2,8 +2,7 @@
//! The image bytes travel inside the design file (base64), so it reopens without the original.
use serde::{Deserialize, Serialize};
use solidcore_record::{Plane, Vec2, Vec3};
use solidcraft_geom::prelude::*;
use solidcraft_geom::{Plane, Vec2, Vec3};
use crate::PlaneRef;
+20 -26
View File
@@ -1,8 +1,7 @@
//! Certificates are producer identities, never a nearest-surface selection.
use crate::{DocError, Document, FeatureKind, ModelBody, Result, SolvedSketch};
use serde::{Deserialize, Serialize};
use solidcore_record::Seg2;
use solidcraft_geom::prelude::*;
use solidcraft_geom::Seg2;
fn valid_signature(outer: &[String], holes: &[Vec<String>]) -> bool {
let valid = |ids: &[String]| {
@@ -69,11 +68,11 @@ pub(crate) fn cap_key(outer: &[String], holes: &[Vec<String>]) -> String {
}
format!("{:x}", hash.finalize())
}
fn exact_region_signature(curves: &ExactSketchCurves, region: &solidcore_record::Region2) -> Option<solidcraft_kernel::CapSignature> {
fn exact_region_signature(curves: &ExactSketchCurves, region: &solidcraft_geom::Region2) -> Option<solidcraft_kernel::CapSignature> {
if region.outer.segs.len() + region.holes.iter().map(|hole| hole.segs.len()).sum::<usize>() > 4096 || region.holes.len() > 128 {
return None;
}
let signature = |outline: &solidcore_record::Loop2| {
let signature = |outline: &solidcraft_geom::Loop2| {
let mut ids = outline.segs.iter().map(|segment| curves.find(segment)).collect::<Option<Vec<_>>>()?;
ids.sort();
ids.dedup();
@@ -117,7 +116,7 @@ pub(crate) fn extrude_cap_bindings(sketch: &SolvedSketch, bodies: &[solidcraft_k
let mut counts = std::collections::BTreeMap::new();
let mut bindings = Vec::with_capacity(bodies.len());
for body in bodies {
let mut cached: Option<(&solidcore_record::Region2, Option<solidcraft_kernel::CapSignature>)> = None;
let mut cached: Option<(&solidcraft_geom::Region2, Option<solidcraft_kernel::CapSignature>)> = None;
let mut values = Vec::new();
for source in body.face_sources().into_iter().flatten() {
let signature = if let Some(FaceSource::ExtrudeCap { region, high, .. }) = source {
@@ -615,7 +614,7 @@ fn exact_segment_contains(original: &Seg2, generated: &Seg2) -> bool {
(Seg2::Line { a, b }, Seg2::Line { a: c, b: d }) => {
let direction = *b - *a;
let scale = direction.len().max((*d - *c).len()).max(1.);
let interval = |point: solidcore_record::Vec2| {
let interval = |point: solidcraft_geom::Vec2| {
let t = (point - *a).dot(direction) / direction.len2();
(-1e-10..=1. + 1e-10).contains(&t)
};
@@ -651,12 +650,7 @@ fn exact_segment_contains(original: &Seg2, generated: &Seg2) -> bool {
}
impl Document {
pub fn configuration_face_target(
&self,
body: &ModelBody,
index: usize,
point: solidcore_record::Vec3,
) -> Result<crate::config::ConfigFaceTarget> {
pub fn configuration_face_target(&self, body: &ModelBody, index: usize, point: solidcraft_geom::Vec3) -> Result<crate::config::ConfigFaceTarget> {
let names = crate::naming::face_names(body);
let mut name = names
.get(index)
@@ -725,7 +719,7 @@ impl Document {
certificate.validate()?;
Ok(Some(certificate))
}
pub fn strong_face_target(&self, body: &ModelBody, index: usize, point: solidcore_record::Vec3) -> Result<crate::config::ConfigFaceTarget> {
pub fn strong_face_target(&self, body: &ModelBody, index: usize, point: solidcraft_geom::Vec3) -> Result<crate::config::ConfigFaceTarget> {
let mut target = self.configuration_face_target(body, index, point)?;
match body.body.face_sources().and_then(|sources| sources.get(index)).and_then(Option::as_ref) {
Some(solidcraft_kernel::FaceSource::Box { producer: Some(producer), axis, high, .. }) => {
@@ -758,27 +752,27 @@ impl Document {
/// is), projected onto it. A face that moves along its normal keeps the frame where it was;
/// one that moves within its plane (a Move feature) keeps it too, as a sketch on a face does
/// in the definition's coordinates.
pub fn lineage_frame(body: &ModelBody, plane: &solidcore_record::Plane) -> Option<solidcore_record::Plane> {
use solidcore_record::Vec3;
pub fn lineage_frame(body: &ModelBody, plane: &solidcraft_geom::Plane) -> Option<solidcraft_geom::Plane> {
use solidcraft_geom::Vec3;
let n = plane.normal();
let m = &body.source_transform;
let origin = crate::apply_point(m, Vec3::ZERO);
let axis = [Vec3::X, Vec3::Y, Vec3::Z].into_iter().filter_map(|a| crate::apply_vector(m, a).normalized()).find(|a| a.dot(n).abs() < 0.6)?;
let x = axis - n * axis.dot(n);
let origin = origin - n * (origin - plane.origin).dot(n);
solidcore_record::Plane::new(origin, x, n.cross(x))
solidcraft_geom::Plane::new(origin, x, n.cross(x))
}
/// Whether Split Face split `body`'s faces on `plane` (a split line lies on it).
fn split_on_purpose(body: &ModelBody, plane: &solidcore_record::Plane) -> bool {
fn split_on_purpose(body: &ModelBody, plane: &solidcraft_geom::Plane) -> bool {
let tolerance = 1e-6 * (1. + body.body.size());
let on_plane =
|line: &Vec<solidcore_record::Vec3>| !line.is_empty() && line.iter().all(|p| (*p - plane.origin).dot(plane.normal()).abs() <= tolerance);
|line: &Vec<solidcraft_geom::Vec3>| !line.is_empty() && line.iter().all(|p| (*p - plane.origin).dot(plane.normal()).abs() <= tolerance);
body.body.split_lines().is_some_and(|lines| lines.iter().any(on_plane))
}
/// Whether two exact planes are one oriented plane (pieces of one face).
fn same_oriented_plane(a: &solidcore_record::Plane, b: &solidcore_record::Plane, scale: f64) -> bool {
fn same_oriented_plane(a: &solidcraft_geom::Plane, b: &solidcraft_geom::Plane, scale: f64) -> bool {
a.normal().dot(b.normal()) >= 1. - 1e-8 && (b.origin - a.origin).dot(a.normal()).abs() <= 1e-7 * (1. + scale)
}
@@ -786,7 +780,7 @@ impl Document {
/// The exact plane an anchor on face `index` of `body` is measured in: the construction
/// certificate's plane, or for a [`FaceCertificate::Lineage`] face its current exact plane in
/// the [`lineage_frame`].
pub fn anchor_face_plane(&self, body: &ModelBody, index: usize, certificate: Option<&FaceCertificate>) -> Option<solidcore_record::Plane> {
pub fn anchor_face_plane(&self, body: &ModelBody, index: usize, certificate: Option<&FaceCertificate>) -> Option<solidcraft_geom::Plane> {
match certificate {
Some(FaceCertificate::Lineage) => body.body.planar_face(index).and_then(|plane| lineage_frame(body, &plane)),
_ => body.body.face_plane(index),
@@ -802,7 +796,7 @@ impl Document {
&self,
body: &ModelBody,
index: usize,
point: solidcore_record::Vec3,
point: solidcraft_geom::Vec3,
) -> Option<crate::config::ConfigFaceTarget> {
let names = crate::naming::names(body);
if !names.verified(index) {
@@ -961,7 +955,7 @@ mod tests {
SolvedSketch {
feature: 1,
name: "sources".into(),
plane: solidcore_record::Plane::XY,
plane: solidcraft_geom::Plane::XY,
sketch,
report,
profiles: Vec::new(),
@@ -996,7 +990,7 @@ mod tests {
#[test]
fn prepared_sources_keep_exact_identity_and_reject_ambiguity() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
let mut sketch = solidcraft_sketch::Sketch::new();
sketch.add_line(Vec2::ZERO, Vec2::new(10., 0.), None, None, Some("line")).unwrap();
sketch.add_circle(Vec2::new(20., 0.), 2., None, Some("circle")).unwrap();
@@ -1021,7 +1015,7 @@ mod tests {
#[test]
fn a_curve_drawn_over_a_projected_boundary_is_the_source() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
// A face sketch's live boundary, with a profile edge traced over it (issue #489).
let mut sketch = solidcraft_sketch::Sketch::new();
let projected = sketch.add_line(Vec2::ZERO, Vec2::new(10., 0.), None, None, Some("edge")).unwrap();
@@ -1045,7 +1039,7 @@ mod tests {
#[test]
#[ignore = "manual optimized certification microbenchmark; no timing assertion"]
fn benchmark_prepared_extrude_sources() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
let mut sketch = solidcraft_sketch::Sketch::new();
for i in 0..1000 {
let y = i as f64;
@@ -1098,7 +1092,7 @@ mod tests {
#[test]
fn analytic_arc_interval_refuses_unsampled_gaps_and_accepts_wrap_reversal() {
let radians = |degrees: f64| degrees.to_radians();
let arc = |start, sweep| Seg2::Arc { center: solidcore_record::Vec2::default(), radius: 10., start: radians(start), sweep: radians(sweep) };
let arc = |start, sweep| Seg2::Arc { center: solidcraft_geom::Vec2::default(), radius: 10., start: radians(start), sweep: radians(sweep) };
assert!(!exact_segment_contains(&arc(10., 350.), &arc(20., 360.)), "five point probes missed the missing ten-degree gap");
assert!(!exact_segment_contains(&arc(10., 350.), &arc(20., -360.)));
assert!(exact_segment_contains(&arc(350., 40.), &arc(355., 20.)));
+2 -2
View File
@@ -142,7 +142,7 @@ mod tests {
body: "Old".into(),
origin_feature: 3,
name: "F3:+z".into(),
point: solidcore_record::Vec3::new(1., 1., 2.),
point: solidcraft_geom::Vec3::new(1., 1., 2.),
certificate: None,
}),
],
@@ -199,7 +199,7 @@ mod tests {
body: "Foreign".into(),
origin_feature: 3,
name: "F3:+z".into(),
point: solidcore_record::Vec3::ZERO,
point: solidcraft_geom::Vec3::ZERO,
certificate: None,
}),
],
+8 -8
View File
@@ -1,7 +1,7 @@
use super::*;
use crate::{FeatureKind, Model, PlaneRef};
use serde_json::json;
use solidcore_record::{Vec2, Vec3};
use solidcraft_geom::{Vec2, Vec3};
fn shell_document() -> (Document, u64, u64) {
let mut doc = Document::new("certified shell");
@@ -43,15 +43,15 @@ fn face_index(body: &ModelBody, inner: bool) -> usize {
#[test]
fn shell_planar_raw_bounds_flat_identity_and_clone_remap() {
let plane = solidcore_record::Plane::XY;
let same = |planes: &[Option<solidcore_record::Plane>]| super::super::styles::shell_plane_family(planes.iter().copied());
let plane = solidcraft_geom::Plane::XY;
let same = |planes: &[Option<solidcraft_geom::Plane>]| super::super::styles::shell_plane_family(planes.iter().copied());
assert!(same(&[Some(plane), Some(plane)]));
assert!(!same(&[]));
assert!(!same(&[Some(plane), None]));
for different in [
solidcore_record::Plane { origin: Vec3::X, ..plane },
solidcore_record::Plane { x: -plane.x, y: -plane.y, ..plane },
solidcore_record::Plane { y: -plane.y, ..plane },
solidcraft_geom::Plane { origin: Vec3::X, ..plane },
solidcraft_geom::Plane { x: -plane.x, y: -plane.y, ..plane },
solidcraft_geom::Plane { y: -plane.y, ..plane },
] {
assert!(!same(&[Some(plane), Some(different)]));
}
@@ -355,7 +355,7 @@ fn saved_curved_strip_projection_asks_for_its_face_again_after_exact_wall_coales
sketch.add_link(LinkKind::Project, source, &geometry).unwrap();
sketch.links[0].face_name = Some(former);
let sk = doc
.add_feature(FeatureKind::Sketch { plane: PlaneRef::Custom { plane: solidcore_record::Plane::YZ }, sketch }, Some("Saved corner strip"))
.add_feature(FeatureKind::Sketch { plane: PlaneRef::Custom { plane: solidcraft_geom::Plane::YZ }, sketch }, Some("Saved corner strip"))
.unwrap();
let boss = doc
.add_feature(
@@ -431,7 +431,7 @@ fn saved_stepped_tab_cut_piece_projection_asks_for_its_face_again_without_expand
// the patch y 10..20, z 0..20 by its piece name. The kernel leaves the wall one face now: the
// patch is only that saved name and its rectangle on YZ.
let at = Vec3::new(0., 15., 1.);
let plane = solidcore_record::Plane::YZ;
let plane = solidcraft_geom::Plane::YZ;
let current_state = model.state();
let current = current_state.body("Case").unwrap();
let stem = crate::naming::face_name_at_body(current, at).unwrap();
+2 -3
View File
@@ -3,10 +3,9 @@ use super::Column;
use crate::{DocError, Document, FeatureKind, ModelState, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use solidcore_record::Vec3;
use solidcraft_geom::prelude::*;
use solidcraft_geom::Vec3;
pub(crate) fn shell_plane_family(planes: impl IntoIterator<Item = Option<solidcore_record::Plane>>) -> bool {
pub(crate) fn shell_plane_family(planes: impl IntoIterator<Item = Option<solidcraft_geom::Plane>>) -> bool {
let mut planes = planes.into_iter();
let Some(Some(first)) = planes.next() else { return false };
first.origin.is_finite() && first.x.is_finite() && first.y.is_finite() && planes.all(|plane| plane == Some(first))
+2 -3
View File
@@ -6,11 +6,10 @@
//! vertex the names of the edges that meet there, so they follow the model when earlier features
//! change; the picked point is the fallback.
use solidcraft_geom::prelude::*;
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use solidcore_record::{Plane, Vec2, Vec3};
use solidcraft_geom::{Plane, Vec2, Vec3};
use solidcraft_kernel::{self as kernel, FaceSurface};
use crate::eval::{ModelBody, ModelState};
@@ -460,7 +459,7 @@ fn path_segs(ctx: &Ctx, g: &GeoRef) -> Result<Vec<kernel::PathSeg>> {
GeoRef::Edge { at, name } => {
let (b, i, pts) = edge_pick(ctx.st, *at, name)?;
let procedural =
b.body.record().edges.get(i).is_some_and(|e| matches!(e.parameters(), solidcore_record::brep::Curve::SurfaceIntersection { .. }));
b.body.record().edges.get(i).is_some_and(|e| matches!(e.parameters(), solidcraft_geom::brep::Curve::SurfaceIntersection { .. }));
if procedural {
return Ok(lines(&pts));
}
+1 -1
View File
@@ -164,7 +164,7 @@ mod tests {
/// closer to the surface, and the reference mesh the model reads never changes.
#[test]
fn each_level_has_its_own_plan_and_the_reference_mesh_stays() {
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
let body = solidcraft_kernel::cylinder(Vec3::ZERO, Vec3::Z, 20.0, 50.0).unwrap();
let mb = crate::ModelBody::new("Body1".into(), body, 1);
let reference = mb.mesh();
+2 -3
View File
@@ -1,6 +1,5 @@
use serde::{Deserialize, Serialize};
use solidcore_record::{Plane, Vec2, Vec3};
use solidcraft_geom::prelude::*;
use solidcraft_geom::{Plane, Vec2, Vec3};
use solidcraft_kernel::Continuity;
use solidcraft_sketch::Sketch;
@@ -3187,7 +3186,7 @@ impl Visibility {
pub struct NamedView {
#[serde(default)]
pub name: String,
pub target: solidcore_record::Vec3,
pub target: solidcraft_geom::Vec3,
pub yaw: f64,
pub pitch: f64,
pub distance: f64,
+22 -24
View File
@@ -5,16 +5,14 @@ mod placed_mesh;
#[path = "placement_reuse.rs"]
mod placement_reuse;
use solidcraft_geom::prelude::*;
use std::collections::BTreeMap;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, OnceLock};
use serde::{Deserialize, Serialize};
use solidcore_record::brep::part::Contact;
use solidcore_record::{Plane, Region2, Vec2, Vec3};
use solidcraft_geom::brep::part::Contact;
use solidcraft_geom::tess::TessPlan;
use solidcraft_geom::{Mesh, UnitVec3, regions_apart};
use solidcraft_geom::{Mesh, Plane, Region2, UnitVec3, Vec2, Vec3, regions_apart};
use solidcraft_kernel::{self as kernel, Body, BoolOp};
use solidcraft_sketch::{CurveKind, Profile, SolveReport, find_profiles};
@@ -3420,7 +3418,7 @@ fn groups_from_body(mb: &ModelBody, groups: &[Vec<usize>]) -> Option<Vec<bool>>
let holds = |k: usize| {
let shells = regions.get(k).and_then(|&r| record.regions.get(r)).map(|r| r.shells.as_slice()).unwrap_or_default();
shells.iter().any(|&s| match record.shells.get(s as usize) {
Some(solidcore_record::brep::Shell::Faces { faces, .. }) => faces.iter().any(|f| from.get(f.face as usize) == Some(&true)),
Some(solidcraft_geom::brep::Shell::Faces { faces, .. }) => faces.iter().any(|f| from.get(f.face as usize) == Some(&true)),
_ => false,
})
};
@@ -4586,7 +4584,7 @@ fn hole_tools_and_prisms(
let seam = std::cell::Cell::new(0.0);
let disc = |rad: f64| {
seam.set(seam.get() + 0.613);
Region2 { outer: solidcore_record::Loop2::circle_from(Vec2::ZERO, rad, seam.get()), holes: vec![] }
Region2 { outer: solidcraft_geom::Loop2::circle_from(Vec2::ZERO, rad, seam.get()), holes: vec![] }
};
let cyl = |rad: f64, lo: f64, hi: f64, part: &str| -> Result<(Traced, Option<more::PrimitivePrism>)> {
let tool = kernel::extrude(&plane, &[disc(rad)], lo, hi)?.pop().ok_or_else(|| DocError::Invalid("hole tool".into()))?;
@@ -4616,7 +4614,7 @@ fn hole_tools_and_prisms(
let side = up.any_perp();
let rp = Plane::new(*position, side, up).ok_or_else(|| DocError::Invalid("hole axis".into()))?;
let pts = [Vec2::new(0.0, top), Vec2::new(r, top), Vec2::new(r, bottom), Vec2::new(0.0, bottom - tip)];
let region = Region2 { outer: solidcore_record::Loop2::polygon(&pts), holes: vec![] };
let region = Region2 { outer: solidcraft_geom::Loop2::polygon(&pts), holes: vec![] };
return Ok(kernel::revolve(&rp, &[region], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU)?
.into_iter()
.map(|tool| (crate::lineage::part_tool(tool, "bore"), None))
@@ -4652,7 +4650,7 @@ fn hole_tools_and_prisms(
let up = -dir;
let rp = Plane::new(*position, up.any_perp(), up).ok_or_else(|| DocError::Invalid("hole axis".into()))?;
let pts = [Vec2::new(rc, 0.0), Vec2::new(r_end, -len), Vec2::new(0.0, -len), Vec2::new(0.0, top), Vec2::new(rc, top)];
let region = Region2 { outer: solidcore_record::Loop2::polygon(&pts), holes: vec![] };
let region = Region2 { outer: solidcraft_geom::Loop2::polygon(&pts), holes: vec![] };
tools.extend(
kernel::revolve(&rp, &[region], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU)?
.into_iter()
@@ -5100,7 +5098,7 @@ fn feature_tools_with_constructor(
cap_bindings.as_ref().and_then(|bindings| bindings.get(body_index)).cloned().unwrap_or_else(|| vec![None; curves.len()]);
// A cap moved off its designed height (into a Join target, or past a
// curved To face) is not the feature's start or end face.
let nominal = |plane: &solidcore_record::Plane| {
let nominal = |plane: &solidcraft_geom::Plane| {
let height = (plane.origin - ss.plane.origin).dot(n);
let tolerance = 1e-9 * (1.0 + height.abs() + (lo + off).abs() + (hi + off).abs());
(height - (lo + off)).abs() <= tolerance || (height - (hi + off)).abs() <= tolerance
@@ -5304,10 +5302,10 @@ fn feature_tools_with_constructor(
let plane = Plane { origin: *base, x, y: k };
let c = Vec2::new(r, 0.0);
let outer = match section {
crate::CoilSection::Circular => solidcore_record::Loop2::circle(c, s / 2.0),
crate::CoilSection::Circular => solidcraft_geom::Loop2::circle(c, s / 2.0),
crate::CoilSection::Square => {
let h = s / 2.0;
solidcore_record::Loop2::polygon(&[c + Vec2::new(-h, -h), c + Vec2::new(h, -h), c + Vec2::new(h, h), c + Vec2::new(-h, h)])
solidcraft_geom::Loop2::polygon(&[c + Vec2::new(-h, -h), c + Vec2::new(h, -h), c + Vec2::new(h, h), c + Vec2::new(-h, h)])
}
};
let region = Region2 { outer, holes: Vec::new() };
@@ -5505,11 +5503,11 @@ fn pipe_region(vals: &BTreeMap<String, Value>, r: f64, wall: &Option<crate::Expr
if !(wt > 0.0 && wt < r) {
return Err(DocError::Invalid("the wall must be thinner than the radius".into()));
}
vec![solidcore_record::Loop2::circle(Vec2::ZERO, r - wt).reversed()]
vec![solidcraft_geom::Loop2::circle(Vec2::ZERO, r - wt).reversed()]
}
None => Vec::new(),
};
Ok(Region2 { outer: solidcore_record::Loop2::circle(Vec2::ZERO, r), holes })
Ok(Region2 { outer: solidcraft_geom::Loop2::circle(Vec2::ZERO, r), holes })
}
pub fn map_wire(wire: &mut solidcraft_sketch::Wire, matrix: &Mat) {
@@ -5587,8 +5585,8 @@ fn path_points(ps: &SolvedSketch, ids: &[String], start: Vec3) -> Result<Option<
Some(solidcraft_sketch::Wire { arc: Some(arc), .. }) => {
let x = (arc.start - arc.center).normalized().ok_or_else(|| DocError::Invalid("a 3D arc of no radius".into()))?;
let y = arc.normal.cross(x);
let frame = solidcore_record::brep::Frame { origin: arc.center.into(), x_axis: x.into(), y_axis: y.into() };
let circle = solidcore_record::brep::Curve::Circle { frame, radius: arc.radius };
let frame = solidcraft_geom::brep::Frame { origin: arc.center.into(), x_axis: x.into(), y_axis: y.into() };
let circle = solidcraft_geom::brep::Curve::Circle { frame, radius: arc.radius };
kernel::PathPiece::Segs(kernel::curve_path(&circle, [0.0, solidcraft_sketch::arc_sweep(arc)])?)
}
Some(w) if w.fit.len() >= 3 => kernel::PathPiece::Spline { fit: w.fit.clone(), reversed: false },
@@ -5658,14 +5656,14 @@ pub(crate) fn path_segments_at(ps: &SolvedSketch, ids: &[String], start: Vec3, p
}
// Each curve as its exact pieces (Béziers, conics) where it has them, in its own direction
// of travel. A curve is turned round whole: its pieces stay one run.
let mut curves: Vec<Vec<solidcore_record::Seg2>> = Vec::with_capacity(ids.len());
let mut curves: Vec<Vec<solidcraft_geom::Seg2>> = Vec::with_capacity(ids.len());
for id in ids {
let ci = ps.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("path curve `{id}`")))?;
curves.push(ps.sketch.exact_segs(ci).unwrap_or_else(|| ps.sketch.segs(ci)));
}
curves.retain(|c| !c.is_empty());
let ends = |c: &[solidcore_record::Seg2]| c.first().zip(c.last()).map(|(f, l)| (f.start(), l.end()));
let reversed = |c: Vec<solidcore_record::Seg2>| c.into_iter().rev().map(|s| s.reversed()).collect::<Vec<_>>();
let ends = |c: &[solidcraft_geom::Seg2]| c.first().zip(c.last()).map(|(f, l)| (f.start(), l.end()));
let reversed = |c: Vec<solidcraft_geom::Seg2>| c.into_iter().rev().map(|s| s.reversed()).collect::<Vec<_>>();
// Where two curves meet: one tolerance for telling which ends touch and for the joint.
const JOINT: f64 = 1e-5;
// Orient the first curve: it runs toward the second where it meets it at one end only;
@@ -5684,9 +5682,9 @@ pub(crate) fn path_segments_at(ps: &SolvedSketch, ids: &[String], start: Vec3, p
}
}
// Each next curve starts where the previous one ended.
let mut segs: Vec<solidcore_record::Seg2> = Vec::new();
let mut segs: Vec<solidcraft_geom::Seg2> = Vec::new();
for curve in curves {
let (Some(prev_end), Some((a, b))) = (segs.last().map(solidcore_record::Seg2::end), ends(&curve)) else {
let (Some(prev_end), Some((a, b))) = (segs.last().map(solidcraft_geom::Seg2::end), ends(&curve)) else {
segs.extend(curve);
continue;
};
@@ -5706,9 +5704,9 @@ pub(crate) fn path_segments_at(ps: &SolvedSketch, ids: &[String], start: Vec3, p
/// `start`: the curve there split in two, its second part first and its first part last (or the
/// chain begun at the joint there). Unchanged when the chain is open, already starts on the
/// profile's plane, or its point nearest `start` is off that plane.
fn from_profile_on_closed_path(segs: Vec<solidcore_record::Seg2>, path: &Plane, profile: &Plane, start: Vec3) -> Vec<solidcore_record::Seg2> {
fn from_profile_on_closed_path(segs: Vec<solidcraft_geom::Seg2>, path: &Plane, profile: &Plane, start: Vec3) -> Vec<solidcraft_geom::Seg2> {
let (Some(first), Some(last)) = (segs.first(), segs.last()) else { return segs };
let size: f64 = segs.iter().map(solidcore_record::Seg2::length).sum();
let size: f64 = segs.iter().map(solidcraft_geom::Seg2::length).sum();
let on_plane = |p: Vec2| profile.height(path.to_world(p)).abs() <= (size * 1e-9).max(1e-9);
if !start.is_finite() || !(size > 0.0 && size.is_finite()) || last.end().dist(first.start()) > 1e-5 || on_plane(first.start()) {
return segs;
@@ -7198,7 +7196,7 @@ fn eval_feature(
(None, Some(sid)) => {
let ss = st.sketch(sid.0).ok_or_else(|| DocError::Unknown(format!("sketch {sid}")))?;
// Each curve's exact segments (lines and arcs split by their own plane and cylinder).
let curves: Vec<Vec<solidcore_record::Seg2>> = (0..ss.sketch.curves.len())
let curves: Vec<Vec<solidcraft_geom::Seg2>> = (0..ss.sketch.curves.len())
.filter(|ci| ss.sketch.curves.get(*ci).is_some_and(|c| !c.construction))
.map(|ci| ss.sketch.segs(ci))
.filter(|segs| !segs.is_empty())
@@ -8666,7 +8664,7 @@ pub fn sketch_line_plane(st: &ModelState, sketch: u64, line: &str) -> Result<Pla
let ss = st.sketch(sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch}")))?;
let ci = ss.sketch.curve_index(line).ok_or_else(|| DocError::Unknown(format!("curve `{line}`")))?;
let (a, b) = match ss.sketch.segs(ci).as_slice() {
[solidcore_record::Seg2::Line { a, b }] => (*a, *b),
[solidcraft_geom::Seg2::Line { a, b }] => (*a, *b),
_ => return Err(DocError::Invalid(format!("`{line}` is not a straight line: split by a sketch line"))),
};
let (wa, wb) = (ss.plane.to_world(a), ss.plane.to_world(b));
+1 -2
View File
@@ -4,12 +4,11 @@
//! component into its frame, and every evaluation wrote every imported STEP file out again to
//! fingerprint it, so each sketch command took longer the more parts were inserted.
use solidcraft_geom::prelude::*;
use std::cell::Cell;
use std::collections::BTreeMap;
use std::sync::Arc;
use solidcore_record::{Plane, Vec2, Vec3};
use solidcraft_geom::{Plane, Vec2, Vec3};
use solidcraft_kernel as kernel;
use solidcraft_sketch::Sketch;
+1 -3
View File
@@ -1,12 +1,10 @@
//! Frame members (#4003): one body per straight line of a frame sketch, its cross-section a
//! profile laid square to the line (`frame::member_axes`) and run from one end to the other.
use solidcraft_geom::prelude::*;
use std::borrow::Cow;
use std::collections::{BTreeMap, HashSet};
use solidcore_record::{Plane, Region2, Vec2, Vec3};
use solidcraft_geom::{Aabb2, Ring};
use solidcraft_geom::{Aabb2, Plane, Region2, Ring, Vec2, Vec3};
use solidcraft_kernel as kernel;
use solidcraft_sketch::CurveKind;
+1 -2
View File
@@ -5,8 +5,7 @@ use super::*;
use crate::EmbossFace;
use crate::apply_point;
use crate::sheet;
use solidcore_record::{Loop2, Seg2};
use solidcraft_geom::{Aabb3, Ring};
use solidcraft_geom::{Aabb3, Loop2, Ring, Seg2};
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
match &f.kind {
+16 -21
View File
@@ -4,7 +4,7 @@
use super::*;
use crate::{RuledCorners, RuledMode, SheetSection};
use solidcore_record::Seg2;
use solidcraft_geom::Seg2;
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState, warning: &mut Option<String>) -> Result<()> {
match &f.kind {
@@ -233,7 +233,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
return Err(DocError::Invalid("a match needs a chain of the target's edges".into()));
}
// Picked by a point on the face; without picks, the body's first face.
let picked: Vec<solidcore_record::Vec3> = if faces.is_empty() {
let picked: Vec<solidcraft_geom::Vec3> = if faces.is_empty() {
let faces = mb.body.faces(mb.body.size() * 1e-2)?;
vec![faces.first().ok_or_else(|| DocError::Invalid(format!("`{body}` has no face to match")))?.centroid]
} else {
@@ -434,7 +434,7 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
kernel::loft_sheet_with_rails(&secs, &rails, crate::eval::kernel_rail_fit(*rail_fit))?
};
// Each wall named by the section curves it runs between, as a loft's sides are.
let loops: Vec<solidcore_record::Loop2> = secs.iter().map(|(_, c)| solidcore_record::Loop2 { segs: c.clone() }).collect();
let loops: Vec<solidcraft_geom::Loop2> = secs.iter().map(|(_, c)| solidcraft_geom::Loop2 { segs: c.clone() }).collect();
let traced = crate::lineage::SectionRoles { sections: sketches.iter().zip(&loops).map(|(s, lp)| (s, vec![lp])).collect() }.tool(body);
push_sheet(st, f, traced);
Ok(())
@@ -572,7 +572,7 @@ fn loop_patch(ss: &SolvedSketch, curves: &[String]) -> Result<kernel::Body> {
/// A network curve resolved in the feature's frame, owning what it is built from.
enum NetSource {
Sketch(solidcore_record::Plane, Vec<Seg2>),
Sketch(solidcraft_geom::Plane, Vec<Seg2>),
Points(Vec<Vec3>),
Edges(kernel::Body, Vec<Vec3>),
Point(Vec3),
@@ -662,7 +662,7 @@ fn boundary_frames(
use solidcraft_kernel::{Boundary, Frame};
// The stations along each curve, evenly in its own arclength, so every side carries the same
// number of frames and the patch's net is square.
let mut sides: Vec<Vec<solidcore_record::Vec3>> = Vec::with_capacity(curves.len());
let mut sides: Vec<Vec<solidcraft_geom::Vec3>> = Vec::with_capacity(curves.len());
for id in curves {
let ci = ss.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("sketch curve `{id}`")))?;
let pieces = ss.sketch.exact_pieces(ci);
@@ -690,7 +690,7 @@ fn boundary_frames(
}
// The middle of the loop, so every side leaves towards the inside of the patch.
let count = sides.iter().map(Vec::len).sum::<usize>().max(1) as f64;
let centre = sides.iter().flatten().fold(solidcore_record::Vec3::ZERO, |a, p| a + *p) * (1.0 / count);
let centre = sides.iter().flatten().fold(solidcraft_geom::Vec3::ZERO, |a, p| a + *p) * (1.0 / count);
let mut out = Vec::with_capacity(sides.len());
for (side, pts) in sides.iter().enumerate() {
let mut frames = Vec::with_capacity(pts.len());
@@ -829,8 +829,8 @@ fn loft_blend(
) -> Result<()> {
/// One section: a chain of sketch curves in its plane, or a body's edge chain.
enum Piece {
Sketch(solidcore_record::Plane, Vec<solidcore_record::Seg2>),
Body(String, Vec<solidcore_record::Vec3>),
Sketch(solidcraft_geom::Plane, Vec<solidcraft_geom::Seg2>),
Body(String, Vec<solidcraft_geom::Vec3>),
}
let mut pieces = Vec::with_capacity(sections.len());
let mut centres = Vec::with_capacity(sections.len());
@@ -840,7 +840,7 @@ fn loft_blend(
return Err(DocError::Invalid("a loft section picked from a body needs a chain of its edges".into()));
}
let n = sec.edges.len() as f64;
centres.push(sec.edges.iter().fold(solidcore_record::Vec3::ZERO, |a, p| a + **p) * (1.0 / n));
centres.push(sec.edges.iter().fold(solidcraft_geom::Vec3::ZERO, |a, p| a + **p) * (1.0 / n));
pieces.push(Piece::Body(name.to_string(), crate::plain(&sec.edges)));
continue;
}
@@ -858,7 +858,7 @@ fn loft_blend(
return Err(DocError::Invalid("each loft section must be one chain: one profile without holes, or curves meeting end to end".into()));
};
let pts: Vec<Vec2> = chain.iter().flat_map(|s| s.polyline(1e-2)).collect();
let centre = pts.iter().fold(solidcore_record::Vec3::ZERO, |a, p| a + ss.plane.to_world(*p)) * (1.0 / pts.len().max(1) as f64);
let centre = pts.iter().fold(solidcraft_geom::Vec3::ZERO, |a, p| a + ss.plane.to_world(*p)) * (1.0 / pts.len().max(1) as f64);
centres.push(centre);
pieces.push(Piece::Sketch(ss.plane, chain));
}
@@ -870,7 +870,7 @@ fn loft_blend(
let (from, to) = if i < last { (i, i + 1) } else { (last, last.saturating_sub(1)) };
let leave = match (centres.get(from), centres.get(to)) {
(Some(a), Some(b)) => *b - *a,
_ => solidcore_record::Vec3::ZERO,
_ => solidcraft_geom::Vec3::ZERO,
};
match piece {
Piece::Body(name, edges) => {
@@ -889,9 +889,9 @@ fn loft_blend(
/// The frames of one sketch chain, sampled by arclength at `stations` stations: a sketch has no
/// face behind it, so the surface leaves along the run with no curvature to repeat there.
fn chain_frames(
plane: &solidcore_record::Plane,
chain: &[solidcore_record::Seg2],
leave: solidcore_record::Vec3,
plane: &solidcraft_geom::Plane,
chain: &[solidcraft_geom::Seg2],
leave: solidcraft_geom::Vec3,
stations: usize,
) -> Vec<kernel::Frame> {
let mut pts: Vec<Vec2> = Vec::new();
@@ -920,19 +920,14 @@ fn chain_frames(
}
let (Some(a), Some(b)) = (pts.get(k).copied(), pts.get(k + 1).copied()) else {
if let Some(p) = pts.get(k).copied() {
out.push(kernel::Frame {
p: plane.to_world(p),
leave,
bend: solidcore_record::Vec3::ZERO,
bend_change: solidcore_record::Vec3::ZERO,
});
out.push(kernel::Frame { p: plane.to_world(p), leave, bend: solidcraft_geom::Vec3::ZERO, bend_change: solidcraft_geom::Vec3::ZERO });
}
continue;
};
let span = (b - a).len();
let t = if span > 1e-12 { ((want - walked) / span).clamp(0.0, 1.0) } else { 0.0 };
let p = a * (1.0 - t) + b * t;
out.push(kernel::Frame { p: plane.to_world(p), leave, bend: solidcore_record::Vec3::ZERO, bend_change: solidcore_record::Vec3::ZERO });
out.push(kernel::Frame { p: plane.to_world(p), leave, bend: solidcraft_geom::Vec3::ZERO, bend_change: solidcraft_geom::Vec3::ZERO });
}
out
}
+3 -3
View File
@@ -674,7 +674,7 @@ fn taper_envelope(t: &ThreadInfo, radius: f64, ends: [End; 2], (d0, from): (f64,
vec![Vec2::new(rc(-e), -e), Vec2::new(rc(len), len), Vec2::new(r_out, len), Vec2::new(r_out, -e)]
}
};
let region = Region2 { outer: solidcore_record::Loop2::polygon(&pts), holes: Vec::new() };
let region = Region2 { outer: solidcraft_geom::Loop2::polygon(&pts), holes: Vec::new() };
let mut ring = kernel::revolve(&plane, &[region], Vec2::new(0.0, 0.0), Vec2::new(0.0, 1.0), std::f64::consts::TAU)?;
Ok(ring.pop())
}
@@ -768,7 +768,7 @@ pub(super) fn cut(st: &mut ModelState, on: ThreadOn, (tip, angle, depth): (Threa
};
let mut grooves = Vec::new();
for g in thread_grooves(&t, radius, (cyl.start, cyl.end), ends, steep, ease, taper)? {
let section = Region2 { outer: solidcore_record::Loop2::polygon(&g.section), holes: Vec::new() };
let section = Region2 { outer: solidcraft_geom::Loop2::polygon(&g.section), holes: Vec::new() };
grooves.push(kernel::sweep_helix_radial(&g.plane, &section, g.base, g.axis, g.pitch, g.turns, &g.law)?);
}
if let Some(taper) = taper
@@ -872,7 +872,7 @@ fn tip_cones(
// In the plane: x radial, y outward along the axis from the end.
let plane = Plane { origin: t.axis_point + k * at, x: u, y: out_dir };
let pts = [Vec2::new(r_f, 0.0), Vec2::new(r_f, e), Vec2::new(r_out, e), Vec2::new(r_out, -(r_out - r_f) / slope)];
let region = Region2 { outer: solidcore_record::Loop2::polygon(&pts), holes: Vec::new() };
let region = Region2 { outer: solidcraft_geom::Loop2::polygon(&pts), holes: Vec::new() };
let mut ring = kernel::revolve(&plane, &[region], Vec2::new(0.0, 0.0), Vec2::new(0.0, 1.0), std::f64::consts::TAU)?;
out.push(ring.pop().ok_or_else(|| DocError::Invalid("chamfer".into()))?);
}
+4 -5
View File
@@ -4,8 +4,7 @@ use crate::config::ConfigFaceTarget;
use crate::{DocError, Document, EmbossFace, FeatureKind, Model, ModelBody, ModelState, PlaneRef, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use solidcore_record::Plane;
use solidcraft_geom::prelude::*;
use solidcraft_geom::Plane;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
@@ -500,7 +499,7 @@ pub fn validate_face_anchors_json(value: &Value) -> Result<()> {
#[cfg(test)]
mod tests {
use super::*;
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
fn fixture() -> (Document, ModelBody, usize, Plane) {
let mut doc = Document::default();
let kind: FeatureKind =
@@ -627,7 +626,7 @@ mod tests {
}
#[test]
fn extrude_cap_profile_reordering_never_retargets_an_ordinal_body() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
let mut sketch = solidcraft_sketch::Sketch::new();
for offset in [0., 30.] {
let points: Vec<_> = [(0., 0.), (10., 0.), (10., 10.), (0., 10.)]
@@ -720,7 +719,7 @@ mod tests {
}
#[test]
fn warm_root_body_offset_edit_matches_cold_attached_sketch_and_downstream_bounds() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
let (mut doc, _, _, _) = fixture();
let mut warm = Model::new();
warm.evaluate(&doc);
+1 -1
View File
@@ -4,7 +4,7 @@
//! The walk (`crate::walk`) is behind `transform_geometry`, `face_points`, `profile_sels` and
//! `point_expr`.
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
use crate::walk::Walk;
use crate::{FeatureKind, PointExpr, ProfileSel};
+15 -16
View File
@@ -1246,7 +1246,6 @@ fn normalise(d: &mut Document) -> Result<()> {
#[cfg(test)]
mod tests {
use super::*;
use solidcraft_geom::prelude::*;
/// A pending change that records that its step ran, in `trace`.
fn traced(name: &'static str) -> Part {
@@ -1677,7 +1676,7 @@ mod tests {
#[test]
fn version_twenty_three_designs_open_and_an_edge_flange_relief_is_saved_in_twenty_four() {
let flange = |shape: Option<&str>, size: Option<&str>| crate::FeatureKind::SheetFlange {
edges: vec![crate::FlangeEdge::at(solidcore_record::Vec3::ZERO)],
edges: vec![crate::FlangeEdge::at(solidcraft_geom::Vec3::ZERO)],
height: "10 mm".into(),
angle: "90 deg".into(),
radius: None,
@@ -1783,7 +1782,7 @@ mod tests {
#[test]
fn version_nine_wire_spaces_preserve_saved_points_and_explicit_markers() {
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
use solidcraft_sketch::{LinkKind, LinkSource, Sketch, WireSpace};
let mut sketch = Sketch::new();
sketch.add_drawn_wire(&[Vec3::new(12.0, 3.0, 4.0)]).unwrap();
@@ -1897,9 +1896,9 @@ mod tests {
through_all: false,
taper: Some("0 deg".into()),
corner: Default::default(),
to: Some(crate::FacePick(solidcore_record::Vec3::new(0.0, 0.0, 10.0))),
to: Some(crate::FacePick(solidcraft_geom::Vec3::new(0.0, 0.0, 10.0))),
to_offset: Some("offset + 1 mm".into()),
from: Some(crate::FacePick(solidcore_record::Vec3::new(0.0, 0.0, 5.0))),
from: Some(crate::FacePick(solidcraft_geom::Vec3::new(0.0, 0.0, 5.0))),
to_body_apart: false,
},
operation: Operation::NewBody,
@@ -1945,7 +1944,7 @@ mod tests {
#[test]
fn saved_offset_cannot_alias_and_overwrite_its_source_geometry() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::{CurveKind, Sketch};
let mut sketch = Sketch::new();
let center = sketch.add_point(Vec2::ZERO, None).unwrap();
@@ -2084,7 +2083,7 @@ mod tests {
#[test]
fn periodic_file_validation_refuses_damaged_closed_geometry() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::{CurveKind, Sketch};
let mut sketch = Sketch::new();
let mut pts = Vec::new();
@@ -2234,7 +2233,7 @@ mod tests {
#[test]
fn version_seven_origin_sketch_keeps_geometry_dimension_ids_and_expressions() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::{ConstraintKind, Sketch};
let mut d = Document::new("legacy origin dimensions");
d.set_param("reach", "25 mm", Some("mm"), None).unwrap();
@@ -2264,7 +2263,7 @@ mod tests {
#[test]
fn version_eight_persistent_angle_sweep_and_slot_dimensions_roundtrip() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::{ConstraintKind, Sketch};
let mut d = Document::new("persistent typed dimensions");
let heading = d.new_model_param("45 deg", "deg");
@@ -2383,12 +2382,12 @@ mod tests {
let (values, errors) = migrated.param_values();
assert!(errors.is_empty());
let p = migrated.resolve_plane(&values, plane, 0).unwrap();
assert_eq!(p.y, solidcore_record::Vec3::Z);
assert_eq!(p.normal(), -solidcore_record::Vec3::Y);
assert_eq!(p.origin, solidcore_record::Vec3::new(0.0, -7.0, 0.0));
assert_eq!(p.y, solidcraft_geom::Vec3::Z);
assert_eq!(p.normal(), -solidcraft_geom::Vec3::Y);
assert_eq!(p.origin, solidcraft_geom::Vec3::new(0.0, -7.0, 0.0));
assert_eq!(read(&migrated.to_json()).unwrap(), migrated);
let fresh = doc.resolve_plane(&values, &crate::PlaneRef::Origin { name: "XZ".into() }, 0).unwrap();
assert_eq!(fresh.normal(), solidcore_record::Vec3::Y);
assert_eq!(fresh.normal(), solidcraft_geom::Vec3::Y);
}
#[test]
@@ -2436,7 +2435,7 @@ mod tests {
let mut sketch = Sketch::new();
let mut previous = 0;
while sketch.points.len() < MAX_POINTS {
let i = sketch.add_point(solidcore_record::Vec2::new(0.001 * sketch.points.len() as f64, 0.0), None).unwrap();
let i = sketch.add_point(solidcraft_geom::Vec2::new(0.001 * sketch.points.len() as f64, 0.0), None).unwrap();
sketch.add_constraint(solidcraft_sketch::ConstraintKind::Coincident { p: previous, q: i }, None).unwrap();
previous = i;
}
@@ -2548,7 +2547,7 @@ mod tests {
#[test]
fn integer_valued_coordinates_are_range_checked_and_large_ids_still_load() {
let mut sketch = solidcraft_sketch::Sketch::new();
sketch.add_point(solidcore_record::Vec2::new(40.0, 0.0), None).unwrap();
sketch.add_point(solidcraft_geom::Vec2::new(40.0, 0.0), None).unwrap();
let mut d = Document::new("range");
d.add_feature(crate::FeatureKind::Sketch { plane: crate::PlaneRef::Origin { name: "XY".into() }, sketch }, None).unwrap();
let last = |v: &Value| v["features"][0]["sketch"]["points"].as_array().map_or(0, |a| a.len() - 1);
@@ -2605,7 +2604,7 @@ mod tests {
"orientation": {"direction": [0.0, 1.0, 0.0]}, "twist": "90 deg", "scale": "0.5"});
let k: FeatureKind = serde_json::from_value(new.clone()).unwrap();
let FeatureKind::SurfaceSweep { orientation, .. } = &k else { panic!("{k:?}") };
assert_eq!(*orientation, SweepOrientation::Direction(crate::Dir(solidcore_record::Vec3::Y)));
assert_eq!(*orientation, SweepOrientation::Direction(crate::Dir(solidcraft_geom::Vec3::Y)));
assert_eq!(serde_json::to_value(&k).unwrap(), new);
let parallel: SweepOrientation = serde_json::from_value(serde_json::json!("parallel")).unwrap();
assert_eq!(parallel, SweepOrientation::Parallel);
+4 -4
View File
@@ -411,9 +411,9 @@ mod tests {
#[test]
fn formats_one_through_eleven_keep_legacy_offset_geometry_and_ids() {
let mut sketch = solidcraft_sketch::Sketch::new();
let center = sketch.add_point(solidcore_record::Vec2::new(4., 7.), Some("center")).unwrap();
sketch.add_circle(solidcore_record::Vec2::new(4., 7.), 10., Some(center), Some("source")).unwrap();
sketch.add_circle(solidcore_record::Vec2::new(4., 7.), 9., Some(center), Some("offset")).unwrap();
let center = sketch.add_point(solidcraft_geom::Vec2::new(4., 7.), Some("center")).unwrap();
sketch.add_circle(solidcraft_geom::Vec2::new(4., 7.), 10., Some(center), Some("source")).unwrap();
sketch.add_circle(solidcraft_geom::Vec2::new(4., 7.), 9., Some(center), Some("offset")).unwrap();
sketch.offsets.push(solidcraft_sketch::OffsetChain {
sources: vec![("source".into(), true)],
outputs: vec!["offset".into()],
@@ -447,7 +447,7 @@ mod tests {
fn missing_boundary_or_defining_sketch_reopens_as_a_model_error_without_retarget() {
for missing_sketch in [false, true] {
let mut sketch = solidcraft_sketch::Sketch::new();
sketch.add_circle(solidcore_record::Vec2::new(0., 0.), 10., None, Some("unrelated-circle")).unwrap();
sketch.add_circle(solidcraft_geom::Vec2::new(0., 0.), 10., None, Some("unrelated-circle")).unwrap();
let mut document = Document::new("repairable lost boundary");
let id = document.add_feature(FeatureKind::Sketch { plane: crate::PlaneRef::Origin { name: "XY".into() }, sketch }, None).unwrap();
document
+1 -2
View File
@@ -13,11 +13,10 @@
//! of the profile's bounding box at the origin. The outline runs counter-clockwise and holes
//! clockwise, as [`Region2`] expects.
use solidcraft_geom::prelude::*;
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use solidcore_record::{Loop2, Region2, Seg2, Vec2, Vec3};
use solidcraft_geom::{Loop2, Region2, Seg2, Vec2, Vec3};
use crate::expr::Kind;
use crate::{DocError, Expr, Result};
+1 -1
View File
@@ -24,7 +24,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::sync::{Arc, OnceLock};
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
use solidcraft_kernel::history::{FaceOrigin, FaceProv, History};
use crate::ModelBody;
+1 -3
View File
@@ -3,9 +3,7 @@
//! measured in the face's plane. The edges are found again by their persistent names, else by
//! their stored points, so the hole follows them when the part changes.
use solidcore_record::Vec3;
use solidcraft_geom::Mesh;
use solidcraft_geom::prelude::*;
use solidcraft_geom::{Mesh, Vec3};
use crate::{DocError, ModelBody, ModelState, Result};
+1 -3
View File
@@ -14,12 +14,10 @@
//! linkage, a slider-crank) are met by moving the values of the other joints around the loop;
//! those that still disagree are reported.
use solidcraft_geom::prelude::*;
use std::collections::{BTreeMap, BTreeSet, VecDeque};
use serde::{Deserialize, Serialize};
use solidcore_record::Vec3;
use solidcraft_geom::Aabb3;
use solidcraft_geom::{Aabb3, Vec3};
use solidcraft_sketch::linalg;
+2 -4
View File
@@ -12,12 +12,10 @@
//! oscillating one from its arm pointing away from the cam's axis) and stops where it first
//! touches the profile: the follower stays in contact, as a spring would hold it.
use solidcraft_geom::prelude::*;
use std::f64::consts::TAU;
use serde::{Deserialize, Serialize};
use solidcore_record::{Vec2, Vec3};
use solidcraft_geom::{Aabb2, Aabb3, Ring};
use solidcraft_geom::{Aabb2, Aabb3, Ring, Vec2, Vec3};
use super::{JointKind, JointOrigin, MotionLink, Size, component_bodies, component_of, flip_of, occurrence_world, size_of};
use crate::{DocError, Document, IDENTITY, Mat, ModelState, Result, apply_point, apply_vector, mat_inverse, mat_mul};
@@ -488,7 +486,7 @@ pub fn sketch_profile<'a>(sk: &'a crate::SolvedSketch, curves: &[String]) -> Opt
}
/// A loop's polyline within `tol`, segment by segment: `None` past `MAX_PROFILE_POINTS` points.
fn sampled(lp: &solidcore_record::Loop2, tol: f64) -> Option<Vec<Vec2>> {
fn sampled(lp: &solidcraft_geom::Loop2, tol: f64) -> Option<Vec<Vec2>> {
let mut out = Vec::new();
for seg in &lp.segs {
let p = seg.polyline(tol);
+1 -2
View File
@@ -8,8 +8,7 @@
//! corner x and y run along the two edges, away from the corner into the face, in the order that
//! makes the frame right-handed.
use solidcore_record::Vec3;
use solidcraft_geom::prelude::*;
use solidcraft_geom::Vec3;
use super::{EdgeAxis, Snap, component_bodies, frame};
use crate::{DocError, Document, IDENTITY, Mat, ModelBody, ModelState, Result, apply_point, mat_inverse, mat_mul};
+1 -3
View File
@@ -4,11 +4,9 @@
//! A round origin's end is kept by the name of the face across the rim at that end, so it is the
//! same end after the design changes; an x reference keeps the origin's turn about its axis.
use solidcraft_geom::prelude::*;
use std::collections::{BTreeMap, BTreeSet};
use solidcore_record::{Loop2, Plane, Vec2, Vec3};
use solidcraft_geom::{Mesh, Ring};
use solidcraft_geom::{Loop2, Mesh, Plane, Ring, Vec2, Vec3};
use solidcraft_kernel::CylinderFace;
use super::{Snap, XRef, component_bodies};
+1 -2
View File
@@ -1,11 +1,10 @@
//! The instantaneous motion a joint value gives a selected occurrence. Probe the same
//! placement graph and loop solver as a drive, including reversed joints and motion links.
use solidcraft_geom::prelude::*;
use std::collections::BTreeSet;
use super::{Analysis, Held, MAX_LOOP_VALUES, Solution, apply_links, build, joint_values, link_chains, solve_holding_with_analysis};
use crate::{Document, Mat, ModelState, apply_point, apply_vector, mat_inverse, mat_mul};
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
/// A driveable value, expressed as an axis of the selected occurrence in world coordinates.
#[derive(Clone, Debug)]
+1 -2
View File
@@ -2,11 +2,10 @@
//! point of it reaches a target (and an axis of it points a given way), every value within its
//! limits, motion links followed, and the closed loops those values are in kept closed.
use solidcraft_geom::prelude::*;
use std::cell::Cell;
use std::collections::BTreeSet;
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
use solidcraft_sketch::linalg;
use super::{
+1 -1
View File
@@ -76,7 +76,7 @@ mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::joints::{JointKind, JointOrigin, Snap};
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
fn joint(kind: JointKind) -> Joint {
let at = |occurrence| JointOrigin { occurrence, snap: Snap::Point { point: Vec3::ZERO, z: None, x: None } };
+5 -6
View File
@@ -9,10 +9,9 @@
//! bodies the feature built itself (whose faces are named by their role: the sketch curve they
//! sweep, the cap they are). Naming (`history_names`) reads it; nothing here samples geometry.
use solidcraft_geom::prelude::*;
use std::sync::{Arc, OnceLock};
use solidcore_record::{Region2, Vec3};
use solidcraft_geom::{Region2, Vec3};
use solidcraft_kernel::Body;
use solidcraft_kernel::history::{BodyHistory, Change, FaceOrigin, FaceProv, History};
@@ -233,7 +232,7 @@ impl<'a> ProfileRoles<'a> {
if history.operands != 0 {
return None;
}
let segments: Vec<&solidcore_record::Seg2> =
let segments: Vec<&solidcraft_geom::Seg2> =
self.region.map(|r| std::iter::once(&r.outer).chain(&r.holes).flat_map(|l| &l.segs).collect()).unwrap_or_default();
let curves = self.sketch.map(crate::config::ExactSketchCurves::new);
// Every curve the segment is drawn on (one, or several coincident or collinear ones).
@@ -295,7 +294,7 @@ impl<'a> ProfileRoles<'a> {
/// numbered through the chains in order. It has no caps.
pub(crate) struct ChainRoles<'a> {
pub sketch: &'a SolvedSketch,
pub chains: &'a [Vec<solidcore_record::Seg2>],
pub chains: &'a [Vec<solidcraft_geom::Seg2>],
}
impl ChainRoles<'_> {
@@ -306,7 +305,7 @@ impl ChainRoles<'_> {
}
fn of(&self, history: &History) -> Vec<Vec<String>> {
let segments: Vec<&solidcore_record::Seg2> = self.chains.iter().flatten().collect();
let segments: Vec<&solidcraft_geom::Seg2> = self.chains.iter().flatten().collect();
let curves = crate::config::ExactSketchCurves::new(self.sketch);
history
.faces
@@ -337,7 +336,7 @@ impl ChainRoles<'_> {
/// answering to the others; its caps `start` and `end`.
pub(crate) struct SectionRoles<'a> {
/// Each section's sketch and loops (outer, then holes), in the order given.
pub sections: Vec<(&'a SolvedSketch, Vec<&'a solidcore_record::Loop2>)>,
pub sections: Vec<(&'a SolvedSketch, Vec<&'a solidcraft_geom::Loop2>)>,
}
impl SectionRoles<'_> {
+1 -1
View File
@@ -3,7 +3,7 @@
//! shown without changing the design).
use serde::{Deserialize, Serialize};
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
/// The motion retained by an associative trace. Joint bounds keep their expressions, in
/// command units (degrees or millimetres); studies are referenced by their stable identity.
+1 -3
View File
@@ -10,9 +10,7 @@
//! ([`crate::DocError::Reselect`]). A face whose name was worked out from geometry, found where
//! the reference's point no longer lies, is reported: only geometry decided.
use solidcore_record::Vec3;
use solidcraft_geom::Mesh;
use solidcraft_geom::prelude::*;
use solidcraft_geom::{Mesh, Vec3};
use crate::history_names::{Match, Names};
use crate::naming::{self, closest, edge_names, face_names, strip_piece};
+1 -2
View File
@@ -1,6 +1,5 @@
use crate::{Document, ModelBody, ModelState};
use solidcore_record::{Plane, Vec3};
use solidcraft_geom::prelude::*;
use solidcraft_geom::{Plane, Vec3};
use solidcraft_sketch::{LinkKind, LinkSource, Sketch};
use std::collections::BTreeMap;
use std::sync::Arc;
+2 -4
View File
@@ -15,11 +15,9 @@
//! are looked up on the input's faces. Those names are marked unverified, and references that
//! resolve through them say so. Names are worked out lazily, on first request.
use solidcraft_geom::prelude::*;
use std::sync::{Arc, OnceLock};
use solidcore_record::{Vec2, Vec3};
use solidcraft_geom::{Aabb3, Mesh};
use solidcraft_geom::{Aabb3, Mesh, Vec2, Vec3};
use crate::history_names::Names;
use crate::lineage::Lineage;
@@ -1448,7 +1446,7 @@ mod tests {
let ss = crate::SolvedSketch {
feature: 1,
name: "S".into(),
plane: solidcore_record::Plane::XY,
plane: solidcraft_geom::Plane::XY,
sketch,
report,
profiles,
+1 -1
View File
@@ -1,7 +1,7 @@
//! Persistable opaque optical parameters, independent of engineering material data.
use serde::{Deserialize, Serialize};
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
use solidcraft_render::surface::{OpaqueSurface, SurfaceError, srgb8_to_linear};
/// Explicitly selected opaque response. Existing scalar looks have no such record.
+1 -3
View File
@@ -1,11 +1,9 @@
//! Reference triangles belong to a definition, even while its occurrences move. Keep one lazy
//! source mesh and map it into each pose; retain the exact placed B-rep separately in ModelBody.
use solidcraft_geom::prelude::*;
use std::sync::{Arc, OnceLock};
use solidcore_record::Vec3;
use solidcraft_geom::Mesh;
use solidcraft_geom::tess::TessPlan;
use solidcraft_geom::{Mesh, Vec3};
use solidcraft_kernel::{self as kernel, Body};
use super::ModelBody;
+1 -2
View File
@@ -5,11 +5,10 @@
//! cloud's own `origin` (double precision), so a scan far from the design's origin keeps its
//! detail.
use solidcraft_geom::prelude::*;
use std::sync::Arc;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
use crate::canvas::{base64_decode, base64_encode};
pub use crate::point_cloud_complex::{Column, Complex, Filter, Floats, Keep, MAX_COLUMNS, MAX_NAMES, Names};
@@ -1,4 +1,4 @@
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
use super::*;
use crate::IDENTITY;
+1 -1
View File
@@ -5,7 +5,7 @@
use std::ops::{Deref, DerefMut};
use serde::{Deserialize, Serialize};
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
macro_rules! point_type {
($(#[$doc:meta])* $name:ident) => {
+1 -2
View File
@@ -3,8 +3,7 @@
//! sketch on a face holds ([`Document::flat_face_target`]), so a rebuild that adds, removes or
//! reorders faces finds the same face again, or says it is gone.
use solidcore_record::Vec3;
use solidcraft_geom::prelude::*;
use solidcraft_geom::Vec3;
use crate::config::ConfigFaceTarget;
use crate::{DocError, Document, ModelBody, ModelState, Result};
+1 -2
View File
@@ -1,5 +1,4 @@
//! Exact profile references through analytic offset topology transitions.
use solidcraft_geom::prelude::*;
use std::collections::BTreeSet;
use crate::{DocError, ProfileSel, Result, SolvedSketch, WholeProfileBoundary};
@@ -365,7 +364,7 @@ mod tests {
#![allow(clippy::unwrap_used, clippy::panic)]
use super::*;
use serde_json::json;
use solidcore_record::{Plane, Vec2};
use solidcraft_geom::{Plane, Vec2};
use solidcraft_sketch::{Sketch, find_profiles, solve};
fn rounded_source(sketch: &mut Sketch, radius: f64) -> Vec<(String, bool)> {
+1 -1
View File
@@ -293,7 +293,7 @@ mod tests {
fn partial_models_go_only_to_a_watcher_that_wants_them() {
let mut doc = crate::Document::new("partial");
let block = crate::FeatureKind::Box {
corner: crate::point_expr(solidcore_record::Vec3::ZERO),
corner: crate::point_expr(solidcraft_geom::Vec3::ZERO),
length: "2 mm".into(),
width: "3 mm".into(),
height: "4 mm".into(),
+6 -8
View File
@@ -10,11 +10,9 @@
//! lines and circular arcs are recovered exactly, the rest (an ellipse from a tilted circle,
//! a spline edge running out of the plane) becomes a chain of lines.
use solidcraft_geom::prelude::*;
use std::collections::BTreeMap;
use solidcore_record::{Plane, Seg2, Vec2, Vec3};
use solidcraft_geom::Mesh;
use solidcraft_geom::{Mesh, Plane, Seg2, Vec2, Vec3};
use solidcraft_sketch::{CurveKind, LinkGeom, LinkKind, LinkSource, Sketch, WireCurve};
use crate::eval::ModelState;
@@ -365,7 +363,7 @@ fn edge_curves(body: Option<&crate::ModelBody>, e: usize) -> Vec<WireCurve> {
record
.edges
.get(e)
.filter(|edge| !matches!(edge.curve, solidcore_record::brep::Curve::SurfaceIntersection { .. }))
.filter(|edge| !matches!(edge.curve, solidcraft_geom::brep::Curve::SurfaceIntersection { .. }))
.map(|edge| vec![WireCurve { curve: edge.parameters().clone(), interval: edge.interval }])
.unwrap_or_default()
}
@@ -396,9 +394,9 @@ fn projected_wires(
/// in front of it.
fn first_hits(
target: &crate::ModelBody,
curves: Vec<(solidcore_record::brep::Curve, [f64; 2])>,
curves: Vec<(solidcraft_geom::brep::Curve, [f64; 2])>,
toward: Vec3,
) -> Result<Vec<(solidcore_record::brep::Curve, [f64; 2])>> {
) -> Result<Vec<(solidcraft_geom::brep::Curve, [f64; 2])>> {
let Some(dir) = toward.normalized() else { return Ok(curves) };
let mesh = target.mesh();
let reach = 2.0 * mesh.bounds().diagonal().max(1e-9);
@@ -420,7 +418,7 @@ fn first_hits(
/// direction they come (a projection split at a sphere's pole into two pieces that both end there
/// joins again there, the second run the other way), each wire its points (for display) and its
/// exact curves.
fn joined_wires(curves: Vec<(solidcore_record::brep::Curve, [f64; 2])>, tol: f64) -> Result<(Vec<Vec<Vec3>>, Vec<Vec<WireCurve>>)> {
fn joined_wires(curves: Vec<(solidcraft_geom::brep::Curve, [f64; 2])>, tol: f64) -> Result<(Vec<Vec<Vec3>>, Vec<Vec<WireCurve>>)> {
let (mut wires, mut exact): (Vec<Vec<Vec3>>, Vec<Vec<WireCurve>>) = (Vec::new(), Vec::new());
for (curve, interval) in curves {
let pts = crate::kernel::PathPiece::Segs(crate::kernel::curve_path(&curve, interval)?).points();
@@ -468,7 +466,7 @@ fn joined_wires(curves: Vec<(solidcore_record::brep::Curve, [f64; 2])>, tol: f64
/// own knots (a line's `1 - t`, a circle's or ellipse's angle negated with its y axis turned).
/// Exact; `None` for a curve kind it doesn't turn.
fn reversed(c: &WireCurve) -> Option<WireCurve> {
use solidcore_record::brep::{Curve, Frame};
use solidcraft_geom::brep::{Curve, Frame};
let [t0, t1] = c.interval;
let flip = |frame: &Frame| Frame { origin: frame.origin, x_axis: frame.x_axis, y_axis: frame.y_axis.map(|v| -v) };
let (curve, interval) = match &c.curve {
+1 -3
View File
@@ -1,9 +1,7 @@
//! 3D sketch curves from the model: curves projected onto faces (on their display meshes), the
//! bodies and faces an intersection curve is taken on, and spun profiles.
use solidcore_record::{Plane, Vec2, Vec3};
use solidcraft_geom::Mesh;
use solidcraft_geom::prelude::*;
use solidcraft_geom::{Mesh, Plane, Vec2, Vec3};
use solidcraft_sketch::LinkSource;
use crate::eval::ModelState;
+10 -12
View File
@@ -1,11 +1,9 @@
//! Stable face projection order: outer boundary first, then holes by geometry.
//! B-rep edge indices depend on face enumeration and must not choose sketch curve IDs.
use solidcraft_geom::prelude::*;
use std::collections::{BTreeMap, BTreeSet};
use solidcore_record::{Plane, Vec3};
use solidcraft_geom::{Aabb3, Mesh};
use solidcraft_geom::{Aabb3, Mesh, Plane, Vec3};
use crate::{DocError, Result};
@@ -230,7 +228,7 @@ mod tests {
#[test]
fn coarsely_tessellated_round_silhouette_fits_an_arc() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::LinkGeom;
let on = |deg: f64| Vec2::new(5. + 5. * deg.to_radians().cos(), 5. + 5. * deg.to_radians().sin());
let mut chain = vec![Vec2::new(0., -20.)];
@@ -243,7 +241,7 @@ mod tests {
#[test]
fn tangent_snapping_leaves_unrelated_collinear_lines_alone() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::LinkGeom;
let v = Vec2::new;
// A rounded corner of radius 2 meeting the top edge, and an unrelated collinear edge.
@@ -260,7 +258,7 @@ mod tests {
#[test]
fn tangent_snapping_leaves_a_line_further_along_alone() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::LinkGeom;
let v = Vec2::new;
let y = |x: f64| 2.0 - (4.0 - x * x).sqrt();
@@ -281,7 +279,7 @@ mod tests {
#[test]
fn tangent_snapping_gives_one_line_to_one_end_of_an_arc() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::LinkGeom;
let v = Vec2::new;
let y = |x: f64| 2.0 - (4.0 - x * x).sqrt();
@@ -295,7 +293,7 @@ mod tests {
#[test]
fn tangent_snapping_moves_the_other_end_of_an_arc_to_its_next_line() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::LinkGeom;
let v = Vec2::new;
let y = |x: f64| 2.0 - (4.0 - x * x).sqrt();
@@ -314,7 +312,7 @@ mod tests {
#[test]
fn tangent_snapping_closes_a_conic_corner_in_any_order() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::LinkGeom;
// A square of side 150 with a conic round of 10 mm legs at each corner, fitted with lines
// and arcs the way a silhouette is, then snapped with the pieces in each order.
@@ -412,7 +410,7 @@ mod tests {
#[test]
fn family_line_normalization_preserves_corners_arcs_branches_and_retracing() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::LinkGeom;
let line = |a, b| LinkGeom::Line(Vec2::new(a, 0.), Vec2::new(b, 0.));
assert_eq!(super::super::merge_family_lines(vec![line(0., 1.), line(2., 1.)]).unwrap(), vec![line(0., 2.)]);
@@ -500,7 +498,7 @@ mod tests {
#[test]
fn rounded_boundary_arc_sweeps_survive_individual_edge_reversal() {
use solidcore_record::{Seg2, Vec2};
use solidcraft_geom::{Seg2, Vec2};
use solidcraft_sketch::LinkGeom;
let mut mesh = Mesh::default();
for (extent, radius) in [(10.0, 3.0), (8.0, 1.0)] {
@@ -595,7 +593,7 @@ mod tests {
#[test]
fn curve_ends_a_hair_apart_become_one_point_and_distant_ones_stay() {
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::LinkGeom;
let mut geom = vec![
LinkGeom::Line(Vec2::new(0., 0.), Vec2::new(10., 0.)),
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::{
render_surface::{RenderBodyTarget, bounded_sequence},
};
use serde::{Deserialize, Serialize};
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
use solidcraft_render::{
surface::OpaqueSurface,
transmission::{Transmission, subsurface::Subsurface},
+1 -1
View File
@@ -3,7 +3,7 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
use solidcraft_render::{BackendId, Camera, RenderFormat, RenderLimits, RenderMode, UpAxis};
pub use crate::render_job::{CpuJob, GpuJob, RenderJob, SkyEnvironment};
+1 -3
View File
@@ -10,9 +10,7 @@
//! together; the flat solid is the outline extruded through the thickness.
use serde::{Deserialize, Serialize};
use solidcore_record::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
use solidcraft_geom::prelude::*;
use solidcraft_geom::{Aabb2, LINEAR_TOL, Ring};
use solidcraft_geom::{Aabb2, LINEAR_TOL, Loop2, Plane, Region2, Ring, Seg2, Vec2, Vec3};
use solidcraft_kernel::{self as kernel, Body, EdgeSpec, FaceSpec, SurfSpec};
use crate::expr::{self, Kind, Value};
+1 -3
View File
@@ -47,12 +47,10 @@
//! edges that end at a bend or a cut edge, loops of bends, two faces that meet along more than
//! one edge, a wall on part of an edge of a face other than the fixed one.
use solidcraft_geom::prelude::*;
use std::collections::BTreeMap;
use std::f64::consts::{FRAC_PI_2, PI, TAU};
use solidcore_record::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
use solidcraft_geom::{Mesh, Ring, UnitVec3};
use solidcraft_geom::{Loop2, Mesh, Plane, Region2, Ring, Seg2, UnitVec3, Vec2, Vec3};
use solidcraft_kernel::{Body, FaceSurface, face_surface};
use super::{FlatWork, ReliefOverride, RuleValues, SheetBody, SheetFlange, base_flange, polys_cross, signed_area, snap};
+1 -2
View File
@@ -5,8 +5,7 @@
//! neighbouring flange), the rim along the bend there, `<flange>:rim.ext0` and `rim.ext1`. A panel is `base` or a flange's tag (`F<feature>.<k>`), so the names
//! say what each face is and stay with it when sizes change or other flanges come and go.
use solidcore_record::{Plane, Vec2, Vec3};
use solidcraft_geom::prelude::*;
use solidcraft_geom::{Plane, Vec2, Vec3};
use super::SheetBody;
use crate::{ModelBody, ModelState, apply_point, mat_inverse};
+1 -2
View File
@@ -5,10 +5,9 @@
//! between the points, up to the most a spline takes. When the parameters change, the same
//! number of points is placed again (`Document::apply_equation_values`), before every solve.
use solidcraft_geom::prelude::*;
use std::collections::BTreeMap;
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::{EquationCurve, MAX_COORD, MAX_EQUATION_TEXT, MAX_SPLINE_POINTS, Sketch, VariableUnit, fit_beziers_mode};
use crate::expr::{self, Defaults, Kind, Value};
+1 -1
View File
@@ -1,4 +1,4 @@
use solidcore_record::{Vec2, Vec3};
use solidcraft_geom::{Vec2, Vec3};
use solidcraft_kernel::measure;
use solidcraft_sketch::{ConstraintKind, Sketch};
+1 -1
View File
@@ -5,7 +5,7 @@
//! traits here carry the walks through the types the fields have: options, lists, arrays, planes
//! and construction definitions.
pub(crate) use solidcore_record::Vec3;
pub(crate) use solidcraft_geom::Vec3;
pub(crate) use crate::ProfileSel;
use crate::construct::{AxisDef, GeoRef, PointDef};
-1
View File
@@ -35,7 +35,6 @@ name = "sketch_target_bench"
required-features = ["solver-profiling"]
[dependencies]
solidcore-record = { workspace = true }
solidcraft-geom = { workspace = true }
solidcraft-sketch = { workspace = true }
solidcraft-kernel = { workspace = true }
@@ -3,8 +3,8 @@
use std::{error::Error, io::Write, path::PathBuf, sync::Arc, time::Instant};
use serde_json::{Value, json};
use solidcore_record::Vec2;
use solidcraft_engine::Session;
use solidcraft_geom::Vec2;
use solidcraft_sketch::{ConstraintKind, Sketch, profiling::capture};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
+1 -1
View File
@@ -2,8 +2,8 @@
use std::{error::Error, io::Write, path::PathBuf, sync::Arc, time::Instant};
use serde_json::{Value, json};
use solidcore_record::Vec2;
use solidcraft_engine::Session;
use solidcraft_geom::Vec2;
use solidcraft_sketch::{Analysis, ConstraintKind, Hold, Sketch, profiling::capture, solve_with};
type Result<T> = std::result::Result<T, Box<dyn Error>>;
@@ -3,7 +3,7 @@
mod four_bar;
use four_bar::Result;
use serde_json::json;
use solidcore_record::Vec2;
use solidcraft_geom::Vec2;
use solidcraft_sketch::profiling::capture;
use std::{io::Write, time::Instant};
+10 -11
View File
@@ -2,7 +2,6 @@
//! `examples/regressions/` (each script's description says what went wrong).
use serde_json::{Value, json};
use solidcraft_geom::prelude::*;
use crate::Session;
@@ -229,10 +228,10 @@ fn shallow_cuts_rebuild_after_inserting_an_earlier_pocket() {
let actual = volume(&session, "Plate");
assert!((actual - expected).abs() < 0.02, "{actual} vs {expected}");
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap();
assert!(!mesh.contains(solidcore_record::Vec3::new(12.0, 14.0, 9.0)));
assert!(mesh.contains(solidcore_record::Vec3::new(12.0, 14.0, 8.0)));
assert!(!mesh.contains(solidcore_record::Vec3::new(20.0, 20.0, 9.5)));
assert!(mesh.contains(solidcore_record::Vec3::new(20.0, 20.0, 9.0)));
assert!(!mesh.contains(solidcraft_geom::Vec3::new(12.0, 14.0, 9.0)));
assert!(mesh.contains(solidcraft_geom::Vec3::new(12.0, 14.0, 8.0)));
assert!(!mesh.contains(solidcraft_geom::Vec3::new(20.0, 20.0, 9.5)));
assert!(mesh.contains(solidcraft_geom::Vec3::new(20.0, 20.0, 9.0)));
}
}
@@ -257,7 +256,7 @@ fn eighteen_tabs_join_the_ribs_of_a_chamfered_base() {
println!("#2059 volume {got:.6} / {expected:.6} mm³; one valid shell, eighteen tabs");
for k in 0..6 {
let angle = k as f64 * std::f64::consts::TAU / 6.0;
let p = |x: f64, y: f64, z| solidcore_record::Vec3::new(x * angle.cos() - y * angle.sin(), x * angle.sin() + y * angle.cos(), z);
let p = |x: f64, y: f64, z| solidcraft_geom::Vec3::new(x * angle.cos() - y * angle.sin(), x * angle.sin() + y * angle.cos(), z);
for x in [15.5, 25.5, 35.5] {
assert!(mesh.contains(p(x, 0.0, 12.0)), "tab {k} at {x}");
assert!(!mesh.contains(p(x, 1.2, 12.0)), "tab {k} grew sideways at {x}");
@@ -800,7 +799,7 @@ fn closed_sweep_redefines_to_a_half_turn_torus_cut() {
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.02)).unwrap();
let expected = (std::f64::consts::PI * 64.0 - 64.0) * std::f64::consts::TAU * 30.0;
assert!((mesh.measure().volume - expected).abs() < expected * 0.015);
for (point, material) in [(solidcore_record::Vec3::new(30.0, 0.0, 0.0), false), (solidcore_record::Vec3::new(30.0, 0.0, 7.0), true)] {
for (point, material) in [(solidcraft_geom::Vec3::new(30.0, 0.0, 0.0), false), (solidcraft_geom::Vec3::new(30.0, 0.0, 7.0), true)] {
assert_eq!(mesh.contains(point), material, "material at {point:?}");
}
}
@@ -832,7 +831,7 @@ fn many_extrusion_profiles_preserve_material_and_body_identity() {
assert!((volume(&s, "Stock") - expected).abs() < 1e-6);
let stock = state.body("Stock").unwrap();
for (point, present) in [([3.0, 3.0, 22.0], operation == "join"), ([3.0, 3.0, 18.0], operation != "cut"), ([7.0, 3.0, 18.0], true)] {
assert_eq!(stock.mesh().contains(solidcore_record::Vec3::from(point)), present, "{operation}: {point:?}");
assert_eq!(stock.mesh().contains(solidcraft_geom::Vec3::from(point)), present, "{operation}: {point:?}");
}
assert!(stock.body.validity().is_empty());
assert!(s.revision > before);
@@ -865,10 +864,10 @@ fn many_circular_extrusion_profiles_cut_a_curved_target() {
let stock = state.body("Stock").unwrap();
assert!(stock.body.validity().is_empty());
let mesh = stock.mesh();
assert!(mesh.contains(solidcore_record::Vec3::new(0.0, 0.0, 10.0)));
assert!(mesh.contains(solidcraft_geom::Vec3::new(0.0, 0.0, 10.0)));
for x in [-9.0, -3.0, 3.0, 9.0] {
for y in [-9.0, -3.0, 3.0, 9.0] {
assert!(!mesh.contains(solidcore_record::Vec3::new(x, y, 10.0)), "hole at {x}, {y}");
assert!(!mesh.contains(solidcraft_geom::Vec3::new(x, y, 10.0)), "hole at {x}, {y}");
}
}
}
@@ -928,7 +927,7 @@ fn unsupported_face_groups_are_atomic() {
#[test]
fn imported_rotated_face_extrusions_preserve_other_operations_and_bodies() {
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
let mut source = Session::default();
source.execute("solid.box", &json!({"corner":[-16,-16,-3],"length":32,"width":32,"height":1,"body_name":"Other"})).unwrap();
source.execute("solid.cylinder", &json!({"radius":14,"height":8,"body_name":"Stock"})).unwrap();
+1 -1
View File
@@ -2166,7 +2166,7 @@ mod tests {
assert!(s.camera.is_none());
let r = run(&mut s, "view.apply", json!({"view": "Photo"}));
let c = s.camera.expect("a camera for the view");
assert_eq!((c.target, c.yaw, c.pitch, c.distance), (solidcore_record::Vec3::new(1.0, 2.0, 3.0), 0.5, -0.6, 150.0));
assert_eq!((c.target, c.yaw, c.pitch, c.distance), (solidcraft_geom::Vec3::new(1.0, 2.0, 3.0), 0.5, -0.6, 150.0));
assert_eq!(r["camera"]["yaw"], 0.5);
// A standard view looks from its direction and frames the model.
run(&mut s, "view.apply", json!({"view": "top"}));
+1 -2
View File
@@ -10,9 +10,8 @@
//! size), and the decoder itself runs under the same limit.
use serde_json::{Value, json};
use solidcore_record::Vec2;
use solidcraft_doc::canvas::{Canvas, base64_decode, image_info};
use solidcraft_geom::prelude::*;
use solidcraft_geom::Vec2;
use solidcraft_io::Geom2;
use super::CommandSpec;
+6 -7
View File
@@ -11,7 +11,6 @@ use std::sync::Arc;
use serde_json::{Value, json};
use solidcraft_geom::mesh_distance::box_gap;
use solidcraft_geom::prelude::*;
use solidcraft_geom::{Aabb3, Mesh};
use solidcraft_kernel::{BodyDistance, DistanceShape};
@@ -59,7 +58,7 @@ pub(crate) struct PairCache(HashMap<(u64, u64, [u64; 3]), PairResult>);
#[derive(Clone, Default)]
struct PairResult {
/// Shared volume, area, box and centre.
overlap: Option<(f64, f64, Aabb3, solidcore_record::Vec3)>,
overlap: Option<(f64, f64, Aabb3, solidcraft_geom::Vec3)>,
distance: Option<BodyDistance>,
unchecked: Option<String>,
}
@@ -191,7 +190,7 @@ fn clearance(s: &mut Session, p: &Value) -> Result<Value> {
p.into_iter().for_each(|v| bx.add(v));
}
let chord = b.display_tol();
let g = solidcore_record::Vec3::new(1.0, 1.0, 1.0) * if chord.is_finite() && chord > 0.0 { chord } else { 0.0 };
let g = solidcraft_geom::Vec3::new(1.0, 1.0, 1.0) * if chord.is_finite() && chord > 0.0 { chord } else { 0.0 };
if bx.is_empty() { bx } else { Aabb3 { min: bx.min - g, max: bx.max + g } }
}
_ => Aabb3::EMPTY,
@@ -199,7 +198,7 @@ fn clearance(s: &mut Session, p: &Value) -> Result<Value> {
.collect();
// Pairs whose boxes, grown by half the clearance each, meet.
let half = solidcore_record::Vec3::new(1.0, 1.0, 1.0) * (gap / 2.0);
let half = solidcraft_geom::Vec3::new(1.0, 1.0, 1.0) * (gap / 2.0);
let grown: Vec<Aabb3> = boxes.iter().map(|b| if b.is_empty() { *b } else { Aabb3 { min: b.min - half, max: b.max + half } }).collect();
let (close, truncated) = super::joints::overlapping_pairs(&grown, MAX_PAIRS);
let has_mesh = |i: usize| meshes.get(i).is_some_and(Option::is_some);
@@ -407,14 +406,14 @@ fn shared_region(a: &DistanceShape, b: &DistanceShape) -> Aabb3 {
return Aabb3::EMPTY;
}
let grow = 1.0 + 0.05 * min.dist(max);
let g = solidcore_record::Vec3::new(grow, grow, grow);
let g = solidcraft_geom::Vec3::new(grow, grow, grow);
Aabb3 { min: min - g, max: max + g }
}
/// [`solidcraft_kernel::measure`] of `body` moved by `-at` and back: far from the origin, the
/// centre of a thin part loses its precision (its moments are large numbers that nearly cancel).
fn measure_near_origin(body: &solidcraft_kernel::Body, at: solidcore_record::Vec3) -> solidcraft_kernel::Result<solidcraft_kernel::BodyMeasure> {
let moved = solidcraft_kernel::transform(body, -at, solidcore_record::Vec3::ZERO, solidcore_record::Vec3::Z, 0.0)?;
fn measure_near_origin(body: &solidcraft_kernel::Body, at: solidcraft_geom::Vec3) -> solidcraft_kernel::Result<solidcraft_kernel::BodyMeasure> {
let moved = solidcraft_kernel::transform(body, -at, solidcraft_geom::Vec3::ZERO, solidcraft_geom::Vec3::Z, 0.0)?;
let mut m = solidcraft_kernel::measure(&moved)?;
m.centroid += at;
m.bbox = Aabb3 { min: m.bbox.min + at, max: m.bbox.max + at };
+2 -3
View File
@@ -41,7 +41,7 @@ fn copy(s: &mut Session, p: &Value) -> Result<Value> {
pub(in crate::cmd) fn explicit_planes(p: &mut PlaneRef) {
match p {
PlaneRef::Origin { name } => {
if let Some(plane) = solidcore_record::Plane::named(name) {
if let Some(plane) = solidcraft_geom::Plane::named(name) {
*p = PlaneRef::Custom { plane };
}
}
@@ -243,7 +243,6 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
#[cfg(test)]
mod tests {
use serde_json::{Value, json};
use solidcraft_geom::prelude::*;
use crate::Session;
@@ -310,7 +309,7 @@ mod tests {
#[test]
fn foreign_sketch_paste_preserves_world_position_in_a_placed_component() {
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
let mut source = dimensioned_sketch();
let arc = [Vec3::new(1., 0., 0.), Vec3::new(0., 0., 1.), Vec3::new(-1., 0., 0.)];
+1 -1
View File
@@ -32,7 +32,7 @@ fn independent_links(sketch: &solidcraft_sketch::Sketch, features: &BTreeSet<u64
L::Text { .. } | L::SketchAxis { .. } => true,
L::SketchCurve { sketch, .. } | L::SketchPoint { sketch, .. } => features.contains(sketch),
L::Origin | L::Axis { .. } => !moved,
L::Plane { name } => !moved && solidcore_record::Plane::named(name).is_some(),
L::Plane { name } => !moved && solidcraft_geom::Plane::named(name).is_some(),
_ => false,
};
if !supported {
+1 -3
View File
@@ -6,10 +6,8 @@
//! the two cuts a − b and b − a that lie there, as `inspect.measure` measures a body.
use serde_json::{Value, json};
use solidcore_record::Vec3;
use solidcraft_geom::Aabb3;
use solidcraft_geom::compare::{self, CompareOptions, Side};
use solidcraft_geom::prelude::*;
use solidcraft_geom::{Aabb3, Vec3};
use super::CommandSpec;
use crate::params::bad;
+1 -2
View File
@@ -3,12 +3,11 @@
//! belong to their feature's component unless moved; occurrences place components (instances
//! share contents, "paste new" copies them) and can be grounded and moved.
use solidcraft_geom::prelude::*;
use std::collections::HashMap;
use serde_json::{Value, json};
use solidcore_record::Vec3;
use solidcraft_doc::{Feature, FeatureKind, Mat, Occurrence};
use solidcraft_geom::Vec3;
use super::CommandSpec;
use crate::params::{bad, bool_, str_, string_list, vec3};
+1 -2
View File
@@ -3,9 +3,8 @@
//! without modifying shared component geometry across other instances.
use serde_json::{Value, json};
use solidcore_record::Vec3;
use solidcraft_doc::{IDENTITY, mat_inverse, mat_mul, rigid};
use solidcraft_geom::prelude::*;
use solidcraft_geom::Vec3;
use super::{occurrence, occurrence_path, valid_position};
use crate::params::{bad, vec3};
+4 -5
View File
@@ -1,6 +1,5 @@
//! Independent copies of a component definition graph, assembled and evaluated atomically.
use solidcraft_doc::BodyName;
use solidcraft_geom::prelude::*;
use std::collections::{BTreeMap, BTreeSet};
use crate::params::bad;
@@ -11,7 +10,7 @@ use solidcraft_doc::{Document, Feature, FeatureKind, Mat, Occurrence};
const CMD: &str = "component.paste_new";
const MAX_WORK: usize = 10_000;
fn mesh_distance(mesh: &solidcraft_geom::Mesh, point: solidcore_record::Vec3) -> f64 {
fn mesh_distance(mesh: &solidcraft_geom::Mesh, point: solidcraft_geom::Vec3) -> f64 {
mesh.triangles
.iter()
.filter_map(|triangle| mesh.tri(triangle))
@@ -34,7 +33,7 @@ fn mesh_distance(mesh: &solidcraft_geom::Mesh, point: solidcore_record::Vec3) ->
.fold(f64::INFINITY, f64::min)
}
fn material_probe_matches(a: &solidcraft_geom::Mesh, b: &solidcraft_geom::Mesh, point: solidcore_record::Vec3, tol_a: f64, tol_b: f64) -> bool {
fn material_probe_matches(a: &solidcraft_geom::Mesh, b: &solidcraft_geom::Mesh, point: solidcraft_geom::Vec3, tol_a: f64, tol_b: f64) -> bool {
if !point.is_finite() || !tol_a.is_finite() || !tol_b.is_finite() || tol_a <= 0. || tol_b <= 0. {
return false;
}
@@ -49,7 +48,7 @@ fn material_probe_matches(a: &solidcraft_geom::Mesh, b: &solidcraft_geom::Mesh,
let offset = 4. * tol_a.max(tol_b);
let mut material = false;
let mut void = false;
for axis in [solidcore_record::Vec3::X, solidcore_record::Vec3::Y, solidcore_record::Vec3::Z] {
for axis in [solidcraft_geom::Vec3::X, solidcraft_geom::Vec3::Y, solidcraft_geom::Vec3::Z] {
for sign in [-1., 1.] {
let q = point + axis * (sign * offset);
if !q.is_finite() {
@@ -768,7 +767,7 @@ pub(super) fn build(
for y in [0.2, 0.5, 0.8] {
for z in [0.2, 0.5, 0.8] {
let d = ma.bbox.max - ma.bbox.min;
let p = ma.bbox.min + solidcore_record::Vec3::new(d.x * x, d.y * y, d.z * z);
let p = ma.bbox.min + solidcraft_geom::Vec3::new(d.x * x, d.y * y, d.z * z);
let roundoff = 64. * f64::EPSILON * (1. + p.len());
material_matches &= material_probe_matches(&mesh_a, &mesh_b, p, tol_a + roundoff, tol_b + roundoff);
}
@@ -1,9 +1,9 @@
#![allow(clippy::unwrap_used, clippy::panic)]
use super::*;
use solidcore_record::Vec3;
use solidcraft_doc::joints::{Coupling, Joint, JointKind, MotionLink, NamedOrigin, Size, Snap, XRef};
use solidcraft_doc::motion::{ContactSet, ExplodedView, MotionStudy, StudyKey};
use solidcraft_doc::{IDENTITY, Occurrence};
use solidcraft_geom::Vec3;
fn at(occurrence: u64) -> JointOrigin {
JointOrigin { occurrence, snap: Snap::Point { point: Vec3::ZERO, z: None, x: None } }
@@ -1,7 +1,6 @@
#![allow(clippy::unwrap_used)]
use crate::Session;
use serde_json::{Value, json};
use solidcraft_geom::prelude::*;
fn run(s: &mut Session, cmd: &str, p: Value) -> Value {
s.execute(cmd, &p).unwrap()
}
@@ -257,7 +256,7 @@ fn configuration_rows_follow_copied_parameters_material_and_strict_face_lineage(
body: "Cube".into(),
origin_feature: source,
name: format!("F{source}:+z"),
point: solidcore_record::Vec3::new(1.0, 1.0, 2.0),
point: solidcraft_geom::Vec3::new(1.0, 1.0, 2.0),
certificate: None,
};
run(&mut s, "config.configure", json!({"params":[length],"materials":["Cube"],"component_appearances":[part],"face_appearances":[target]}));
@@ -295,7 +294,7 @@ fn configured_extrude_cap_copy_keeps_independent_semantic_lineage() {
body: body_name,
origin_feature: feature,
name: format!("F{feature}:top"),
point: solidcore_record::Vec3::new(1.0, 1.0, 2.0),
point: solidcraft_geom::Vec3::new(1.0, 1.0, 2.0),
certificate: None,
};
run(&mut s, "config.configure", json!({"face_appearances":[target]}));
@@ -359,7 +358,7 @@ fn reassigned_sample_plate_paste_new_preserves_complex_frozen_geometry() {
run(&mut s, "occurrence.ground", json!({"occurrence":occurrence,"grounded":true}));
let original_volume = volume(&mut s, part);
let source_mesh = s.model.state().body(&body).unwrap().mesh();
let boss_probe = solidcore_record::Vec3::new(40.0, 35.0, 12.0);
let boss_probe = solidcraft_geom::Vec3::new(40.0, 35.0, 12.0);
let source_boss = source_mesh.contains(boss_probe);
let copied = run(&mut s, "component.paste_new", json!({"component":part,"translate":[10,10,0]}));
assert_eq!(s.doc.components.len(), 2);
@@ -374,7 +373,7 @@ fn reassigned_sample_plate_paste_new_preserves_complex_frozen_geometry() {
for (point, expected) in
[([5.0, 25.0, 4.0], true), ([12.0, 25.0, 4.0], false), ([68.0, 25.0, 4.0], false), ([40.0, 25.0, 12.0], false), ([0.0, 0.0, 4.0], false)]
{
assert_eq!(mesh.contains(solidcore_record::Vec3::new(point[0], point[1], point[2])), expected, "{point:?}");
assert_eq!(mesh.contains(solidcraft_geom::Vec3::new(point[0], point[1], point[2])), expected, "{point:?}");
}
assert_eq!(mesh.contains(boss_probe), source_boss);
assert_eq!(state.body(&body).unwrap().mesh().contains(boss_probe), source_boss);
@@ -474,7 +473,7 @@ fn transferred_frozen_face_paint_ignores_consumed_legacy_offset() {
run(&mut s, "appearance.assign", json!({"body":"Foreign","faces":[[1,1,2]],"color":"#00ff00"}));
s.doc_mut().body_offsets.insert(
"Foreign".into(),
solidcraft_doc::rigid(solidcore_record::Vec3::new(7.0, 0.0, 0.0), solidcore_record::Vec3::ZERO, solidcore_record::Vec3::Z, 0.0),
solidcraft_doc::rigid(solidcraft_geom::Vec3::new(7.0, 0.0, 0.0), solidcraft_geom::Vec3::ZERO, solidcraft_geom::Vec3::Z, 0.0),
);
s.refresh();
let part = component(&mut s, "Part");
@@ -488,7 +487,7 @@ fn transferred_frozen_face_paint_ignores_consumed_legacy_offset() {
#[test]
fn boundary_material_proof_requires_resolvable_stock_and_void() {
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
let a = solidcraft_kernel::box_solid(Vec3::ZERO, Vec3::new(10., 10., 10.)).unwrap().mesh(&solidcraft_kernel::MeshTolerance::chord(1e-3)).unwrap();
let b = solidcraft_kernel::box_solid(Vec3::new(1e-5, 0., 0.), Vec3::new(10.00001, 10., 10.))
.unwrap()
@@ -517,7 +516,7 @@ fn boundary_material_proof_requires_resolvable_stock_and_void() {
#[test]
fn frozen_bore_boundary_copy_preserves_stock_void_and_reopen() {
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
let bore = |center: f64| {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length":30,"width":20,"height":10,"body_name":"Drilled"}));
@@ -553,7 +552,7 @@ fn frozen_bore_boundary_copy_preserves_stock_void_and_reopen() {
#[test]
fn frozen_r6_bore_copy_uses_current_geometry_despite_mesh_measure_bias() {
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length":20,"width":20,"height":10,"body_name":"Drilled"}));
run(&mut s, "solid.cylinder", json!({"base":[10,10,0],"radius":6,"height":10,"operation":"cut"}));
+1 -2
View File
@@ -10,13 +10,12 @@
//! as it did before. Anything that crosses the component's boundary (a feature outside that uses
//! its sketches, bodies moved in or out, a joint to one of its subcomponents) is refused.
use solidcraft_geom::prelude::*;
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use serde_json::{Value, json};
use solidcore_record::Vec3;
use solidcraft_doc::{DesignLink, Document, FeatureKind, ModelBody, ModelState};
use solidcraft_geom::Vec3;
use super::{absolute, component_key, file_sha256, inherit_properties, resolve, session_bodies, stored};
use crate::params::{bad, str_};
@@ -1,7 +1,6 @@
#![allow(clippy::unwrap_used)]
use crate::{ForeignCopy, Session};
use serde_json::{Value, json};
use solidcraft_geom::prelude::*;
use std::sync::Arc;
fn run(s: &mut Session, cmd: &str, p: Value) -> Value {
+1 -2
View File
@@ -8,7 +8,6 @@
//! the design gives them (`Feature::link_faces`), so a joint on a linked face stays on that face
//! when the design changes, as it would inside the design, and reports it when the face is gone.
use solidcraft_geom::prelude::*;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
@@ -497,7 +496,7 @@ fn insert_design(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "component.insert_design";
let given = str_(p, "path").filter(|x| !x.trim().is_empty() && x.len() < 4096).ok_or_else(|| bad(cmd, "`path` is required"))?;
let translate = match p.get("translate") {
None => solidcore_record::Vec3::ZERO,
None => solidcraft_geom::Vec3::ZERO,
Some(v) => vec3(v).filter(|t| t.is_finite() && t.len() < 1e9).ok_or_else(|| bad(cmd, "`translate` must be [x, y, z] in mm"))?,
};
// A network or device path is judged from its text, as given, before anything reads it.
@@ -1,12 +1,10 @@
//! Physical properties of an assembly placement, with original body material assignments.
use solidcraft_geom::prelude::*;
use std::collections::{BTreeMap, BTreeSet};
use serde_json::{Value, json};
use solidcore_record::Vec3;
use solidcraft_doc::{Document, Mat};
use solidcraft_geom::Aabb3;
use solidcraft_geom::inertia::{self, Tensor3};
use solidcraft_geom::{Aabb3, Vec3};
use crate::params::bad;
use crate::{Result, Session};
@@ -1,6 +1,5 @@
use crate::Session;
use serde_json::{Value, json};
use solidcraft_geom::prelude::*;
fn run(s: &mut Session, cmd: &str, params: Value) -> Value {
s.execute(cmd, &params).unwrap()
@@ -457,7 +456,7 @@ fn repeated_placements_reuse_one_measurement_of_their_body() {
/// the meshes it is taken from) also when the placement turns it off the axes.
#[test]
fn a_placed_body_measures_as_its_placed_copy() {
use solidcore_record::Vec3;
use solidcraft_geom::Vec3;
let mut s = Session::default();
run(&mut s, "solid.cylinder", json!({"diameter": 4, "height": 30}));
run(&mut s, "solid.box", json!({"length": 10, "width": 20, "height": 5}));

Some files were not shown because too many files have changed in this diff Show More