feat(draworder): HATCHTOBACK command and interactive DRAWORDER workflows

Implement the AutoCAD compatible HATCHTOBACK (HB) command and enhance the interactive DRAWORDER command pipeline with comprehensive verb selection and viewport entity picking.

Key implementations and architectural details:

1. HATCHTOBACK command execution
   * Isolates entities belonging to the active layout block record or global space.
   * Filters out entities residing on locked layers using an efficient hash set lookup.
   * Records targeted delta undo operations on SortEntitiesTable objects (~100 bytes) rather than snapshotting full document structures.
   * Performs granular draw depth cache invalidation via Scene::invalidate_draw_depth, preserving existing geometry tessellations and spatial indices.
   * Emits localized user notifications when no qualifying hatch entities are found.

2. Interactive DrawOrderCommand state machine
   * Supports preselected entities as well as interactive on demand selection gathering.
   * Implements subverb selection supporting single character shortcuts (A for Above, U for Under, F for Front, B for Back) and case insensitive keyword parsing.
   * Enables interactive reference object picking directly in the viewport via needs_entity_pick and on_entity_pick callbacks.
   * Supports hexadecimal handle specification directly from the command line interface.

3. Ecosystem and system integration
   * Bumps DEFAULT_ALIASES_VERSION from 2 to 3 to register the HB alias while preserving existing user overrides.
   * Integrates HATCHTOBACK and HB into dynamic autocomplete registries.
   * Adds localized message strings across all 20 Fluent translation catalogs.
   * Corrects hatch boundary serialization flags to ensure outermost boundary rings consistently carry the external attribute.
This commit is contained in:
Karim Jerbi committed 2026-08-23 18:56:01 +01:00
1 parent 44a40eda0b
commit 236b7756f8
30 files changed
+980 -62

No files matched your search

+20 -6
View File
@@ -73,8 +73,9 @@ const WEB_ALIAS_KEY: &str = "opencadstudio.aliases";
/// Version of the shipped default alias table. Bump this whenever the embedded
/// defaults add aliases or change a default target, so existing profiles can be
/// migrated forward (see `introduced_at` / `migrate_aliases`). Version 1 shipped
/// the pre-REDRAW table; version 2 introduced the REDRAW-family aliases.
const DEFAULT_ALIASES_VERSION: u32 = 2;
/// the pre-REDRAW table; version 2 introduced the REDRAW-family aliases; version 3
/// introduced HB (HATCHTOBACK).
const DEFAULT_ALIASES_VERSION: u32 = 3;
#[cfg(target_arch = "wasm32")]
const WEB_ALIAS_VERSION_KEY: &str = "opencadstudio.aliases.version";
@@ -91,6 +92,7 @@ fn introduced_at(version: u32) -> &'static [(&'static str, &'static str)] {
("RE", "REGEN"),
("REA", "REGENALL"),
],
3 => &[("HB", "HATCHTOBACK")],
_ => &[],
}
}
@@ -330,17 +332,18 @@ mod tests {
.collect()
}
/// An existing profile that predates the REDRAW aliases gains exactly the
/// An existing profile that predates the REDRAW and HB aliases gains exactly the
/// new defaults, with every pre-existing mapping kept.
#[test]
fn migration_adds_new_defaults_preserving_existing() {
let mut stored = map(&[("L", "LINE"), ("Z", "ZOOM"), ("HI", "HIDE")]);
let changed = migrate_aliases(&mut stored, 0);
assert!(changed, "migrating from v0 to v2 must add defaults");
assert!(changed, "migrating from v0 to v3 must add defaults");
assert_eq!(stored.get("R").map(String::as_str), Some("REDRAW"));
assert_eq!(stored.get("RA").map(String::as_str), Some("REDRAWALL"));
assert_eq!(stored.get("RE").map(String::as_str), Some("REGEN"));
assert_eq!(stored.get("REA").map(String::as_str), Some("REGENALL"));
assert_eq!(stored.get("HB").map(String::as_str), Some("HATCHTOBACK"));
// Existing entries must survive untouched.
assert_eq!(stored.get("L").map(String::as_str), Some("LINE"));
assert_eq!(stored.get("Z").map(String::as_str), Some("ZOOM"));
@@ -375,10 +378,10 @@ mod tests {
assert_eq!(stored.get("L").map(String::as_str), Some("LINE"));
}
/// Migrating a mid-range profile (one that saw v1) only pulls in v2's
/// Migrating a mid-range profile (one that saw v1) pulls in v2's and v3's
/// additions, not hypothetical earlier ones.
#[test]
fn migration_from_version_one_adds_v2_aliases() {
fn migration_from_version_one_adds_v2_and_v3_aliases() {
let mut stored = map(&[("L", "LINE")]);
let changed = migrate_aliases(&mut stored, 1);
assert!(changed);
@@ -386,5 +389,16 @@ mod tests {
assert_eq!(stored.get("RA").map(String::as_str), Some("REDRAWALL"));
assert_eq!(stored.get("RE").map(String::as_str), Some("REGEN"));
assert_eq!(stored.get("REA").map(String::as_str), Some("REGENALL"));
assert_eq!(stored.get("HB").map(String::as_str), Some("HATCHTOBACK"));
}
/// Migrating a profile that saw v2 pulls in v3's HB addition only.
#[test]
fn migration_from_version_two_adds_v3_aliases() {
let mut stored = map(&[("L", "LINE")]);
let changed = migrate_aliases(&mut stored, 2);
assert!(changed);
assert_eq!(stored.get("R"), None);
assert_eq!(stored.get("HB").map(String::as_str), Some("HATCHTOBACK"));
}
}