fix: spatial index, decimation overflow, input validation, accessibility

Round 2 of performance and correctness improvements:

- Spatial grid index for brush painting: forEachTriInSphere now queries
  only nearby grid cells instead of scanning all triangles. ~5.7x faster
  for brush operations on 68k+ tri meshes.

- Decimation overflow fix: hasLinkViolation used a fixed 0x200000
  multiplier for vertex-pair keys, overflowing at >2M vertices.
  Now uses dynamic multiplier based on actual vertex count.

- Decimation determinant threshold: solveQ used absolute 1e-10 which
  fails for large coordinates. Now relative to matrix element magnitude.

- 3MF triangle index validation: bounds-check all parsed indices against
  vertex count, throw clear error on corrupt files instead of silent NaN.

- File size limit: reject files >500 MB before loading into memory,
  prevents browser tab crash on oversized files.

- Accessibility: preset swatches now keyboard-navigable (role=button,
  tabindex=0, Enter/Space to select). Modal dialogs trap focus and
  close on Escape.

- Ctrl+click straight line tool: click to set start point, Ctrl+click
  to paint a straight line between points. Ctrl+hover shows preview.

- Precision masking available for radius brush mode.

- Spatial grid rebuilt when entering/leaving precision mode.
This commit is contained in:
Avatarsia
2026-04-06 05:14:23 +02:00
parent 4811b55d5c
commit 689c192a89
5 changed files with 270 additions and 46 deletions
+4 -2
View File
@@ -81,7 +81,7 @@ export const TRANSLATIONS = {
'excl.toolBrushTitle': 'Brush: paint triangles to exclude',
'excl.toolFill': 'Fill',
'excl.toolFillTitle': 'Bucket fill: flood-fill surface up to a threshold angle',
'excl.shiftHint': 'Hold Shift to erase',
'excl.shiftHint': 'Shift: erase \u00b7 Ctrl+click: straight line',
'labels.type': 'Type',
'brushType.single': 'Single',
'brushType.circle': 'Circle',
@@ -185,6 +185,7 @@ export const TRANSLATIONS = {
// Alerts
'alerts.loadFailed': 'Could not load model: {msg}',
'alerts.exportFailed': 'Export failed: {msg}',
'alerts.fileTooLarge': 'File too large ({size} MB). Maximum: {max} MB.',
},
de: {
@@ -266,7 +267,7 @@ export const TRANSLATIONS = {
'excl.toolBrushTitle': 'Pinsel: Dreiecke zum Ausschlie\u00dfen einf\u00e4rben',
'excl.toolFill': 'F\u00fcllen',
'excl.toolFillTitle': 'F\u00fcllen: Fl\u00e4che bis zu einem Winkel fluten',
'excl.shiftHint': 'Shift gedr\u00fcckt halten zum Radieren',
'excl.shiftHint': 'Shift: Radieren \u00b7 Strg+Klick: Gerade Linie',
'labels.type': 'Typ',
'brushType.single': 'Einzeln',
'brushType.circle': 'Kreis',
@@ -370,6 +371,7 @@ export const TRANSLATIONS = {
// Alerts
'alerts.loadFailed': 'Modell konnte nicht geladen werden: {msg}',
'alerts.exportFailed': 'Export fehlgeschlagen: {msg}',
'alerts.fileTooLarge': 'Datei zu gross ({size} MB). Maximum: {max} MB.',
},
};