Retire legacy reference data and the external command catalog (#302)
- Remove xtask/data/fusion-catalog.tsv and xtask/data/fusion-ids.tsv. - Remove the legacy command-id table (crates/engine/src/legacy_ids.rs) and its use in find_command and in the shortcut, recents and pinned-command loaders. Only SolidCraft's dotted ids name commands; any other id is an unknown command. - Boundary Fill's id becomes solid.boundary_fill. - Replace the parity report with SolidCraft's own feature checklist: `cargo xtask features` writes docs/features.md from the command registry. - docs/oracle.md, docs/step-import.md (corpus section) and docs/iges-import.md: reference results are being rebuilt on analytic and open-source (FreeCAD) values; see #292. `cargo xtask oracle` and `cargo xtask step-corpus [--iges]` write these placeholders. - AGENTS.md, CLAUDE.md, CONTRIBUTING.md, README.md, ROADMAP.md and the book describe SolidCraft as independent, with reference tests on analytic values and open-source tools, and agents do not drive or extract data from commercial CAD products. Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c7f0139ad4
commit
3b8028a040
32 files changed
+695
-2324
No files matched your search
@@ -11,7 +11,7 @@ use crate::params::{bad, bool_, expr, req_expr, str_, string_list, vec3};
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[
|
||||
CommandSpec::new("SurfaceSculpt", "Boundary Fill", boundary_fill).at("SOLID", "CREATE").icon("boundary_fill").params(
|
||||
CommandSpec::new("solid.boundary_fill", "Boundary Fill", boundary_fill).at("SOLID", "CREATE").icon("boundary_fill").params(
|
||||
"tools: [bodies]; cells: [[x,y,z] a point inside each cell to fill (a cell: inside some tools, outside the rest)]; operation?: new|join|cut|intersect; remove_tools?: bool",
|
||||
),
|
||||
CommandSpec::new("solid.coil", "Coil", coil).at("SOLID", "CREATE").icon("coil").params(
|
||||
@@ -422,7 +422,7 @@ pub(super) fn snap_point(s: &Session, kind: &str, at: Vec3) -> Option<Vec3> {
|
||||
}
|
||||
|
||||
fn boundary_fill(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "SurfaceSculpt";
|
||||
let cmd = "solid.boundary_fill";
|
||||
let tools = string_list(p, "tools");
|
||||
let st = s.model.state();
|
||||
if tools.is_empty() || tools.len() > 64 || tools.iter().any(|t| st.body(t).is_none()) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The command registry. Ids are SolveCraft's own dotted ids (`solid.extrude`,
|
||||
//! `sketch.rectangle.two_point`); the toolbar placement (`tab`, `panel`) follows the familiar
|
||||
//! Design workspace layout. Ids used before the rename still resolve (`legacy_ids`).
|
||||
//! Design workspace layout.
|
||||
|
||||
/// The params-doc note of every command that adds sketch geometry (see `sketch::created`).
|
||||
macro_rules! created_doc {
|
||||
@@ -180,7 +180,5 @@ pub fn command_specs() -> Vec<&'static CommandSpec> {
|
||||
|
||||
pub fn find_command(id: &str) -> Option<&'static CommandSpec> {
|
||||
let all = command_specs();
|
||||
// Ids from before the rename still find their command.
|
||||
let id = crate::legacy_ids::current_id(id);
|
||||
all.iter().find(|c| c.id == id).or_else(|| all.iter().find(|c| c.id.eq_ignore_ascii_case(id))).copied()
|
||||
}
|
||||
@@ -184,7 +184,7 @@ fn base(r: &mut Rng) -> Step {
|
||||
("sketch.line", json!({"points": pts})),
|
||||
("sketch.arc.three_point", json!({"start": [0, h], "end": [0, 0], "through": [-bulge, h / 2.0]})),
|
||||
("sketch.finish", json!({})),
|
||||
("Extrude", json!({"distance": d})),
|
||||
("solid.extrude", json!({"distance": d})),
|
||||
],
|
||||
)
|
||||
}
|
||||
@@ -198,7 +198,7 @@ fn base(r: &mut Rng) -> Step {
|
||||
("sketch.create", json!({"plane": "XZ"})),
|
||||
("sketch.rectangle.two_point", json!({"p0": [r0, 0], "p1": [r1, h]})),
|
||||
("sketch.finish", json!({})),
|
||||
("Revolve", json!({"axis": "Z", "angle": angle})),
|
||||
("solid.revolve", json!({"axis": "Z", "angle": angle})),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
//! Legacy migration: the command ids SolveCraft used before 2026-10-09, when many commands
|
||||
//! carried the id of the matching command in another CAD program, and the dotted ids that
|
||||
//! replaced them. This is the one place the old ids live. Old scripts, recipes, control-channel
|
||||
//! calls, saved shortcuts and toolbar layouts keep working: `find_command` and the preference
|
||||
//! loaders map an old id through `current_id`. `solvecraft-tools/rename_ids.sh` converts a
|
||||
//! worktree with this same table.
|
||||
|
||||
/// (old id, current id).
|
||||
pub const LEGACY_IDS: &[(&str, &str)] = &[
|
||||
("AsBuiltPositionsCmd", "component.revert_position"),
|
||||
("FusionCreateComponentsFromBodiesCommand", "component.from_bodies"),
|
||||
("FusionCreateNewComponentCommand", "component.create"),
|
||||
("SnapshotCmd", "component.capture_position"),
|
||||
("FusionShowDesignConfigPanelCmd", "config.table"),
|
||||
("FusionStartDesignConfigModeCmd", "config.configure"),
|
||||
("FusionComputeAllCommand", "timeline.compute_all"),
|
||||
("FusionDeleteCommand", "timeline.delete"),
|
||||
("FusionRenameTimelineEntryCommand", "timeline.rename"),
|
||||
("RedoCommand", "edit.redo"),
|
||||
("UndoCommand", "edit.undo"),
|
||||
("timeline.rollTo", "timeline.roll_to"),
|
||||
("AlignCmd", "solid.align"),
|
||||
("EmbossCmd", "solid.emboss"),
|
||||
("FusionReplaceFaceCommand", "solid.replace_face"),
|
||||
("FusionRibCommand", "solid.rib"),
|
||||
("FusionWebCommand", "solid.web"),
|
||||
("PrimitiveCoil", "solid.coil"),
|
||||
("SoftDeleteCommand", "solid.remove"),
|
||||
("ConstructionPlaneAtAngleCommand", "construct.plane.at_angle"),
|
||||
("ConstructionPlaneOffsetFromPlaneCommand", "construct.plane.offset"),
|
||||
("Extrude", "solid.extrude"),
|
||||
("FusionChamferCommand", "solid.chamfer"),
|
||||
("FusionCombineCommand", "solid.combine"),
|
||||
("FusionDraftCommand", "solid.draft"),
|
||||
("FusionFilletEdgesCommand", "solid.fillet"),
|
||||
("FusionHoleCommand", "solid.hole"),
|
||||
("FusionMoveCommand", "solid.move"),
|
||||
("FusionOffsetFacesCommand", "solid.offset_face"),
|
||||
("FusionPressPullCommand", "solid.press_pull"),
|
||||
("FusionShellBodyCommand", "solid.shell"),
|
||||
("FusionSplitBodyCommand", "solid.split_body"),
|
||||
("FusionThreadCommand", "solid.thread"),
|
||||
("MirrorCommand", "solid.mirror"),
|
||||
("ModifyScale", "solid.scale"),
|
||||
("PatternCircular", "solid.pattern.circular"),
|
||||
("PatternOnPath", "solid.pattern.path"),
|
||||
("PatternRectangular", "solid.pattern.rectangular"),
|
||||
("PrimitiveBox", "solid.box"),
|
||||
("PrimitiveCylinder", "solid.cylinder"),
|
||||
("PrimitivePipe", "solid.pipe"),
|
||||
("PrimitiveSphere", "solid.sphere"),
|
||||
("PrimitiveTorus", "solid.torus"),
|
||||
("Revolve", "solid.revolve"),
|
||||
("SolidLoft", "solid.loft"),
|
||||
("StockModelCommand", "solid.bounding_solid"),
|
||||
("Sweep", "solid.sweep"),
|
||||
("ExportCommand", "file.export"),
|
||||
("FusionImportCommandFromToolbar", "file.insert_step"),
|
||||
("FusionSaveAsSTLCommand", "file.save_mesh"),
|
||||
("NewDocumentCommand", "file.new"),
|
||||
("ParaMeshInsertAlignCommand", "file.insert_mesh"),
|
||||
("SaveDocumentAsCommand", "file.save_as"),
|
||||
("SaveDocumentCommand", "file.save"),
|
||||
("AppearanceCommand", "appearance.assign"),
|
||||
("MeasureCommand", "inspect.measure"),
|
||||
("PhysicalMaterialCommand", "material.assign"),
|
||||
("FusionMotionRelationshipCommand", "joint.motion_link"),
|
||||
("FusionMoveJointsCommand", "joint.drive"),
|
||||
("InterferenceCheckCommand", "inspect.interference"),
|
||||
("JointAsBuiltCmd", "joint.as_built"),
|
||||
("JointAssembleCmdNew", "joint.create"),
|
||||
("JointOrigin", "joint.origin"),
|
||||
("RigidGroupCmd", "joint.rigid_group"),
|
||||
("ContactSetCmd", "contact.create"),
|
||||
("DisableAllContactCmd", "contact.disable_all"),
|
||||
("EnableAllContactCmd", "contact.enable_all"),
|
||||
("EnableContactSetsCmd", "contact.enable_sets"),
|
||||
("FusionMotionStudyCommand", "motion.study"),
|
||||
("ChangeParameterCommand", "parameters.change"),
|
||||
("FusionFastenersCommand", "parts.fastener"),
|
||||
("FusionAssignPlasticRuleCommand", "plastic.assign_rule"),
|
||||
("FusionBossCommand", "plastic.boss"),
|
||||
("FusionLipCommand", "plastic.lip"),
|
||||
("FusionManagePlasticRuleCommand", "plastic.manage_rules"),
|
||||
("FusionRestCommand", "plastic.rest"),
|
||||
("FusionSnapFitCommand", "plastic.snap_fit"),
|
||||
("FusionHalfSectionViewCommand", "inspect.section"),
|
||||
("ConvertToSheetMetalCmd", "sheet.convert"),
|
||||
("FusionSheetMetalFlangeCommand", "sheet.flange"),
|
||||
("FusionSheetMetalFlatPatternCmd", "sheet.flat_pattern"),
|
||||
("FusionSheetMetalHemFlangeCommand", "sheet.hem"),
|
||||
("FusionSheetMetalRulesCommand", "sheet.manage_rules"),
|
||||
("FusionSheetmetalUnfoldCommand", "sheet.unfold"),
|
||||
("SheetMetalFoldCmd", "sheet.fold"),
|
||||
("FusionAddCanvasCommand", "canvas.insert"),
|
||||
("FusionAddEditDecalCommand", "canvas.decal"),
|
||||
("ConstraintSmooth", "sketch.constraint.curvature"),
|
||||
("SketchAutoConstrainAndFinish", "sketch.finish_auto_constrain"),
|
||||
("SketchAutoConstraintAndDimCmd", "sketch.auto_constrain"),
|
||||
("SketchAutoConstraintAndDimFromDatumCmd", "sketch.auto_constrain_from_datum"),
|
||||
("SketchConstrainer", "sketch.constrainer"),
|
||||
("SketchPolygonConstraintCmd", "sketch.constraint.polygon"),
|
||||
("ArcTangent", "sketch.arc.tangent"),
|
||||
("BlendG1CurveSketchCmd", "sketch.blend_curve"),
|
||||
("CircleElipse", "sketch.ellipse"),
|
||||
("CircleTanTanRadius", "sketch.circle.two_tangent"),
|
||||
("CircleThreeTangent", "sketch.circle.three_tangent"),
|
||||
("ConicCurveCmd", "sketch.conic"),
|
||||
("DrawCVMSpline3D", "sketch.spline.control_point"),
|
||||
("DrawCVMSpline5D", "sketch.spline.control_point_5"),
|
||||
("DrawSpline", "sketch.spline.fit_point"),
|
||||
("MTextCmd", "sketch.text"),
|
||||
("ShapeArcSlotCenterTwoPoint", "sketch.slot.arc_center"),
|
||||
("ShapeArcSlotThreePoint", "sketch.slot.arc_three_point"),
|
||||
("ShapeSlotCenterPoint", "sketch.slot.center_point"),
|
||||
("SketchMidpointLine", "sketch.line.midpoint"),
|
||||
("ImportDxfFileCommand", "sketch.insert_dxf"),
|
||||
("SketchImportSVG", "sketch.insert_svg"),
|
||||
("FusionCenterOfMassCommand", "inspect.center_of_mass"),
|
||||
("FusionCurvatureCombAnalysisCommand", "inspect.curvature_comb"),
|
||||
("FusionIsoCurveAnalysisCommand", "inspect.isocurve"),
|
||||
("FusionMinimumRadiusAnalysisCommand", "inspect.minimum_radius"),
|
||||
("BreakSketchCmd", "sketch.break"),
|
||||
("ChamferSketchDistanceAngle", "sketch.chamfer.distance_angle"),
|
||||
("ChamferSketchDistanceDistance", "sketch.chamfer.two_distance"),
|
||||
("ChamferSketchEqualDistance", "sketch.chamfer.equal_distance"),
|
||||
("CircularSketchPatternCommand", "sketch.pattern.circular"),
|
||||
("ExtendSketchCmd", "sketch.extend"),
|
||||
("FilletSketchCmd", "sketch.fillet"),
|
||||
("MirrorSketchCommand", "sketch.mirror"),
|
||||
("Offset", "sketch.offset"),
|
||||
("RectangularSketchPatternCommand", "sketch.pattern.rectangular"),
|
||||
("SketchScaleCmd", "sketch.scale"),
|
||||
("TrimSketchCmd", "sketch.trim"),
|
||||
("FitCurvesToSectionCommand", "sketch.fit_curves_to_section"),
|
||||
("Include3DGeometry", "sketch.include_3d"),
|
||||
("IntersectCmd", "sketch.intersect"),
|
||||
("IntersectionCurve", "sketch.intersection_curve"),
|
||||
("ProjectNewCmd", "sketch.project"),
|
||||
("ProjectToSurface", "sketch.project_to_surface"),
|
||||
("SketchIsoparametricCurve", "sketch.isoparametric_curve"),
|
||||
("SpunProfileCmd", "sketch.spun_profile"),
|
||||
("ArcCenterTwoPoint", "sketch.arc.center_point"),
|
||||
("ArcThreePoint", "sketch.arc.three_point"),
|
||||
("CircleCenterRadius", "sketch.circle.center"),
|
||||
("CircleThreePoint", "sketch.circle.three_point"),
|
||||
("CircleTwoPoint", "sketch.circle.two_point"),
|
||||
("ConstraintCoincident", "sketch.constraint.coincident"),
|
||||
("ConstraintCollinear", "sketch.constraint.collinear"),
|
||||
("ConstraintConcentric", "sketch.constraint.concentric"),
|
||||
("ConstraintEqual", "sketch.constraint.equal"),
|
||||
("ConstraintFix", "sketch.constraint.fix"),
|
||||
("ConstraintHorizontalVertical", "sketch.constraint.horizontal_vertical"),
|
||||
("ConstraintMidPoint", "sketch.constraint.midpoint"),
|
||||
("ConstraintParallel", "sketch.constraint.parallel"),
|
||||
("ConstraintPerpendicular", "sketch.constraint.perpendicular"),
|
||||
("ConstraintSymmetry", "sketch.constraint.symmetry"),
|
||||
("ConstraintTangent", "sketch.constraint.tangent"),
|
||||
("DrawPoint", "sketch.point"),
|
||||
("DrawPolyline", "sketch.line"),
|
||||
("ShapePolygonCircumscribed", "sketch.polygon.circumscribed"),
|
||||
("ShapePolygonEdge", "sketch.polygon.edge"),
|
||||
("ShapePolygonInscribed", "sketch.polygon.inscribed"),
|
||||
("ShapeRectangleCenter", "sketch.rectangle.center"),
|
||||
("ShapeRectangleThreePoint", "sketch.rectangle.three_point"),
|
||||
("ShapeRectangleTwoPoint", "sketch.rectangle.two_point"),
|
||||
("ShapeSlotCenterToCenter", "sketch.slot.center_to_center"),
|
||||
("ShapeSlotOverall", "sketch.slot.overall"),
|
||||
("SketchActivate", "sketch.edit"),
|
||||
("SketchCreate", "sketch.create"),
|
||||
("SketchDimension", "sketch.dimension"),
|
||||
("SketchStop", "sketch.finish"),
|
||||
("FusionAccessibilityAnalysisCommand", "inspect.accessibility"),
|
||||
("FusionCurvatureMapAnalysisCommand", "inspect.curvature_map"),
|
||||
("FusionDraftAnalysisCommand", "inspect.draft"),
|
||||
("FusionEnvironmentMapAnalysisCommand", "inspect.environment_map"),
|
||||
("FusionZebraAnalysisCommand", "inspect.zebra"),
|
||||
("FusionExtendCommand", "surface.extend"),
|
||||
("FusionSurfacePatchCommand", "surface.patch"),
|
||||
("FusionSurfaceStitchCommand", "surface.stitch"),
|
||||
("FusionSurfaceThickenCommand", "surface.thicken"),
|
||||
("FusionSurfaceTrimCommand", "surface.trim"),
|
||||
("FusionSplitFaceCommand", "solid.split_face"),
|
||||
("ConstructionAxisThroughCylinderCommand", "construct.axis.through_cylinder"),
|
||||
("ConstructionAxisNormalToFaceCommand", "construct.axis.normal_to_face"),
|
||||
("ConstructionAxisFromTwoPlanesCommand", "construct.axis.two_planes"),
|
||||
("ConstructionAxisThroughTwoPointsCommand", "construct.axis.two_points"),
|
||||
("ConstructionAxisThroughEdgeCommand", "construct.axis.edge"),
|
||||
("ConstructionPointFromPointCommand", "construct.point.vertex"),
|
||||
("ConstructionPointThroughTwoEdgesCommand", "construct.point.two_edges"),
|
||||
("ConstructionPointFromThreePlanesCommand", "construct.point.three_planes"),
|
||||
("ConstructionPointFromCircleOrSphereCommand", "construct.point.center"),
|
||||
("ConstructionPointAtEdgeAndPlaneCommand", "construct.point.edge_plane"),
|
||||
("ConstructionPointAlongPathCommand", "construct.point.along_path"),
|
||||
("ConstructionMidPlaneCommand", "construct.plane.midplane"),
|
||||
("ConstructionPlaneFromTwoEdgesCommand", "construct.plane.two_edges"),
|
||||
("ConstructionPlaneFromThreePointsCommand", "construct.plane.three_points"),
|
||||
("ConstructionTangentPlaneCommand", "construct.plane.tangent"),
|
||||
("ConstructionPlaneAlongPathCommand", "construct.plane.along_path"),
|
||||
];
|
||||
|
||||
/// The current id for a command id that may be a legacy one (unchanged otherwise).
|
||||
pub fn current_id(id: &str) -> &str {
|
||||
LEGACY_IDS.iter().find(|(old, _)| old.eq_ignore_ascii_case(id)).map(|(_, new)| *new).unwrap_or(id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_legacy_id_names_a_command() {
|
||||
let unknown: Vec<&str> =
|
||||
LEGACY_IDS.iter().filter(|(_, new)| crate::find_command(new).is_none_or(|c| c.id != *new)).map(|(_, new)| *new).collect();
|
||||
// The construction set and Split Face are on a branch not yet merged when the table was made.
|
||||
let pending = ["solid.split_face"];
|
||||
let unknown: Vec<&str> = unknown
|
||||
.into_iter()
|
||||
.filter(|n| {
|
||||
!pending.contains(n)
|
||||
&& !n.starts_with("construct.axis")
|
||||
&& !n.starts_with("construct.point")
|
||||
&& ![
|
||||
"construct.plane.midplane",
|
||||
"construct.plane.two_edges",
|
||||
"construct.plane.three_points",
|
||||
"construct.plane.tangent",
|
||||
"construct.plane.along_path",
|
||||
]
|
||||
.contains(n)
|
||||
})
|
||||
.collect();
|
||||
assert!(unknown.is_empty(), "{unknown:?}");
|
||||
assert_eq!(crate::find_command("SketchCreate").map(|c| c.id), Some("sketch.create"));
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
pub mod cmd;
|
||||
pub mod frames;
|
||||
pub mod fuzz;
|
||||
pub mod legacy_ids;
|
||||
pub mod licences;
|
||||
pub mod occurrences;
|
||||
pub mod params;
|
||||
|
||||
@@ -512,11 +512,22 @@ fn registry_is_consistent() {
|
||||
let n = ids.len();
|
||||
ids.dedup();
|
||||
assert_eq!(ids.len(), n, "duplicate command ids");
|
||||
assert!(find_command("extrude").is_some(), "case-insensitive lookup");
|
||||
assert_eq!(find_command("Solid.Extrude").map(|c| c.id), Some("solid.extrude"), "case-insensitive lookup");
|
||||
let s = Session::default();
|
||||
assert!(!find_command("sketch.line").unwrap().info(&s).enabled);
|
||||
}
|
||||
|
||||
/// Only SolidCraft's own dotted ids name commands; other ids are unknown commands.
|
||||
#[test]
|
||||
fn only_own_ids_name_commands() {
|
||||
for old in ["Extrude", "SketchCreate", "FusionFilletEdgesCommand"] {
|
||||
assert!(find_command(old).is_none(), "{old}");
|
||||
let mut s = Session::default();
|
||||
let err = s.execute(old, &serde_json::json!({})).unwrap_err();
|
||||
assert!(matches!(err, crate::EngineError::UnknownCommand(_)), "{old}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_design_builds() {
|
||||
let mut s = Session::default();
|
||||
@@ -2336,15 +2347,15 @@ fn boundary_fill_cells() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 20, "width": 20, "height": 20, "body_name": "A"}));
|
||||
run(&mut s, "solid.box", json!({"corner": [10, 0, 0], "length": 20, "width": 20, "height": 20, "body_name": "B"}));
|
||||
let r = run(&mut s, "SurfaceSculpt", json!({"tools": ["A", "B"], "cells": [[15, 10, 10]], "remove_tools": true}));
|
||||
let r = run(&mut s, "solid.boundary_fill", json!({"tools": ["A", "B"], "cells": [[15, 10, 10]], "remove_tools": true}));
|
||||
let m = run(&mut s, "inspect.measure", json!({}));
|
||||
assert_eq!(m["body_count"], 1, "{r} {m}");
|
||||
assert!(rel(m["total"]["volume_mm3"].as_f64().unwrap_or(0.0), 10.0 * 20.0 * 20.0) < 1e-9);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
run(&mut s, "SurfaceSculpt", json!({"tools": ["A", "B"], "cells": [[5, 10, 10], [25, 10, 10]], "remove_tools": true}));
|
||||
run(&mut s, "solid.boundary_fill", json!({"tools": ["A", "B"], "cells": [[5, 10, 10], [25, 10, 10]], "remove_tools": true}));
|
||||
let m = run(&mut s, "inspect.measure", json!({}));
|
||||
assert!(rel(m["total"]["volume_mm3"].as_f64().unwrap_or(0.0), 2.0 * 10.0 * 20.0 * 20.0) < 1e-9, "{m}");
|
||||
assert!(s.execute("SurfaceSculpt", &json!({"tools": ["A"], "cells": [[100, 100, 100]]})).is_err());
|
||||
assert!(s.execute("solid.boundary_fill", &json!({"tools": ["A"], "cells": [[100, 100, 100]]})).is_err());
|
||||
}
|
||||
|
||||
/// Rectangles name their sides and corners, so they can be dimensioned without inspecting the
|
||||
|
||||
Reference in new issue
Block a user