@pooder/kit 5.3.1 → 6.0.0
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/.test-dist/src/extensions/background.js +475 -131
- package/.test-dist/src/extensions/dieline.js +283 -180
- package/.test-dist/src/extensions/dielineShape.js +66 -0
- package/.test-dist/src/extensions/feature.js +388 -303
- package/.test-dist/src/extensions/film.js +133 -74
- package/.test-dist/src/extensions/geometry.js +120 -56
- package/.test-dist/src/extensions/image.js +296 -212
- package/.test-dist/src/extensions/index.js +1 -3
- package/.test-dist/src/extensions/maskOps.js +75 -20
- package/.test-dist/src/extensions/ruler.js +312 -215
- package/.test-dist/src/extensions/sceneLayoutModel.js +9 -3
- package/.test-dist/src/extensions/sceneVisibility.js +3 -10
- package/.test-dist/src/extensions/tracer.js +229 -58
- package/.test-dist/src/extensions/white-ink.js +139 -129
- package/.test-dist/src/services/CanvasService.js +888 -126
- package/.test-dist/src/services/index.js +1 -0
- package/.test-dist/src/services/visibility.js +54 -0
- package/.test-dist/tests/run.js +58 -4
- package/CHANGELOG.md +12 -0
- package/dist/index.d.mts +377 -82
- package/dist/index.d.ts +377 -82
- package/dist/index.js +3920 -2178
- package/dist/index.mjs +3992 -2247
- package/package.json +1 -1
- package/src/extensions/background.ts +631 -145
- package/src/extensions/dieline.ts +280 -187
- package/src/extensions/dielineShape.ts +109 -0
- package/src/extensions/feature.ts +485 -366
- package/src/extensions/film.ts +152 -76
- package/src/extensions/geometry.ts +203 -104
- package/src/extensions/image.ts +319 -238
- package/src/extensions/index.ts +0 -1
- package/src/extensions/ruler.ts +481 -268
- package/src/extensions/sceneLayoutModel.ts +18 -6
- package/src/extensions/white-ink.ts +157 -171
- package/src/services/CanvasService.ts +1126 -140
- package/src/services/index.ts +1 -0
- package/src/services/renderSpec.ts +69 -4
- package/src/services/visibility.ts +78 -0
- package/tests/run.ts +139 -4
- package/.test-dist/src/CanvasService.js +0 -249
- package/.test-dist/src/ViewportSystem.js +0 -75
- package/.test-dist/src/background.js +0 -203
- package/.test-dist/src/bridgeSelection.js +0 -20
- package/.test-dist/src/constraints.js +0 -237
- package/.test-dist/src/dieline.js +0 -818
- package/.test-dist/src/edgeScale.js +0 -12
- package/.test-dist/src/feature.js +0 -826
- package/.test-dist/src/featureComplete.js +0 -32
- package/.test-dist/src/film.js +0 -167
- package/.test-dist/src/geometry.js +0 -506
- package/.test-dist/src/image.js +0 -1250
- package/.test-dist/src/maskOps.js +0 -270
- package/.test-dist/src/mirror.js +0 -104
- package/.test-dist/src/renderSpec.js +0 -2
- package/.test-dist/src/ruler.js +0 -343
- package/.test-dist/src/sceneLayout.js +0 -99
- package/.test-dist/src/sceneLayoutModel.js +0 -196
- package/.test-dist/src/sceneView.js +0 -40
- package/.test-dist/src/sceneVisibility.js +0 -42
- package/.test-dist/src/size.js +0 -332
- package/.test-dist/src/tracer.js +0 -544
- package/.test-dist/src/white-ink.js +0 -829
- package/.test-dist/src/wrappedOffsets.js +0 -33
- package/src/extensions/sceneVisibility.ts +0 -71
|
@@ -12,6 +12,7 @@ exports.computeSceneLayout = computeSceneLayout;
|
|
|
12
12
|
exports.buildSceneGeometry = buildSceneGeometry;
|
|
13
13
|
const coordinate_1 = require("../coordinate");
|
|
14
14
|
const units_1 = require("../units");
|
|
15
|
+
const dielineShape_1 = require("./dielineShape");
|
|
15
16
|
const DEFAULT_SIZE_STATE = {
|
|
16
17
|
unit: "mm",
|
|
17
18
|
actualWidthMm: 500,
|
|
@@ -180,10 +181,13 @@ function computeSceneLayout(canvasService, size) {
|
|
|
180
181
|
function buildSceneGeometry(configService, layout) {
|
|
181
182
|
const radiusMm = (0, units_1.parseLengthToMm)(configService.get("dieline.radius", 0), "mm");
|
|
182
183
|
const offset = (layout.cutRect.width - layout.trimRect.width) / 2;
|
|
184
|
+
const sourceWidth = Number(configService.get("dieline.customSourceWidthPx", 0));
|
|
185
|
+
const sourceHeight = Number(configService.get("dieline.customSourceHeightPx", 0));
|
|
186
|
+
const shapeStyle = (0, dielineShape_1.normalizeShapeStyle)(configService.get("dieline.shapeStyle", dielineShape_1.DEFAULT_DIELINE_SHAPE_STYLE));
|
|
183
187
|
return {
|
|
184
|
-
shape: configService.get("dieline.shape",
|
|
185
|
-
|
|
186
|
-
|
|
188
|
+
shape: (0, dielineShape_1.normalizeDielineShape)(configService.get("dieline.shape", dielineShape_1.DEFAULT_DIELINE_SHAPE)),
|
|
189
|
+
shapeStyle,
|
|
190
|
+
unit: "px",
|
|
187
191
|
x: layout.trimRect.centerX,
|
|
188
192
|
y: layout.trimRect.centerY,
|
|
189
193
|
width: layout.trimRect.width,
|
|
@@ -192,5 +196,7 @@ function buildSceneGeometry(configService, layout) {
|
|
|
192
196
|
offset,
|
|
193
197
|
scale: layout.scale,
|
|
194
198
|
pathData: configService.get("dieline.pathData"),
|
|
199
|
+
customSourceWidthPx: Number.isFinite(sourceWidth) && sourceWidth > 0 ? sourceWidth : undefined,
|
|
200
|
+
customSourceHeightPx: Number.isFinite(sourceHeight) && sourceHeight > 0 ? sourceHeight : undefined,
|
|
195
201
|
};
|
|
196
202
|
}
|
|
@@ -45,17 +45,10 @@ class SceneVisibilityService {
|
|
|
45
45
|
const dielineLayer = this.canvasService.getLayer("dieline-overlay");
|
|
46
46
|
if (dielineLayer) {
|
|
47
47
|
const visible = !HIDDEN_DIELINE_TOOLS.has(this.activeToolId || "");
|
|
48
|
-
|
|
49
|
-
dielineLayer.set({ visible });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const rulerLayer = this.canvasService.getLayer("ruler-overlay");
|
|
53
|
-
if (rulerLayer) {
|
|
54
|
-
const visible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
|
|
55
|
-
if (rulerLayer.visible !== visible) {
|
|
56
|
-
rulerLayer.set({ visible });
|
|
57
|
-
}
|
|
48
|
+
this.canvasService.setLayerVisibility("dieline-overlay", visible);
|
|
58
49
|
}
|
|
50
|
+
const rulerVisible = !HIDDEN_RULER_TOOLS.has(this.activeToolId || "");
|
|
51
|
+
this.canvasService.setLayerVisibility("ruler-overlay", rulerVisible);
|
|
59
52
|
this.canvasService.requestRenderAll();
|
|
60
53
|
}
|
|
61
54
|
}
|
|
@@ -24,6 +24,15 @@ class ImageTracer {
|
|
|
24
24
|
const img = await this.loadImage(imageUrl);
|
|
25
25
|
const width = img.width;
|
|
26
26
|
const height = img.height;
|
|
27
|
+
if (width <= 0 || height <= 0) {
|
|
28
|
+
const w = options.scaleToWidth ?? 0;
|
|
29
|
+
const h = options.scaleToHeight ?? 0;
|
|
30
|
+
return {
|
|
31
|
+
pathData: `M 0 0 L ${w} 0 L ${w} ${h} L 0 ${h} Z`,
|
|
32
|
+
baseBounds: { x: 0, y: 0, width: w, height: h },
|
|
33
|
+
bounds: { x: 0, y: 0, width: w, height: h },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
27
36
|
const debug = options.debug === true;
|
|
28
37
|
const debugLog = (message, payload) => {
|
|
29
38
|
if (!debug)
|
|
@@ -34,7 +43,7 @@ class ImageTracer {
|
|
|
34
43
|
}
|
|
35
44
|
console.info(`[ImageTracer] ${message}`);
|
|
36
45
|
};
|
|
37
|
-
//
|
|
46
|
+
// Draw to canvas and get pixel data
|
|
38
47
|
const canvas = document.createElement("canvas");
|
|
39
48
|
canvas.width = width;
|
|
40
49
|
canvas.height = height;
|
|
@@ -43,64 +52,122 @@ class ImageTracer {
|
|
|
43
52
|
throw new Error("Could not get 2D context");
|
|
44
53
|
ctx.drawImage(img, 0, 0);
|
|
45
54
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
46
|
-
//
|
|
55
|
+
// Strategy: fixed internal morphology + single-component target.
|
|
47
56
|
const threshold = options.threshold ?? 10;
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
57
|
+
const expand = Math.max(0, Math.floor(options.expand ?? 0));
|
|
58
|
+
const simplifyTolerance = options.simplifyTolerance ?? 2.5;
|
|
59
|
+
const useSmoothing = options.smoothing !== false;
|
|
60
|
+
const componentMode = "all";
|
|
61
|
+
const minComponentArea = 0;
|
|
62
|
+
const maxDim = Math.max(width, height);
|
|
63
|
+
const maskMode = "auto";
|
|
64
|
+
const whiteThreshold = 240;
|
|
65
|
+
const alphaOpaqueCutoff = 250;
|
|
66
|
+
const preprocessDilateRadius = Math.max(2, Math.floor(Math.max(maxDim * 0.012, expand * 0.35)));
|
|
67
|
+
const preprocessErodeRadius = Math.max(1, Math.floor(preprocessDilateRadius * 0.65));
|
|
68
|
+
const smoothDilateRadius = Math.max(1, Math.floor(preprocessDilateRadius * 0.25));
|
|
69
|
+
const smoothErodeRadius = Math.max(1, Math.floor(smoothDilateRadius * 0.8));
|
|
70
|
+
const connectStartDilateRadius = Math.max(1, Math.floor(Math.max(maxDim * 0.006, expand * 0.2)));
|
|
71
|
+
const connectMaxDilateRadius = Math.max(connectStartDilateRadius, Math.floor(Math.max(maxDim * 0.2, expand * 2.5)));
|
|
72
|
+
const connectErodeRatio = 0.65;
|
|
60
73
|
debugLog("traceWithBounds:start", {
|
|
61
74
|
width,
|
|
62
75
|
height,
|
|
63
76
|
threshold,
|
|
64
|
-
radius,
|
|
65
77
|
expand,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
simplifyTolerance,
|
|
79
|
+
smoothing: useSmoothing,
|
|
80
|
+
strategy: {
|
|
81
|
+
maskMode,
|
|
82
|
+
whiteThreshold,
|
|
83
|
+
alphaOpaqueCutoff,
|
|
84
|
+
fillHoles: true,
|
|
85
|
+
preprocessDilateRadius,
|
|
86
|
+
preprocessErodeRadius,
|
|
87
|
+
smoothDilateRadius,
|
|
88
|
+
smoothErodeRadius,
|
|
89
|
+
connectEnabled: true,
|
|
90
|
+
connectStartDilateRadius,
|
|
91
|
+
connectMaxDilateRadius,
|
|
92
|
+
connectErodeRatio,
|
|
74
93
|
},
|
|
75
|
-
componentMode,
|
|
76
|
-
minComponentArea,
|
|
77
|
-
forceConnected: options.forceConnected === true,
|
|
78
|
-
simplifyTolerance: options.simplifyTolerance ?? 2.5,
|
|
79
|
-
smoothing: options.smoothing !== false,
|
|
80
94
|
});
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
const padding = radius + expand + 2;
|
|
95
|
+
// Padding must cover morphology and expansion margins.
|
|
96
|
+
const padding = Math.max(preprocessDilateRadius, smoothDilateRadius, connectMaxDilateRadius, expand) + 2;
|
|
84
97
|
const paddedWidth = width + padding * 2;
|
|
85
98
|
const paddedHeight = height + padding * 2;
|
|
99
|
+
const summarizeMaskContours = (m) => {
|
|
100
|
+
const summary = this.summarizeAllContours(m, paddedWidth, paddedHeight, minComponentArea);
|
|
101
|
+
return {
|
|
102
|
+
rawContourCount: summary.rawCount,
|
|
103
|
+
selectedContourCount: summary.selectedCount,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
86
106
|
let mask = (0, maskOps_1.createMask)(imageData, {
|
|
87
107
|
threshold,
|
|
88
108
|
padding,
|
|
89
109
|
paddedWidth,
|
|
90
110
|
paddedHeight,
|
|
91
|
-
maskMode
|
|
92
|
-
whiteThreshold
|
|
111
|
+
maskMode,
|
|
112
|
+
whiteThreshold,
|
|
93
113
|
alphaOpaqueCutoff,
|
|
94
114
|
});
|
|
95
|
-
if (
|
|
96
|
-
mask
|
|
115
|
+
if (debug) {
|
|
116
|
+
debugLog("traceWithBounds:mask:after-create", summarizeMaskContours(mask));
|
|
117
|
+
}
|
|
118
|
+
mask = (0, maskOps_1.circularMorphology)(mask, paddedWidth, paddedHeight, preprocessDilateRadius, "dilate");
|
|
119
|
+
mask = (0, maskOps_1.fillHoles)(mask, paddedWidth, paddedHeight);
|
|
120
|
+
mask = (0, maskOps_1.circularMorphology)(mask, paddedWidth, paddedHeight, preprocessErodeRadius, "erode");
|
|
121
|
+
mask = (0, maskOps_1.fillHoles)(mask, paddedWidth, paddedHeight);
|
|
122
|
+
if (debug) {
|
|
123
|
+
debugLog("traceWithBounds:mask:after-preprocess", {
|
|
124
|
+
dilateRadius: preprocessDilateRadius,
|
|
125
|
+
erodeRadius: preprocessErodeRadius,
|
|
126
|
+
...summarizeMaskContours(mask),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
mask = (0, maskOps_1.circularMorphology)(mask, paddedWidth, paddedHeight, smoothDilateRadius, "dilate");
|
|
130
|
+
mask = (0, maskOps_1.fillHoles)(mask, paddedWidth, paddedHeight);
|
|
131
|
+
mask = (0, maskOps_1.circularMorphology)(mask, paddedWidth, paddedHeight, smoothErodeRadius, "erode");
|
|
132
|
+
mask = (0, maskOps_1.fillHoles)(mask, paddedWidth, paddedHeight);
|
|
133
|
+
if (debug) {
|
|
134
|
+
debugLog("traceWithBounds:mask:after-smooth", {
|
|
135
|
+
dilateRadius: smoothDilateRadius,
|
|
136
|
+
erodeRadius: smoothErodeRadius,
|
|
137
|
+
...summarizeMaskContours(mask),
|
|
138
|
+
});
|
|
97
139
|
}
|
|
98
|
-
|
|
99
|
-
|
|
140
|
+
const beforeConnectSummary = summarizeMaskContours(mask);
|
|
141
|
+
if (beforeConnectSummary.selectedContourCount <= 1) {
|
|
142
|
+
debugLog("traceWithBounds:mask:connect-skipped", {
|
|
143
|
+
reason: "already-single-component",
|
|
144
|
+
before: beforeConnectSummary,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
const connectResult = this.findForceConnectResult(mask, paddedWidth, paddedHeight, minComponentArea, connectStartDilateRadius, connectMaxDilateRadius, connectErodeRatio);
|
|
149
|
+
if (debug) {
|
|
150
|
+
debugLog("traceWithBounds:mask:after-connect", {
|
|
151
|
+
before: beforeConnectSummary,
|
|
152
|
+
appliedDilateRadius: connectResult.appliedDilateRadius,
|
|
153
|
+
appliedErodeRadius: connectResult.appliedErodeRadius,
|
|
154
|
+
reachedSingleComponent: connectResult.reachedSingleComponent,
|
|
155
|
+
after: {
|
|
156
|
+
rawContourCount: connectResult.rawContourCount,
|
|
157
|
+
selectedContourCount: connectResult.selectedContourCount,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
mask = connectResult.mask;
|
|
100
162
|
}
|
|
101
|
-
if (
|
|
102
|
-
const
|
|
103
|
-
|
|
163
|
+
if (debug) {
|
|
164
|
+
const afterConnectSummary = summarizeMaskContours(mask);
|
|
165
|
+
if (afterConnectSummary.selectedContourCount > 1) {
|
|
166
|
+
debugLog("traceWithBounds:mask:connect-warning", {
|
|
167
|
+
reason: "still-multi-component-after-connect-search",
|
|
168
|
+
summary: afterConnectSummary,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
104
171
|
}
|
|
105
172
|
const baseMask = mask;
|
|
106
173
|
const baseContoursRaw = this.traceAllContours(baseMask, paddedWidth, paddedHeight);
|
|
@@ -117,10 +184,10 @@ class ImageTracer {
|
|
|
117
184
|
};
|
|
118
185
|
}
|
|
119
186
|
const baseUnpaddedContours = baseContours
|
|
120
|
-
.map((contour) =>
|
|
187
|
+
.map((contour) => contour.map((p) => ({
|
|
121
188
|
x: p.x - padding,
|
|
122
189
|
y: p.y - padding,
|
|
123
|
-
}))
|
|
190
|
+
})))
|
|
124
191
|
.filter((contour) => contour.length > 2);
|
|
125
192
|
if (!baseUnpaddedContours.length) {
|
|
126
193
|
const w = options.scaleToWidth ?? width;
|
|
@@ -175,7 +242,7 @@ class ImageTracer {
|
|
|
175
242
|
};
|
|
176
243
|
}
|
|
177
244
|
let globalBounds = this.boundsFromPoints(this.flattenContours(expandedUnpaddedContours));
|
|
178
|
-
//
|
|
245
|
+
// Post-processing (Scale)
|
|
179
246
|
let finalContours = expandedUnpaddedContours;
|
|
180
247
|
if (options.scaleToWidth && options.scaleToHeight) {
|
|
181
248
|
finalContours = this.scaleContours(expandedUnpaddedContours, options.scaleToWidth, options.scaleToHeight, globalBounds);
|
|
@@ -183,8 +250,35 @@ class ImageTracer {
|
|
|
183
250
|
const baseScaledContours = this.scaleContours(baseUnpaddedContours, options.scaleToWidth, options.scaleToHeight, baseBounds);
|
|
184
251
|
baseBounds = this.boundsFromPoints(this.flattenContours(baseScaledContours));
|
|
185
252
|
}
|
|
186
|
-
|
|
187
|
-
|
|
253
|
+
if (expand > 0) {
|
|
254
|
+
const expectedExpandedBounds = {
|
|
255
|
+
x: baseBounds.x - expand,
|
|
256
|
+
y: baseBounds.y - expand,
|
|
257
|
+
width: baseBounds.width + expand * 2,
|
|
258
|
+
height: baseBounds.height + expand * 2,
|
|
259
|
+
};
|
|
260
|
+
if (expectedExpandedBounds.width > 0 &&
|
|
261
|
+
expectedExpandedBounds.height > 0 &&
|
|
262
|
+
globalBounds.width > 0 &&
|
|
263
|
+
globalBounds.height > 0) {
|
|
264
|
+
const shouldNormalizeExpandBounds = Math.abs(globalBounds.x - expectedExpandedBounds.x) > 1 ||
|
|
265
|
+
Math.abs(globalBounds.y - expectedExpandedBounds.y) > 1 ||
|
|
266
|
+
Math.abs(globalBounds.width - expectedExpandedBounds.width) > 1 ||
|
|
267
|
+
Math.abs(globalBounds.height - expectedExpandedBounds.height) > 1;
|
|
268
|
+
if (shouldNormalizeExpandBounds) {
|
|
269
|
+
const beforeNormalize = globalBounds;
|
|
270
|
+
finalContours = this.translateContours(this.scaleContours(finalContours, expectedExpandedBounds.width, expectedExpandedBounds.height, globalBounds), expectedExpandedBounds.x, expectedExpandedBounds.y);
|
|
271
|
+
globalBounds = this.boundsFromPoints(this.flattenContours(finalContours));
|
|
272
|
+
debugLog("traceWithBounds:expand-normalized", {
|
|
273
|
+
expand,
|
|
274
|
+
expectedExpandedBounds,
|
|
275
|
+
beforeNormalize,
|
|
276
|
+
afterNormalize: globalBounds,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Simplify and Generate SVG
|
|
188
282
|
debugLog("traceWithBounds:contours", {
|
|
189
283
|
baseContourCount: baseContoursRaw.length,
|
|
190
284
|
baseSelectedCount: baseContours.length,
|
|
@@ -200,16 +294,17 @@ class ImageTracer {
|
|
|
200
294
|
});
|
|
201
295
|
if (useSmoothing) {
|
|
202
296
|
return {
|
|
203
|
-
pathData: this.contoursToSVGPaper(finalContours,
|
|
297
|
+
pathData: this.contoursToSVGPaper(finalContours, simplifyTolerance),
|
|
204
298
|
baseBounds,
|
|
205
299
|
bounds: globalBounds,
|
|
206
300
|
};
|
|
207
301
|
}
|
|
208
302
|
else {
|
|
209
303
|
const simplifiedContours = finalContours
|
|
210
|
-
.map((points) => this.douglasPeucker(points,
|
|
304
|
+
.map((points) => this.douglasPeucker(points, simplifyTolerance))
|
|
211
305
|
.filter((points) => points.length > 2);
|
|
212
|
-
const pathData = this.contoursToSVG(simplifiedContours) ||
|
|
306
|
+
const pathData = this.contoursToSVG(simplifiedContours) ||
|
|
307
|
+
this.contoursToSVG(finalContours);
|
|
213
308
|
return {
|
|
214
309
|
pathData,
|
|
215
310
|
baseBounds,
|
|
@@ -251,7 +346,7 @@ class ImageTracer {
|
|
|
251
346
|
const xj = polygon[j].x;
|
|
252
347
|
const yj = polygon[j].y;
|
|
253
348
|
const intersects = yi > y !== yj > y &&
|
|
254
|
-
x < ((xj - xi) * (y - yi)) / (
|
|
349
|
+
x < ((xj - xi) * (y - yi)) / (yj - yi || Number.EPSILON) + xi;
|
|
255
350
|
if (intersects)
|
|
256
351
|
inside = !inside;
|
|
257
352
|
}
|
|
@@ -271,6 +366,84 @@ class ImageTracer {
|
|
|
271
366
|
}
|
|
272
367
|
return selected;
|
|
273
368
|
}
|
|
369
|
+
static summarizeAllContours(mask, width, height, minComponentArea) {
|
|
370
|
+
const raw = this.traceAllContours(mask, width, height);
|
|
371
|
+
const selected = this.selectContours(raw, "all", minComponentArea);
|
|
372
|
+
return {
|
|
373
|
+
rawCount: raw.length,
|
|
374
|
+
selectedCount: selected.length,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
static findForceConnectResult(sourceMask, width, height, minComponentArea, startDilateRadius, maxDilateRadius, erodeRatio) {
|
|
378
|
+
const initial = this.summarizeAllContours(sourceMask, width, height, minComponentArea);
|
|
379
|
+
if (initial.selectedCount <= 1) {
|
|
380
|
+
return {
|
|
381
|
+
mask: sourceMask,
|
|
382
|
+
appliedDilateRadius: 0,
|
|
383
|
+
appliedErodeRadius: 0,
|
|
384
|
+
reachedSingleComponent: true,
|
|
385
|
+
rawContourCount: initial.rawCount,
|
|
386
|
+
selectedContourCount: initial.selectedCount,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
const normalizedStart = Math.max(1, Math.floor(startDilateRadius));
|
|
390
|
+
const normalizedMax = Math.max(normalizedStart, Math.floor(maxDilateRadius));
|
|
391
|
+
const normalizedErodeRatio = Math.max(0, erodeRatio);
|
|
392
|
+
const evaluate = (dilateRadius) => {
|
|
393
|
+
const erodeRadius = Math.max(1, Math.floor(dilateRadius * normalizedErodeRatio));
|
|
394
|
+
let mask = sourceMask;
|
|
395
|
+
mask = (0, maskOps_1.circularMorphology)(mask, width, height, dilateRadius, "dilate");
|
|
396
|
+
mask = (0, maskOps_1.fillHoles)(mask, width, height);
|
|
397
|
+
mask = (0, maskOps_1.circularMorphology)(mask, width, height, erodeRadius, "erode");
|
|
398
|
+
mask = (0, maskOps_1.fillHoles)(mask, width, height);
|
|
399
|
+
const summary = this.summarizeAllContours(mask, width, height, minComponentArea);
|
|
400
|
+
return {
|
|
401
|
+
dilateRadius,
|
|
402
|
+
erodeRadius,
|
|
403
|
+
mask,
|
|
404
|
+
rawCount: summary.rawCount,
|
|
405
|
+
selectedCount: summary.selectedCount,
|
|
406
|
+
};
|
|
407
|
+
};
|
|
408
|
+
let low = normalizedStart - 1;
|
|
409
|
+
let high = normalizedStart;
|
|
410
|
+
let highResult = evaluate(high);
|
|
411
|
+
while (high < normalizedMax && highResult.selectedCount > 1) {
|
|
412
|
+
low = high;
|
|
413
|
+
high = Math.min(normalizedMax, Math.max(high + 1, Math.floor(high * 1.6)));
|
|
414
|
+
highResult = evaluate(high);
|
|
415
|
+
}
|
|
416
|
+
if (highResult.selectedCount > 1) {
|
|
417
|
+
return {
|
|
418
|
+
mask: highResult.mask,
|
|
419
|
+
appliedDilateRadius: highResult.dilateRadius,
|
|
420
|
+
appliedErodeRadius: highResult.erodeRadius,
|
|
421
|
+
reachedSingleComponent: false,
|
|
422
|
+
rawContourCount: highResult.rawCount,
|
|
423
|
+
selectedContourCount: highResult.selectedCount,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
let best = highResult;
|
|
427
|
+
while (low + 1 < high) {
|
|
428
|
+
const mid = Math.floor((low + high) / 2);
|
|
429
|
+
const midResult = evaluate(mid);
|
|
430
|
+
if (midResult.selectedCount <= 1) {
|
|
431
|
+
best = midResult;
|
|
432
|
+
high = mid;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
low = mid;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
mask: best.mask,
|
|
440
|
+
appliedDilateRadius: best.dilateRadius,
|
|
441
|
+
appliedErodeRadius: best.erodeRadius,
|
|
442
|
+
reachedSingleComponent: true,
|
|
443
|
+
rawContourCount: best.rawCount,
|
|
444
|
+
selectedContourCount: best.selectedCount,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
274
447
|
static selectContours(contours, mode, minComponentArea) {
|
|
275
448
|
if (!contours.length)
|
|
276
449
|
return [];
|
|
@@ -468,13 +641,11 @@ class ImageTracer {
|
|
|
468
641
|
static scaleContours(contours, targetWidth, targetHeight, bounds) {
|
|
469
642
|
return contours.map((points) => this.scalePoints(points, targetWidth, targetHeight, bounds));
|
|
470
643
|
}
|
|
471
|
-
static
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
y: Math.max(0, Math.min(maxY, p.y)),
|
|
477
|
-
}));
|
|
644
|
+
static translateContours(contours, offsetX, offsetY) {
|
|
645
|
+
return contours.map((points) => points.map((p) => ({
|
|
646
|
+
x: p.x + offsetX,
|
|
647
|
+
y: p.y + offsetY,
|
|
648
|
+
})));
|
|
478
649
|
}
|
|
479
650
|
static pointsToSVG(points) {
|
|
480
651
|
if (points.length === 0)
|
|
@@ -503,8 +674,8 @@ class ImageTracer {
|
|
|
503
674
|
this.ensurePaper();
|
|
504
675
|
// Create Path
|
|
505
676
|
const path = new paper_1.default.Path({
|
|
506
|
-
segments: points.map(p => [p.x, p.y]),
|
|
507
|
-
closed: true
|
|
677
|
+
segments: points.map((p) => [p.x, p.y]),
|
|
678
|
+
closed: true,
|
|
508
679
|
});
|
|
509
680
|
// Simplify
|
|
510
681
|
path.simplify(tolerance);
|