Support bulk layout review selection edits
This commit is contained in:
parent
1e55035f09
commit
e4d129cc06
|
|
@ -1933,6 +1933,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
<button type="button" class="layout-tool-btn" id="layout-select-all">Select All</button>
|
||||
<button type="button" class="layout-tool-btn" id="layout-multi-select">Multi</button>
|
||||
<button type="button" class="layout-tool-btn" id="layout-clear-selection">Clear</button>
|
||||
<button type="button" class="layout-tool-btn" id="layout-invert-selection">Invert</button>
|
||||
<button type="button" class="layout-tool-btn" id="layout-select-line">Line</button>
|
||||
<button type="button" class="layout-tool-btn" id="layout-tool-pan">Pan</button>
|
||||
<button type="button" class="layout-tool-btn" id="layout-tool-add">Add</button>
|
||||
<button type="button" class="layout-tool-btn danger" id="layout-delete-word">Delete</button>
|
||||
|
|
@ -2513,6 +2515,16 @@ const HANDLE_SIZE_PX = 14;
|
|||
let selectedId = null;
|
||||
let selectedIds = new Set();
|
||||
let multiSelectMode = false;
|
||||
|
||||
function getSelectedWords() {
|
||||
if (selectedIds && selectedIds.size) return words.filter(w => selectedIds.has(String(w.id)));
|
||||
const w = getSelectedWord();
|
||||
return w ? [w] : [];
|
||||
}
|
||||
|
||||
let touchMultiAnchor = false;
|
||||
let touchMultiTimer = null;
|
||||
const activePointers = new Set();
|
||||
let tool = "select";
|
||||
let zoom = 1;
|
||||
let dragState = null;
|
||||
|
|
@ -3205,8 +3217,9 @@ function refreshSelectionUI(opts = {}) {
|
|||
}
|
||||
|
||||
function applyEditorValues(push = true) {
|
||||
const selectedWords = getSelectedWords();
|
||||
const w = getSelectedWord();
|
||||
if (!w) return null;
|
||||
if (!selectedWords.length || !w) return null;
|
||||
if (push) pushHistory();
|
||||
|
||||
const readValue = (el, fallback) => {
|
||||
|
|
@ -3231,23 +3244,25 @@ function refreshSelectionUI(opts = {}) {
|
|||
const nextColor = readValue(colorSource, w.text_color_guess || "#000000");
|
||||
|
||||
w.text = nextText;
|
||||
w.font_family_guess = nextFamily;
|
||||
w.font_size_guess = nextSize;
|
||||
w.text_color_guess = nextColor;
|
||||
for (const item of selectedWords) {
|
||||
item.font_family_guess = nextFamily;
|
||||
item.font_size_guess = nextSize;
|
||||
item.text_color_guess = nextColor;
|
||||
|
||||
w.override_style = w.override_style && typeof w.override_style === "object" ? w.override_style : {};
|
||||
w.override_style.font_family = nextFamily;
|
||||
w.override_style.font_size = nextSize;
|
||||
w.override_style.text_color = nextColor;
|
||||
item.override_style = item.override_style && typeof item.override_style === "object" ? item.override_style : {};
|
||||
item.override_style.font_family = nextFamily;
|
||||
item.override_style.font_size = nextSize;
|
||||
item.override_style.text_color = nextColor;
|
||||
|
||||
w.resolved_style = w.resolved_style && typeof w.resolved_style === "object" ? w.resolved_style : {};
|
||||
w.resolved_style.font_family = nextFamily;
|
||||
w.resolved_style.font_size = nextSize;
|
||||
w.resolved_style.text_color = nextColor;
|
||||
item.resolved_style = item.resolved_style && typeof item.resolved_style === "object" ? item.resolved_style : {};
|
||||
item.resolved_style.font_family = nextFamily;
|
||||
item.resolved_style.font_size = nextSize;
|
||||
item.resolved_style.text_color = nextColor;
|
||||
|
||||
w.manual_flags = w.manual_flags && typeof w.manual_flags === "object" ? w.manual_flags : {};
|
||||
w.manual_flags.text_edited = true;
|
||||
w.manual_flags.style_edited = true;
|
||||
item.manual_flags = item.manual_flags && typeof item.manual_flags === "object" ? item.manual_flags : {};
|
||||
item.manual_flags.style_edited = true;
|
||||
if (String(item.id) === String(w.id)) item.manual_flags.text_edited = true;
|
||||
}
|
||||
|
||||
if (textInput) textInput.value = nextText;
|
||||
if (popoverTextInput) popoverTextInput.value = nextText;
|
||||
|
|
@ -3270,7 +3285,7 @@ function refreshSelectionUI(opts = {}) {
|
|||
refreshSelectionUI({ forceText: true });
|
||||
syncReviewedTextarea();
|
||||
renderCanvas();
|
||||
setStatus("Applied changes: " + nextFamily + " " + nextSize);
|
||||
setStatus("Applied style to " + selectedWords.length + " selected");
|
||||
return w;
|
||||
}
|
||||
|
||||
|
|
@ -3316,34 +3331,40 @@ function refreshSelectionUI(opts = {}) {
|
|||
}
|
||||
|
||||
function nudge(dx, dy) {
|
||||
const w = getSelectedWord();
|
||||
if (!w) return;
|
||||
const selectedWords = getSelectedWords();
|
||||
if (!selectedWords.length) return;
|
||||
pushHistory();
|
||||
w.bbox = normalizeBBox([
|
||||
Number(w.bbox[0]) + dx,
|
||||
Number(w.bbox[1]) + dy,
|
||||
Number(w.bbox[2]) + dx,
|
||||
Number(w.bbox[3]) + dy,
|
||||
]);
|
||||
w.bbox = snapBBox(w.bbox, w.id);
|
||||
for (const item of selectedWords) {
|
||||
item.bbox = normalizeBBox([
|
||||
Number(item.bbox[0]) + dx,
|
||||
Number(item.bbox[1]) + dy,
|
||||
Number(item.bbox[2]) + dx,
|
||||
Number(item.bbox[3]) + dy,
|
||||
]);
|
||||
item.bbox = snapBBox(item.bbox, item.id);
|
||||
item.manual_flags = item.manual_flags && typeof item.manual_flags === "object" ? item.manual_flags : {};
|
||||
item.manual_flags.geometry_edited = true;
|
||||
}
|
||||
refreshSelectionUI({ geometryOnly: true });
|
||||
syncReviewedTextarea();
|
||||
renderCanvas();
|
||||
setStatus("Nudged selection");
|
||||
setStatus("Nudged " + selectedWords.length + " selected");
|
||||
}
|
||||
|
||||
function deleteSelectedWord() {
|
||||
const w = getSelectedWord();
|
||||
if (!w) return;
|
||||
const selectedWords = getSelectedWords();
|
||||
if (!selectedWords.length) return;
|
||||
pushHistory();
|
||||
words = words.filter(item => String(item.id) !== String(selectedId));
|
||||
const deleteIds = new Set(selectedWords.map(w => String(w.id)));
|
||||
words = words.filter(item => !deleteIds.has(String(item.id)));
|
||||
selectedIds.clear();
|
||||
selectedId = null;
|
||||
nextWordId = Math.max(0, ...words.map(item => Number(item.id || 0))) + 1;
|
||||
snapGuides = null;
|
||||
refreshSelectionUI({ forceText: true });
|
||||
syncReviewedTextarea();
|
||||
renderCanvas();
|
||||
setStatus("Deleted selected box");
|
||||
setStatus("Deleted " + deleteIds.size + " selected");
|
||||
}
|
||||
|
||||
function buildLayoutReviewPayload() {
|
||||
|
|
@ -3540,6 +3561,14 @@ function refreshSelectionUI(opts = {}) {
|
|||
}
|
||||
|
||||
function handlePointerDown(ev) {
|
||||
activePointers.add(ev.pointerId);
|
||||
if (ev.pointerType === "touch" && activePointers.size >= 2) {
|
||||
multiSelectMode = true;
|
||||
document.body.classList.add("layout-multi-select-active");
|
||||
document.getElementById("layout-multi-select")?.classList.add("active");
|
||||
hidePopover();
|
||||
setStatus("Touch multi-select on");
|
||||
}
|
||||
if (tool === "pan") {
|
||||
ev.preventDefault();
|
||||
beginPan(ev);
|
||||
|
|
@ -3573,13 +3602,29 @@ function refreshSelectionUI(opts = {}) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Click selects immediately. Drag starts only if pointer moves enough.
|
||||
ev.preventDefault();
|
||||
selectedId = String(hit.word.id);
|
||||
refreshSelectionUI({ forceText: true });
|
||||
renderCanvas();
|
||||
// Click selects immediately. Drag starts only if pointer moves enough.
|
||||
ev.preventDefault();
|
||||
|
||||
dragState = {
|
||||
const id = String(hit.word.id);
|
||||
const additiveSelect = multiSelectMode || ev.ctrlKey || ev.metaKey || ev.shiftKey || activePointers.size >= 2 || touchMultiAnchor;
|
||||
|
||||
if (additiveSelect) {
|
||||
if (selectedIds.has(id)) selectedIds.delete(id);
|
||||
else selectedIds.add(id);
|
||||
selectedId = Array.from(selectedIds).at(-1) || null;
|
||||
hidePopover();
|
||||
refreshSelectionUI({ forceText: true });
|
||||
renderCanvas();
|
||||
setStatus("Selected: " + selectedIds.size);
|
||||
return;
|
||||
}
|
||||
|
||||
selectedIds = new Set([id]);
|
||||
selectedId = id;
|
||||
refreshSelectionUI({ forceText: true });
|
||||
renderCanvas();
|
||||
|
||||
dragState = {
|
||||
mode: "pending-move",
|
||||
wordId: hit.word.id,
|
||||
startX: ev.clientX,
|
||||
|
|
@ -3684,7 +3729,8 @@ function refreshSelectionUI(opts = {}) {
|
|||
document.body.classList.remove("layout-review-dragging");
|
||||
}
|
||||
|
||||
function endDrag() {
|
||||
function endDrag(ev) {
|
||||
if (ev && ev.pointerId != null) activePointers.delete(ev.pointerId);
|
||||
if (dragState && dragState.mode === "pending-move") {
|
||||
dragState = null;
|
||||
snapGuides = null;
|
||||
|
|
@ -3752,6 +3798,35 @@ function refreshSelectionUI(opts = {}) {
|
|||
renderCanvas();
|
||||
setStatus("Selection cleared");
|
||||
});
|
||||
|
||||
document.getElementById("layout-invert-selection")?.addEventListener("click", () => {
|
||||
const next = new Set();
|
||||
for (const w of words) {
|
||||
const id = String(w.id);
|
||||
if (!selectedIds.has(id)) next.add(id);
|
||||
}
|
||||
selectedIds = next;
|
||||
selectedId = Array.from(selectedIds).at(-1) || null;
|
||||
refreshSelectionUI({ forceText: true });
|
||||
renderCanvas();
|
||||
setStatus("Inverted selection: " + selectedIds.size);
|
||||
});
|
||||
|
||||
document.getElementById("layout-select-line")?.addEventListener("click", () => {
|
||||
const w = getSelectedWord();
|
||||
if (!w) return;
|
||||
const b = normalizeBBox(w.bbox || [0,0,0,0]);
|
||||
const cy = (b[1] + b[3]) / 2;
|
||||
selectedIds = new Set(words.filter(item => {
|
||||
const ib = normalizeBBox(item.bbox || [0,0,0,0]);
|
||||
const icy = (ib[1] + ib[3]) / 2;
|
||||
return Math.abs(icy - cy) <= LINE_GROUP_TOLERANCE;
|
||||
}).map(item => String(item.id)));
|
||||
selectedId = Array.from(selectedIds).at(-1) || null;
|
||||
refreshSelectionUI({ forceText: true });
|
||||
renderCanvas();
|
||||
setStatus("Selected line: " + selectedIds.size);
|
||||
});
|
||||
document.getElementById("layout-multi-select")?.addEventListener("click", () => {
|
||||
multiSelectMode = !multiSelectMode;
|
||||
document.getElementById("layout-multi-select")?.classList.toggle("active", multiSelectMode);
|
||||
|
|
|
|||
Loading…
Reference in New Issue