@aspiresys/visor 1.2.11 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.js CHANGED
@@ -1,2 +1,270 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Region = void 0;
4
+ const nut_js_1 = require("@nut-tree-fork/nut-js");
5
+ const mouse_1 = require("./mouse");
6
+ const ocr_1 = require("./ocr");
7
+ const text_1 = require("./text");
8
+ const matcher_1 = require("./matcher");
9
+ const screen_1 = require("./screen");
10
+ const path_1 = require("./path");
11
+ const config_1 = require("./config");
12
+ const console_1 = require("console");
13
+ const display_1 = require("./display");
14
+ /**
15
+ * Represents a rectangular screen region.
16
+ *
17
+ * A Region can be returned by:
18
+ * - image matching
19
+ * - OCR text search
20
+ * - region-based searches
21
+ *
22
+ * Provides helper methods for:
23
+ * - mouse interaction
24
+ * - OCR operations
25
+ * - image matching
26
+ * - geometry calculations
27
+ * Example:
28
+ *
29
+ * const dialog = await visor.find("dialog.png");
30
+ *
31
+ * const saveButton = await dialog.find("save.png");
32
+ *
33
+ * await saveButton.click();
34
+ *
35
+ * const text = await dialog.findText("Submit");
36
+ */
37
+ class Region {
38
+ constructor(x, y, width, height, confidence = 1) {
39
+ this.x = x;
40
+ this.y = y;
41
+ this.width = width;
42
+ this.height = height;
43
+ this.confidence = confidence;
44
+ }
45
+ checkConfig() {
46
+ if (Region.configWarningShown) {
47
+ return;
48
+ }
49
+ const missing = [];
50
+ if (!config_1.visorConfig.imagePath) {
51
+ missing.push("imagePath");
52
+ }
53
+ if (!config_1.visorConfig.outputPath) {
54
+ missing.push("outputPath");
55
+ }
56
+ if (missing.length > 0) {
57
+ (0, console_1.warn)(`[VISOR] Configuration incomplete.\n` +
58
+ `Missing: ${missing.join(", ")}\n\n` +
59
+ `Relative paths may not work correctly.\n` +
60
+ `Use visor.loadConfig(...) or provide absolute paths directly to functions.`);
61
+ }
62
+ if (!config_1.visorConfig.initialized) {
63
+ config_1.visorConfig.scaleFactor = (0, display_1.getWindowsScaleFactor)();
64
+ (0, console_1.warn)(`[VISOR] Configuration not loaded.\n` +
65
+ `Auto-detected scaleFactor=${config_1.visorConfig.scaleFactor}.\n`);
66
+ }
67
+ if (missing.length > 0 || !config_1.visorConfig.initialized) {
68
+ Region.configWarningShown = true;
69
+ }
70
+ }
71
+ /**
72
+ * Returns the center point of the region.
73
+ *
74
+ * Useful for:
75
+ * - mouse movement
76
+ * - coordinate calculations
77
+ * - custom interactions
78
+ */
79
+ center() {
80
+ return {
81
+ x: this.x + Math.floor(this.width / 2),
82
+ y: this.y + Math.floor(this.height / 2)
83
+ };
84
+ }
85
+ /**
86
+ * Moves the mouse to the center of the region.
87
+ *
88
+ * Mouse coordinates are automatically
89
+ * adjusted for display scaling.
90
+ */
91
+ async move() {
92
+ await (0, mouse_1.moveToRegion)(this);
93
+ }
94
+ /**
95
+ * Performs a left click on the region.
96
+ *
97
+ * Clicks the center of the region by default.
98
+ *
99
+ * Optional offset can be supplied to click
100
+ * a specific position relative to the center.
101
+ */
102
+ async click(offset) {
103
+ await (0, mouse_1.clickRegion)(this, offset);
104
+ }
105
+ /**
106
+ * Performs a double left click on the region.
107
+ *
108
+ * Optional offset can be supplied to click
109
+ * a specific position relative to the center.
110
+ */
111
+ async doubleClick(offset) {
112
+ await (0, mouse_1.moveToRegion)(this, offset);
113
+ await nut_js_1.mouse.doubleClick(nut_js_1.Button.LEFT);
114
+ }
115
+ /**
116
+ * Performs a right click on the region.
117
+ *
118
+ * Optional offset can be supplied to click
119
+ * a specific position relative to the center.
120
+ */
121
+ async rightClick(offset) {
122
+ await (0, mouse_1.moveToRegion)(this, offset);
123
+ await nut_js_1.mouse.click(nut_js_1.Button.RIGHT);
124
+ }
125
+ /**
126
+ * Checks whether the supplied coordinates
127
+ * are located inside this region.
128
+ *
129
+ * Returns:
130
+ * - true if inside
131
+ * - false otherwise
132
+ */
133
+ contains(x, y) {
134
+ return (x >= this.x) && (x <= this.x + this.width) && (y >= this.y) && (y <= this.y + this.height);
135
+ }
136
+ /**
137
+ * Creates a new Region shifted by the
138
+ * supplied x and y offsets.
139
+ *
140
+ * Original region remains unchanged.
141
+ */
142
+ offset(x, y) {
143
+ return new Region(this.x + x, this.y + y, this.width, this.height, this.confidence);
144
+ }
145
+ /**
146
+ * Checks whether this region overlaps
147
+ * with another region.
148
+ *
149
+ * Returns:
150
+ * - true if regions intersect
151
+ * - false otherwise
152
+ */
153
+ intersects(region) {
154
+ return (this.x <= region.x + region.width) && (this.x + this.width >= region.x) && (this.y <= region.y + region.height) && (this.y + this.height >= region.y);
155
+ }
156
+ /**
157
+ * Returns the total area of the region
158
+ * in pixels.
159
+ */
160
+ area() {
161
+ return this.width * this.height;
162
+ }
163
+ /**
164
+ * Extracts OCR text from this region.
165
+ *
166
+ * Returns full OCR output including:
167
+ * - text
168
+ * - TSV data
169
+ * - HOCR data
170
+ * - block information
171
+ */
172
+ async readText() {
173
+ this.checkConfig();
174
+ return await (0, ocr_1.extractTextFromRegion)(this);
175
+ }
176
+ /**
177
+ * Searches for text within this region.
178
+ *
179
+ * Returns the matching text region
180
+ * if found, otherwise null.
181
+ *
182
+ * Supports selecting a specific match
183
+ * using the index parameter.
184
+ */
185
+ async findText(text, index = 0) {
186
+ this.checkConfig();
187
+ const match = await (0, text_1.findText)(text, index, this);
188
+ if (match == null) {
189
+ return null;
190
+ }
191
+ return match.offset(this.x, this.y);
192
+ }
193
+ /**
194
+ * Checks whether the supplied text
195
+ * exists within this region.
196
+ *
197
+ * Returns:
198
+ * - true if found
199
+ * - false otherwise
200
+ */
201
+ async existsText(text) {
202
+ this.checkConfig();
203
+ return await (0, text_1.existsText)(text, this);
204
+ }
205
+ /**
206
+ * Searches for an image within this region.
207
+ *
208
+ * Matching is performed only inside
209
+ * the current region, improving speed
210
+ * and reducing false positives.
211
+ *
212
+ * Returns:
213
+ * - matching Region if found
214
+ * - null otherwise
215
+ */
216
+ async find(image, confidence = 0.8) {
217
+ this.checkConfig();
218
+ const screenshot = await (0, screen_1.captureScreen)();
219
+ const croppedScreenshot = await (0, matcher_1.cropMat)(screenshot, this);
220
+ const template = await (0, matcher_1.loadTemplate)((0, path_1.resolveImagePath)(image));
221
+ try {
222
+ const match = (0, matcher_1.matchInspectorTemplate)(croppedScreenshot, template, confidence, false);
223
+ if (!match) {
224
+ return null;
225
+ }
226
+ return match.offset(this.x, this.y);
227
+ }
228
+ finally {
229
+ screenshot.delete();
230
+ croppedScreenshot.delete();
231
+ template.delete();
232
+ }
233
+ }
234
+ /**
235
+ * Finds all image matches within this region.
236
+ *
237
+ * Matching is performed only inside
238
+ * the current region.
239
+ *
240
+ * Returns an array of matching regions.
241
+ */
242
+ async findAll(image, confidence = 0.8) {
243
+ this.checkConfig();
244
+ const screenshot = await (0, screen_1.captureScreen)();
245
+ const croppedScreenshot = await (0, matcher_1.cropMat)(screenshot, this);
246
+ const template = await (0, matcher_1.loadTemplate)((0, path_1.resolveImagePath)(image));
247
+ try {
248
+ const matches = (0, matcher_1.findAllMatches)(croppedScreenshot, template, confidence);
249
+ return matches.map(match => match.offset(this.x, this.y));
250
+ }
251
+ finally {
252
+ screenshot.delete();
253
+ croppedScreenshot.delete();
254
+ template.delete();
255
+ }
256
+ }
257
+ /**
258
+ * Checks whether an image exists
259
+ * within this region.
260
+ *
261
+ * Returns:
262
+ * - true if found
263
+ * - false otherwise
264
+ */
265
+ async exists(image, confidence = 0.8) {
266
+ return await this.find(image, confidence) !== null;
267
+ }
268
+ }
269
+ exports.Region = Region;
270
+ Region.configWarningShown = false;
@@ -5,6 +5,9 @@
5
5
  <link rel="stylesheet" href="styles.css" />
6
6
  </head>
7
7
  <body>
8
+ <div id="imageModal" class="image-modal">
9
+ <img id="modalImage"/>
10
+ </div>
8
11
  <div class="container">
9
12
  <header class="header">
10
13
  <h3>VISOR INSPECTOR</h3>
@@ -22,10 +25,13 @@
22
25
  <div class="controls-grid">
23
26
  <div class="control-group">
24
27
  <div class="input-wrapper">
25
- <span class="input-hint">Confidence</span>
26
- <input id="confidenceInput" type="number" min="0" max="1" step="0.01" value="0.8" />
27
- <span class="input-hint">(0.0 - 1.0) </span>
28
- Current Template: <span id="currentTemplate" class="template-status">None</span>
28
+ <span class="input-hint" title="Range [0, 1]">Confidence</span>
29
+ <input type="range" id="confRange" min="0" max="1" value="0.8" step="0.01"
30
+ oninput="document.getElementById('confidenceInput').value = this.value"/>
31
+ <input id="confidenceInput" type="number" min="0" max="1" step="0.01" value="0.8"
32
+ onchange="document.getElementById('confRange').value = this.value"/>
33
+ <!--<span class="input-hint">Multi Sacle Match:</span><input id="multiScaleCheck" type="checkbox" value="false"/>-->
34
+ <span class="input-hint">Current Template:</span><span id="currentTemplate" class="template-status">None</span>
29
35
  </div>
30
36
  </div>
31
37
  </div>
package/inspector/main.js CHANGED
@@ -135,12 +135,17 @@ ipcMain.handle("test-match",
135
135
  const capturePath = path.join(__dirname, "assets", "capture.png");
136
136
  const screen = await visorMatcher.loadScreen(capturePath);
137
137
  const template = await visorMatcher.loadScreen(data.templatePath);
138
- const result = visorMatcher
139
- .matchTemplate(
140
- screen,
141
- template,
142
- data.confidence
143
- );
138
+ const result = visorMatcher.findAllInspectorMatches(
139
+ screen,
140
+ template,
141
+ data.confidence
142
+ );
143
+ ` const result = visorMatcher.matchInspectorTemplate(
144
+ screen,
145
+ template,
146
+ data.confidence,
147
+ data.isMultiMatch
148
+ );`
144
149
  console.log("match Result:", result);
145
150
  screen.delete();
146
151
  template.delete();
@@ -31,6 +31,7 @@ captureBtn.addEventListener("click",
31
31
  canvas.width = image.width * scale;
32
32
  canvas.height = image.height * scale;
33
33
  matchResult.textContent = " No Match Tested ";
34
+ ctx.imageSmoothingEnabled = false;
34
35
  ctx.drawImage(
35
36
  image,
36
37
  0,
@@ -89,24 +90,14 @@ canvas.addEventListener("mousemove",
89
90
  canvas.addEventListener("mouseup",
90
91
  () => {
91
92
  selecting = false;
92
- console.log(
93
- startX,
94
- startY,
95
- endX,
96
- endY
97
- );
93
+ console.log(startX, startY, endX, endY);
98
94
  }
99
95
  );
100
96
 
101
97
 
102
98
  saveBtn.addEventListener("click",
103
99
  async () => {
104
- if (
105
- startX === undefined ||
106
- startY === undefined ||
107
- endX === undefined ||
108
- endY === undefined
109
- ) {
100
+ if (startX === undefined || startY === undefined || endX === undefined || endY === undefined) {
110
101
  alert("Please select an area first.");
111
102
  return;
112
103
  }
@@ -120,17 +111,17 @@ saveBtn.addEventListener("click",
120
111
  return;
121
112
  }
122
113
  await window.visor.saveTemplate({
123
- x: Math.min(startX, endX),
124
- y: Math.min(startY, endY),
125
- width: Math.abs(endX - startX),
126
- height: Math.abs(endY - startY),
127
- outputPath: filePath
128
- });
114
+ x: Math.min(startX, endX),
115
+ y: Math.min(startY, endY),
116
+ width: Math.abs(endX - startX),
117
+ height: Math.abs(endY - startY),
118
+ outputPath: filePath
119
+ });
129
120
  const fileName = filePath.split(/[\\/]/).pop();
130
121
  selectedTemplatePath = filePath;
131
122
  currentTemplate.textContent = fileName;
132
123
  currentTemplate.title = filePath;
133
- matchResult.textContent = " No Match Tested ";
124
+ matchResult.textContent = "<b>No Match Tested</b>";
134
125
  ctx.drawImage(
135
126
  image,
136
127
  0,
@@ -143,29 +134,36 @@ saveBtn.addEventListener("click",
143
134
  }
144
135
  );
145
136
 
146
- testMatchBtn.addEventListener("click", async () => {
147
- if (!selectedTemplatePath) {
148
- alert("Please load or save a template first.");
149
- return;
150
- }
151
-
152
- const confidence = parseFloat(confidenceInput.value);
153
- if (isNaN(confidence) || confidence < 0 || confidence > 1) {
154
- alert("Confidence must be between 0 and 1");
155
- return;
156
- }
157
-
158
- const result = await window.visor.testMatch({
159
- templatePath: selectedTemplatePath,
160
- confidence: confidence
161
- });
162
- console.log(
163
- "Match Result:",
164
- result
165
- );
166
-
167
- if (!result) {
168
- matchResult.textContent = "✗ MATCH NOT FOUND";
137
+ testMatchBtn.addEventListener("click",
138
+ async () => {
139
+ if (!selectedTemplatePath) {
140
+ alert("Please load or save a template first.");
141
+ return;
142
+ }
143
+ const confidence = parseFloat(confidenceInput.value);
144
+ if (isNaN(confidence) || confidence < 0 || confidence > 1) {
145
+ alert("Confidence must be between 0 and 1");
146
+ return;
147
+ }
148
+ matchResult.textContent = "🔍 Matching...";
149
+ await new Promise(resolve => setTimeout(resolve, 10));
150
+ const result = await window.visor.testMatch({
151
+ templatePath: selectedTemplatePath,
152
+ confidence: confidence,
153
+ //isMultiMatch: document.getElementById("multiScaleCheck").checked
154
+ });
155
+ console.log("Match Result:", result);
156
+ if (!result || result.length==0) {
157
+ matchResult.textContent = "✗ MATCH NOT FOUND";
158
+ ctx.drawImage(
159
+ image,
160
+ 0,
161
+ 0,
162
+ canvas.width,
163
+ canvas.height
164
+ );
165
+ return;
166
+ }
169
167
  ctx.drawImage(
170
168
  image,
171
169
  0,
@@ -173,37 +171,43 @@ testMatchBtn.addEventListener("click", async () => {
173
171
  canvas.width,
174
172
  canvas.height
175
173
  );
176
- return;
177
- }
178
- ctx.drawImage(
179
- image,
180
- 0,
181
- 0,
182
- canvas.width,
183
- canvas.height
184
- );
185
- ctx.strokeStyle = "lime";
186
- ctx.lineWidth = 3;
187
- ctx.strokeRect(
188
- result.x / imageScaleX,
189
- result.y / imageScaleY,
190
- result.width / imageScaleX,
191
- result.height / imageScaleY
192
- );
193
- matchResult.textContent = `
194
- ✓ MATCH FOUND
195
-
196
- Confidence:
197
- ${result.confidence.toFixed(3)}
198
-
199
- Location:
200
- (${result.x}, ${result.y})
201
-
202
- Size:
203
- ${result.width} x ${result.height}
204
- `;
174
+ let matchR = "<b>✓ MATCH FOUND</b>\n";
175
+ let i = 1;
176
+ for(const match of result){
177
+ ctx.strokeStyle = "lime";
178
+ ctx.lineWidth = 2;
179
+ ctx.strokeRect(
180
+ match.x / imageScaleX,
181
+ match.y / imageScaleY,
182
+ match.width / imageScaleX,
183
+ match.height / imageScaleY
184
+ );
185
+ matchR += `<b>Match:</b> ${i}
186
+ <b>Confidence:</b>
187
+ ${match.confidence.toFixed(3)}
188
+ <b>Location:</b>
189
+ (${match.x}, ${match.y})
190
+ <b>Size:</b>
191
+ ${match.width} x ${match.height}
192
+ <hr>
193
+ `;
194
+ i += 1;
195
+ }
196
+ matchResult.innerHTML = matchR;
205
197
  });
206
198
 
199
+ templatePreview.addEventListener("click",
200
+ () => {
201
+ modalImage.src = templatePreview.src;
202
+ imageModal.style.display = "flex";
203
+ }
204
+ );
205
+
206
+ imageModal.addEventListener("click",
207
+ () => {
208
+ imageModal.style.display = "none";
209
+ }
210
+ );
207
211
 
208
212
  loadTemplateBtn.addEventListener("click",
209
213
  async () => {
@@ -215,7 +219,7 @@ loadTemplateBtn.addEventListener("click",
215
219
  selectedTemplatePath = path;
216
220
  currentTemplate.textContent = fileName;
217
221
  currentTemplate.title = path;
218
- matchResult.textContent = " No Match Tested ";
222
+ matchResult.innerHTML = "<b>No Match Tested</b>";
219
223
  ctx.drawImage(
220
224
  image,
221
225
  0,
@@ -8,10 +8,7 @@ const electron = require("electron");
8
8
  spawn(
9
9
  electron,
10
10
  [
11
- path.join(
12
- __dirname,
13
- "main.js"
14
- )
11
+ path.join(__dirname, "main.js")
15
12
  ],
16
13
  {
17
14
  stdio: "inherit"
@@ -199,18 +199,29 @@ body {
199
199
  gap: 10px;
200
200
  }
201
201
 
202
- input[type="number"] {
202
+ input[type="range"] {
203
203
  background: rgba(37, 37, 38, 0.8);
204
204
  color: #00d4ff;
205
- border: 1px solid rgba(0, 212, 255, 0.3);
206
- padding: 5px 6px;
207
- border-radius: 6px;
208
- font-size: 0.8em;
209
- width: 60px;
205
+ width: 100px;
210
206
  transition: all 0.3s ease;
207
+ height: 5px;
211
208
  }
212
209
 
213
- input[type="number"]:focus {
210
+ input[type="number"] {
211
+ color: rgb(0, 212, 255);
212
+ font-size: 0.8em;
213
+ width: 60px;
214
+ background: rgba(37, 37, 38, 0.8);
215
+ border-width: 1px;
216
+ border-style: solid;
217
+ border-color: rgba(0, 212, 255, 0.3);
218
+ border-image: initial;
219
+ padding: 5px 6px;
220
+ border-radius: 6px;
221
+ transition: all 0.3s ease;
222
+ }
223
+
224
+ input[type="range"]:focus, input[type="number"]:focus {
214
225
  outline: none;
215
226
  border-color: #00d4ff;
216
227
  box-shadow: 0 0 12px rgba(0, 212, 255, 0.2);
@@ -286,6 +297,7 @@ input[type="number"]:focus {
286
297
  display: none;
287
298
  margin-top: 15px;
288
299
  margin-bottom: 15px;
300
+ cursor: zoom-in;
289
301
  }
290
302
 
291
303
  .template-preview[src]:not([src=""]) {
@@ -344,22 +356,23 @@ input[type="number"]:focus {
344
356
  }
345
357
 
346
358
  #matchResult {
347
- margin-top: 15px;
348
- padding: 15px;
359
+ padding: 12px;
360
+ min-height: 157px;
361
+ max-height: 300px;
362
+ overflow-y: auto;
363
+ max-width: max-content;
364
+ background: rgba(255, 255, 255, 0.03);
365
+ border: 1px solid rgba(0, 212, 255, 0.2);
349
366
  border-radius: 8px;
350
- background: rgba(37, 37, 38, 0.6);
351
- border: 1px solid rgba(100, 150, 180, 0.2);
352
- min-height: 60px;
353
- display: inline-flex;
354
- align-items: center;
355
- justify-content: center;
356
- color: #888;
367
+ font-family: Consolas, monospace;
368
+ white-space: pre-line;
357
369
  font-size: 0.8em;
370
+ border-color: rgb(76, 175, 79);
358
371
  }
359
372
 
360
373
  #matchResult:not(:empty) {
361
374
  background: rgba(76, 175, 80, 0.1);
362
- border-color: rgba(76, 175, 80, 0.3);
375
+ border-color: rgb(76, 175, 79);
363
376
  color: #e0e0e0;
364
377
  }
365
378
 
@@ -424,22 +437,22 @@ input[type="number"]:focus {
424
437
 
425
438
  .result-panel {
426
439
  width: 250px;
427
-
428
440
  flex-shrink: 0;
429
441
  }
430
442
 
431
- #matchResult {
432
- padding: 12px;
433
-
434
- min-height: 157px;
435
-
436
- background: rgba(255, 255, 255, 0.03);
437
-
438
- border: 1px solid rgba(0, 212, 255, 0.2);
439
-
440
- border-radius: 8px;
441
-
442
- font-family: Consolas, monospace;
443
443
 
444
- white-space: pre-line;
444
+ .image-modal {
445
+ display: none;
446
+ position: fixed;
447
+ inset: 0;
448
+ background:
449
+ rgba(0,0,0,0.8);
450
+ justify-content: center;
451
+ align-items: center;
452
+ z-index: 9999;
445
453
  }
454
+
455
+ .image-modal img {
456
+ max-width: 90vw;
457
+ max-height: 90vh;
458
+ }
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "@aspiresys/visor",
3
- "version": "1.2.11",
3
+ "version": "1.3.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
7
7
  "build": "tsc",
8
8
  "start": "ts-node src/test.ts"
9
9
  },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
10
16
  "dependencies": {
11
17
  "@nut-tree-fork/nut-js": "^4.1.0",
12
18
  "@techstark/opencv-js": "^4.12.0-release.1",