import { isAlive, types } from "mobx-state-tree";
|
|
import BaseTool, { DEFAULT_DIMENSIONS } from "./Base";
|
import ToolMixin from "../mixins/Tool";
|
import { MultipleClicksDrawingTool } from "../mixins/DrawingTool";
|
import { NodeViews } from "../components/Node/Node";
|
import { observe } from "mobx";
|
|
const _Tool = types
|
.model("VectorTool", {
|
group: "segmentation",
|
shortcut: "tool:vector",
|
})
|
.views((self) => {
|
const Super = {
|
createRegionOptions: self.createRegionOptions,
|
isIncorrectControl: self.isIncorrectControl,
|
isIncorrectLabel: self.isIncorrectLabel,
|
};
|
|
return {
|
get getActiveVector() {
|
const poly = self.currentArea;
|
|
if (poly && !isAlive(poly)) return null;
|
if (poly && poly.closed) return null;
|
if (poly === undefined) return null;
|
if (poly && poly.type !== "vectorregion") return null;
|
|
return poly;
|
},
|
|
get tagTypes() {
|
return {
|
stateTypes: "vectorlabels",
|
controlTagTypes: ["vectorlabels", "vector"],
|
};
|
},
|
|
get viewTooltip() {
|
return "Vector region";
|
},
|
|
get iconComponent() {
|
return self.dynamic ? NodeViews.VectorRegionModel.altIcon : NodeViews.VectorRegionModel.icon;
|
},
|
|
get defaultDimensions() {
|
return DEFAULT_DIMENSIONS.vector;
|
},
|
|
createRegionOptions() {
|
return Super.createRegionOptions({
|
vertices: [],
|
converted: true,
|
closed: false,
|
transformMode: false,
|
});
|
},
|
|
isIncorrectControl() {
|
return Super.isIncorrectControl() && self.current() === null;
|
},
|
isIncorrectLabel() {
|
return !self.current() && Super.isIncorrectLabel();
|
},
|
canStart() {
|
// Allow starting if no current region, OR if current region is closed (finished)
|
const currentRegion = self.current();
|
return currentRegion === null || (currentRegion && currentRegion.closed);
|
},
|
|
current() {
|
// First check self.currentArea
|
if (self.currentArea) {
|
return self.getActiveVector;
|
}
|
|
// If currentArea is null, try to find an active drawing vector region
|
// This handles the case when continuing to draw an existing region
|
const obj = self.obj;
|
|
// Try obj.regs first
|
let regionsToSearch = [];
|
if (obj?.regs && obj.regs.length > 0) {
|
regionsToSearch = Array.from(obj.regs);
|
} else if (self.annotation?.regions && self.annotation.regions.length > 0) {
|
regionsToSearch = Array.from(self.annotation.regions);
|
}
|
|
if (regionsToSearch.length > 0) {
|
// Priority 1: Check for highlighted/selected vector region that's not closed
|
const highlighted = self.annotation?.regionStore?.selection?.highlighted;
|
if (highlighted && highlighted.type === "vectorregion" && !highlighted.closed && isAlive(highlighted)) {
|
return highlighted;
|
}
|
|
// Priority 2: Check selected regions - if only one vector region is selected and not closed
|
const selectedRegions = self.annotation?.selectedRegions || [];
|
const selectedVectorRegions = selectedRegions.filter(
|
(reg) => reg.type === "vectorregion" && !reg.closed && isAlive(reg),
|
);
|
if (selectedVectorRegions.length === 1) {
|
return selectedVectorRegions[0];
|
}
|
|
// Priority 3: Try to find a region that's actively drawing
|
// Only allow continuing to draw if the region is actively being drawn (isDrawing: true)
|
// This prevents drawing on unselected regions that are just not closed
|
const activeDrawingVector = regionsToSearch.find(
|
(reg) => reg.type === "vectorregion" && reg.isDrawing && !reg.closed && isAlive(reg),
|
);
|
|
if (activeDrawingVector) {
|
return activeDrawingVector;
|
}
|
}
|
|
return self.getActiveVector;
|
},
|
|
getCurrentArea() {
|
// Override to use current() which finds regions even when self.currentArea is null
|
const currentRegion = self.current();
|
if (currentRegion) {
|
return currentRegion;
|
}
|
// Fallback to parent implementation
|
return self.currentArea;
|
},
|
};
|
})
|
.actions((self) => {
|
// Store the MultipleClicksDrawingTool's canStartDrawing before we override it
|
const MultipleClicksCanStartDrawing = self.canStartDrawing;
|
|
const Super = {
|
startDrawing: self.startDrawing,
|
_finishDrawing: self._finishDrawing,
|
deleteRegion: self.deleteRegion,
|
event: self.event,
|
};
|
|
const disposers = [];
|
let down = false;
|
let initialCursorPosition = null;
|
let lastClick = {
|
ts: 0,
|
x: 0,
|
y: 0,
|
};
|
|
return {
|
// Override event() to allow shift-key events through for ghost point insertion
|
event(name, ev, [x, y, canvasX, canvasY]) {
|
// For Vector tool, allow shift-key events to pass through
|
// This enables shift-click for inserting points on segments
|
if (ev.button > 0) return; // Still filter right clicks and middle clicks
|
|
let fn = `${name}Ev`;
|
|
if (typeof self[fn] !== "undefined") self[fn].call(self, ev, [x, y], [canvasX, canvasY]);
|
|
// Emulating of dblclick event
|
if (name === "click") {
|
const ts = ev.timeStamp;
|
|
if (ts - lastClick.ts < 300 && self.comparePointsWithThreshold(lastClick, { x, y })) {
|
fn = `dbl${fn}`;
|
if (typeof self[fn] !== "undefined") self[fn].call(self, ev, [x, y], [canvasX, canvasY]);
|
}
|
lastClick = { ts, x, y };
|
}
|
},
|
canStartDrawing() {
|
// Override to allow continuing to draw on selected/highlighted regions even if there's a selection
|
// This is Vector-specific behavior - other tools should use the default behavior from MultipleClicksDrawingTool
|
// First call the MultipleClicksDrawingTool's canStartDrawing (which includes selection check)
|
const mixinResult = MultipleClicksCanStartDrawing();
|
|
// If mixin allows drawing, we're good
|
if (mixinResult) return true;
|
|
// Otherwise, check if we have a current drawing region that should allow continuing
|
const currentRegion = self.current();
|
const hasCurrentDrawing = currentRegion && (currentRegion.isDrawing || !currentRegion.closed);
|
|
// Allow continuing to draw if there's a current drawing region, even with selection
|
if (hasCurrentDrawing) {
|
// Still need to check base conditions
|
return (
|
!self.disabled &&
|
!self.isIncorrectControl() &&
|
!self.isIncorrectLabel() &&
|
self.canStart() &&
|
!self.annotation.isDrawing
|
);
|
}
|
|
return false;
|
},
|
handleToolSwitch(tool) {
|
self.stopListening();
|
if (self.getCurrentArea()?.isDrawing && tool.toolName !== "ZoomPanTool") {
|
const shape = self.getCurrentArea()?.toJSON();
|
|
if (shape?.vertices?.length > 2) self.finishDrawing();
|
else self.cleanupUncloseableShape();
|
}
|
},
|
|
listenForClose() {
|
const { currentArea } = self;
|
if (!currentArea || !currentArea.closable) return;
|
|
disposers.push(
|
observe(currentArea, "closed", ({ newValue }) => newValue.storedValue && self.finishDrawing(), true),
|
);
|
disposers.push(
|
observe(currentArea, "finished", ({ newValue }) => newValue.storedValue && self.finishDrawing(), true),
|
);
|
},
|
|
stopListening() {
|
for (const disposer of disposers) {
|
disposer();
|
}
|
},
|
|
realCoordsFromCursor(x, y) {
|
const image = self.obj.currentImageEntity;
|
const width = image.naturalWidth;
|
const height = image.naturalHeight;
|
|
const realX = (x / 100) * width;
|
const realY = (y / 100) * height;
|
return { x: realX, y: realY };
|
},
|
|
startDrawing(x, y) {
|
if (!self.canStartDrawing()) return;
|
|
const { x: rx, y: ry } = self.realCoordsFromCursor(x, y);
|
|
initialCursorPosition = { x: rx, y: ry };
|
|
// Try to find existing drawing region first
|
let area = self.getCurrentArea();
|
|
// If no currentArea but there's an active drawing region, use it
|
if (!area) {
|
const obj = self.obj;
|
if (obj && obj.regs) {
|
const activeDrawingVector = obj.regs.find(
|
(reg) => reg.type === "vectorregion" && reg.isDrawing && !reg.closed && isAlive(reg),
|
);
|
if (activeDrawingVector) {
|
area = activeDrawingVector;
|
self.currentArea = area;
|
}
|
}
|
}
|
|
const currentArea = area && isAlive(area) ? area : null;
|
|
self.annotation.history.freeze();
|
|
// Only create new region if we don't have an existing one
|
if (!currentArea) {
|
self.currentArea = self.createRegion(self.createRegionOptions(), true);
|
} else {
|
self.currentArea = currentArea;
|
// If reusing an existing region, make sure it's marked as drawing
|
if (!currentArea.isDrawing) {
|
currentArea.setDrawing(true);
|
}
|
}
|
|
self.mode = "drawing";
|
self.setDrawing(true);
|
|
self.applyActiveStates(self.currentArea);
|
|
// Start listening for path closure
|
self.listenForClose();
|
|
// Only call startPoint if this is a new region (no existing points)
|
// If continuing an existing region, we'll just add points via addPoint
|
if (!currentArea || currentArea.vertices.length === 0) {
|
// we must skip one frame before starting a line
|
// to make sure KonvaVector was fully initialized
|
setTimeout(() => {
|
self.currentArea.startPoint(rx, ry);
|
});
|
}
|
},
|
|
mousedownEv(e, [x, y]) {
|
if (self.mode === "drawing") {
|
return;
|
}
|
down = true;
|
self.startDrawing(x, y);
|
},
|
|
mousemoveEv(_, [x, y]) {
|
if (!self.isDrawing) return;
|
const { x: rx, y: ry } = self.realCoordsFromCursor(x, y);
|
if (down && self.checkDistance(rx, ry)) {
|
self.currentArea?.updatePoint?.(rx, ry);
|
}
|
},
|
|
mouseupEv(_, [x, y]) {
|
if (!self.isDrawing) return;
|
const { x: rx, y: ry } = self.realCoordsFromCursor(x, y);
|
down = false;
|
|
// skipping a frame to let KonvaVector render and update properly
|
setTimeout(() => {
|
self.currentArea?.commitPoint?.(rx, ry);
|
self.annotation.history.unfreeze();
|
self.finishDrawing();
|
});
|
},
|
|
checkDistance(x, y) {
|
const distX = x - initialCursorPosition.x;
|
const distY = y - initialCursorPosition.y;
|
|
return Math.abs(distX) >= 5 || Math.abs(distY) >= 5;
|
},
|
|
_finishDrawing() {
|
const { currentArea, control } = self;
|
|
if (currentArea === null) return;
|
|
down = false;
|
self.currentArea?.notifyDrawingFinished();
|
self.setDrawing(false);
|
self.mode = "viewing";
|
self.currentArea = null;
|
self.stopListening();
|
self.annotation.afterCreateResult(currentArea, control);
|
},
|
|
setDrawing(drawing) {
|
self.currentArea?.setDrawing(drawing);
|
self.annotation.setIsDrawing(drawing);
|
},
|
|
deleteRegion() {
|
const { currentArea } = self;
|
|
self.setDrawing(false);
|
self.currentArea = null;
|
self.stopListening();
|
if (currentArea) {
|
currentArea.deleteRegion();
|
}
|
},
|
|
// Add point to current vector
|
addPoint(x, y) {
|
// Convert from percentage (0-100) to real coordinates using the same formula as startDrawing
|
const { x: rx, y: ry } = self.realCoordsFromCursor(x, y);
|
|
// Try to find the area - first check getCurrentArea, then look in annotation store
|
let area = self.getCurrentArea();
|
|
// If no currentArea but there's an active drawing region, use it
|
if (!area) {
|
const obj = self.obj;
|
if (obj && obj.regs) {
|
const activeDrawingVector = obj.regs.find(
|
(reg) => reg.type === "vectorregion" && reg.isDrawing && !reg.closed && isAlive(reg),
|
);
|
if (activeDrawingVector) {
|
area = activeDrawingVector;
|
self.currentArea = area;
|
}
|
}
|
}
|
|
if (area) {
|
area.addPoint(rx, ry);
|
}
|
},
|
|
// Finish drawing the current vector
|
finishDrawing() {
|
if (self.currentArea?.finished) {
|
self._finishDrawing();
|
}
|
},
|
|
complete() {
|
self._finishDrawing();
|
},
|
|
// Clean up uncloseable shape
|
cleanupUncloseableShape() {
|
if (self.currentArea?.incomplete) {
|
self.deleteRegion();
|
}
|
},
|
};
|
});
|
|
const Vector = types.compose(_Tool.name, ToolMixin, BaseTool, MultipleClicksDrawingTool, _Tool);
|
|
export { Vector };
|