editmamei 0.20.0 → 0.22.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/NOTICES.md +17 -1
- package/README.md +8 -7
- package/dist/api/extendscript/_helpers.js +1 -1
- package/dist/bin/editmamei-core-darwin-arm64 +0 -0
- package/dist/bin/editmamei-core-darwin-x64 +0 -0
- package/dist/bin/editmamei-core-win-x64.exe +0 -0
- package/dist/cli/config.js +3 -3
- package/dist/cli/install.js +2 -2
- package/dist/core/server.js +7 -7
- package/dist/core/settings.js +2 -2
- package/dist/core/tool-groups.js +91 -82
- package/dist/core/tool-tiers.js +91 -82
- package/dist/detection/landmark-detection-client.js +8 -0
- package/dist/detection/mesh-eye-correction.js +126 -0
- package/dist/detection/sam-segmenter.js +83 -0
- package/dist/index.js +2 -0
- package/dist/license/entitlement.js +52 -0
- package/dist/modules/ce/index.js +4 -4
- package/dist/perception/grounding-anchors.js +37 -0
- package/dist/perception/grounding-corners.js +93 -0
- package/dist/perception/grounding-edge-trace.js +90 -0
- package/dist/perception/grounding-extrema.js +41 -0
- package/dist/perception/grounding-gate.js +154 -0
- package/dist/perception/grounding-geometry.js +132 -0
- package/dist/perception/grounding-grid.js +34 -0
- package/dist/perception/grounding-landmarks.js +40 -0
- package/dist/perception/grounding-locate.js +341 -0
- package/dist/perception/grounding-resolver.js +202 -0
- package/dist/perception/grounding-review-crop.js +85 -0
- package/dist/perception/grounding-warp.js +182 -0
- package/dist/perception/sky-mask-transfer.js +50 -18
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +3 -3
- package/dist/tools/brush-tools.js +111 -23
- package/dist/tools/channel-compose-tools.js +241 -0
- package/dist/tools/detection-tools.js +1 -1
- package/dist/tools/diagnostics-tools.js +1 -1
- package/dist/tools/document-tools.js +6 -6
- package/dist/tools/filter-tools.js +476 -19
- package/dist/tools/group-tools.js +10 -10
- package/dist/tools/guide-tools.js +4 -4
- package/dist/tools/history-tools.js +4 -4
- package/dist/tools/image-placement-tools.js +1 -1
- package/dist/tools/image-tools.js +62 -19
- package/dist/tools/inspect-tools.js +2 -2
- package/dist/tools/layer-ordering-tools.js +1 -1
- package/dist/tools/layer-properties-tools.js +42 -23
- package/dist/tools/layer-tools.js +10 -10
- package/dist/tools/layer-transform-tools.js +80 -41
- package/dist/tools/overview-tools.js +42 -42
- package/dist/tools/path-tools.js +53 -10
- package/dist/tools/portrait-tools.js +3 -3
- package/dist/tools/preview-tools.js +7 -82
- package/dist/tools/retouch-tools.js +93 -25
- package/dist/tools/scene-tools.js +13 -13
- package/dist/tools/selection-tools.js +319 -40
- package/dist/tools/shape-tools.js +216 -0
- package/dist/tools/text-tools.js +1 -1
- package/dist/tools/transform-canvas-tools.js +2 -2
- package/dist/tools/vector-mask-tools.js +23 -8
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/preview/coordinate-grid.js +0 -250
- package/dist/tools/object-tools.js +0 -193
- package/dist/tools/text-on-object-tools.js +0 -225
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { boundsOf, dist, minSignedGap, polygonArea, pointInBox, polylineIntersectsRegion, } from './grounding-geometry.js';
|
|
2
|
+
const inCanvas = (p, c) => p.x >= 0 && p.x <= c.width && p.y >= 0 && p.y <= c.height;
|
|
3
|
+
function gatePoint(spec) {
|
|
4
|
+
const { point: p, canvas, withinBounds, anchors } = spec;
|
|
5
|
+
const minSep = spec.minAnchorSeparation ?? 2;
|
|
6
|
+
const sep = anchors ? dist(anchors[0], anchors[1]) : undefined;
|
|
7
|
+
const measured = { x: p.x, y: p.y };
|
|
8
|
+
if (sep !== undefined)
|
|
9
|
+
measured.anchorSeparation = sep;
|
|
10
|
+
if (!inCanvas(p, canvas)) {
|
|
11
|
+
return {
|
|
12
|
+
pass: false,
|
|
13
|
+
reason: `point (${Math.round(p.x)},${Math.round(p.y)}) outside canvas ${canvas.width}x${canvas.height}`,
|
|
14
|
+
measured,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (sep !== undefined && sep < minSep) {
|
|
18
|
+
return {
|
|
19
|
+
pass: false,
|
|
20
|
+
reason: `degenerate relation — anchors ${sep.toFixed(1)}px apart (< ${minSep}px)`,
|
|
21
|
+
measured,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (withinBounds && !pointInBox(p, withinBounds)) {
|
|
25
|
+
return { pass: false, reason: 'point outside the expected region for this relation', measured };
|
|
26
|
+
}
|
|
27
|
+
return { pass: true, measured };
|
|
28
|
+
}
|
|
29
|
+
function gateOffsetCurve(spec) {
|
|
30
|
+
const { curve, source, side, requiredMargin, exclusion, canvas } = spec;
|
|
31
|
+
const minMargin = minSignedGap(curve, source, side);
|
|
32
|
+
const sideOk = minMargin > 0;
|
|
33
|
+
const intersects = exclusion ? polylineIntersectsRegion(curve, exclusion) : false;
|
|
34
|
+
const outOfCanvas = curve.some((p) => !inCanvas(p, canvas));
|
|
35
|
+
const measured = {
|
|
36
|
+
minMargin: Math.round(minMargin * 100) / 100,
|
|
37
|
+
requiredMargin,
|
|
38
|
+
side,
|
|
39
|
+
sideOk,
|
|
40
|
+
intersectsExclusion: intersects,
|
|
41
|
+
};
|
|
42
|
+
if (!sideOk) {
|
|
43
|
+
return {
|
|
44
|
+
pass: false,
|
|
45
|
+
reason: `offset-curve on the wrong side of the source (worst gap ${minMargin.toFixed(1)}px on '${side}')`,
|
|
46
|
+
measured,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (minMargin < requiredMargin) {
|
|
50
|
+
return {
|
|
51
|
+
pass: false,
|
|
52
|
+
reason: `offset-curve clearance ${minMargin.toFixed(1)}px < required ${requiredMargin}px (overlaps feature)`,
|
|
53
|
+
measured,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (intersects) {
|
|
57
|
+
return { pass: false, reason: 'offset-curve enters the exclusion region', measured };
|
|
58
|
+
}
|
|
59
|
+
if (outOfCanvas) {
|
|
60
|
+
return { pass: false, reason: 'offset-curve runs off canvas', measured };
|
|
61
|
+
}
|
|
62
|
+
return { pass: true, measured };
|
|
63
|
+
}
|
|
64
|
+
function gateEdge(spec) {
|
|
65
|
+
const { curve, confidences, minConfidence, canvas } = spec;
|
|
66
|
+
const minConf = confidences.length ? Math.min(...confidences) : 0;
|
|
67
|
+
const outOfCanvas = curve.some((p) => !inCanvas(p, canvas));
|
|
68
|
+
const measured = { minConfidence: minConf, required: minConfidence };
|
|
69
|
+
if (confidences.length !== curve.length) {
|
|
70
|
+
return {
|
|
71
|
+
pass: false,
|
|
72
|
+
reason: `confidence count ${confidences.length} != curve points ${curve.length}`,
|
|
73
|
+
measured,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (minConf < minConfidence) {
|
|
77
|
+
return {
|
|
78
|
+
pass: false,
|
|
79
|
+
reason: `edge confidence ${minConf} < required ${minConfidence} (weak/absent boundary)`,
|
|
80
|
+
measured,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (outOfCanvas) {
|
|
84
|
+
return { pass: false, reason: 'edge runs off canvas', measured };
|
|
85
|
+
}
|
|
86
|
+
return { pass: true, measured };
|
|
87
|
+
}
|
|
88
|
+
function gateCurve(spec) {
|
|
89
|
+
const { curve, canvas } = spec;
|
|
90
|
+
const minLength = spec.minLength ?? 2;
|
|
91
|
+
let length = 0;
|
|
92
|
+
for (let k = 0; k < curve.length - 1; k++)
|
|
93
|
+
length += Math.hypot(curve[k + 1].x - curve[k].x, curve[k + 1].y - curve[k].y);
|
|
94
|
+
const outOfCanvas = curve.some((p) => !inCanvas(p, canvas));
|
|
95
|
+
const measured = {
|
|
96
|
+
points: curve.length,
|
|
97
|
+
length: Math.round(length),
|
|
98
|
+
on_canvas: !outOfCanvas,
|
|
99
|
+
};
|
|
100
|
+
if (curve.length < 2)
|
|
101
|
+
return { pass: false, reason: 'curve has fewer than 2 points', measured };
|
|
102
|
+
if (length < minLength)
|
|
103
|
+
return {
|
|
104
|
+
pass: false,
|
|
105
|
+
reason: `curve length ${Math.round(length)} < required ${minLength} (degenerate)`,
|
|
106
|
+
measured,
|
|
107
|
+
};
|
|
108
|
+
if (outOfCanvas)
|
|
109
|
+
return { pass: false, reason: 'curve runs off canvas', measured };
|
|
110
|
+
return { pass: true, measured };
|
|
111
|
+
}
|
|
112
|
+
function gateRegion(spec) {
|
|
113
|
+
const { polygon, canvas } = spec;
|
|
114
|
+
const maxFrac = spec.maxAreaFraction ?? 0.9;
|
|
115
|
+
const area = polygonArea(polygon);
|
|
116
|
+
const frameArea = canvas.width * canvas.height;
|
|
117
|
+
const areaFraction = frameArea > 0 ? area / frameArea : 0;
|
|
118
|
+
const b = polygon.length ? boundsOf(polygon) : undefined;
|
|
119
|
+
const outOfCanvas = b
|
|
120
|
+
? b.left < 0 || b.top < 0 || b.right > canvas.width || b.bottom > canvas.height
|
|
121
|
+
: true;
|
|
122
|
+
const measured = {
|
|
123
|
+
area: Math.round(area),
|
|
124
|
+
areaFraction: Math.round(areaFraction * 1000) / 1000,
|
|
125
|
+
};
|
|
126
|
+
if (area <= 0) {
|
|
127
|
+
return { pass: false, reason: 'empty region', measured };
|
|
128
|
+
}
|
|
129
|
+
if (outOfCanvas) {
|
|
130
|
+
return { pass: false, reason: 'region extends off canvas', measured };
|
|
131
|
+
}
|
|
132
|
+
if (areaFraction > maxFrac) {
|
|
133
|
+
return {
|
|
134
|
+
pass: false,
|
|
135
|
+
reason: `region ${(areaFraction * 100).toFixed(0)}% of frame — implausibly large for the relation`,
|
|
136
|
+
measured,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return { pass: true, measured };
|
|
140
|
+
}
|
|
141
|
+
export function runGate(spec) {
|
|
142
|
+
switch (spec.target) {
|
|
143
|
+
case 'point':
|
|
144
|
+
return gatePoint(spec);
|
|
145
|
+
case 'region':
|
|
146
|
+
return gateRegion(spec);
|
|
147
|
+
case 'path':
|
|
148
|
+
return spec.kind === 'offset-curve'
|
|
149
|
+
? gateOffsetCurve(spec)
|
|
150
|
+
: spec.kind === 'edge'
|
|
151
|
+
? gateEdge(spec)
|
|
152
|
+
: gateCurve(spec);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export function dist(a, b) {
|
|
2
|
+
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
3
|
+
}
|
|
4
|
+
export function boundsOf(poly) {
|
|
5
|
+
if (poly.length === 0)
|
|
6
|
+
throw new Error('boundsOf: empty polyline');
|
|
7
|
+
let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
|
|
8
|
+
for (const p of poly) {
|
|
9
|
+
if (p.x < left)
|
|
10
|
+
left = p.x;
|
|
11
|
+
if (p.x > right)
|
|
12
|
+
right = p.x;
|
|
13
|
+
if (p.y < top)
|
|
14
|
+
top = p.y;
|
|
15
|
+
if (p.y > bottom)
|
|
16
|
+
bottom = p.y;
|
|
17
|
+
}
|
|
18
|
+
return { left, top, right, bottom };
|
|
19
|
+
}
|
|
20
|
+
export function interpolateCurveAt(curve, at, axis) {
|
|
21
|
+
if (curve.length === 0)
|
|
22
|
+
throw new Error('interpolateCurveAt: empty curve');
|
|
23
|
+
const other = axis === 'x' ? 'y' : 'x';
|
|
24
|
+
if (curve.length === 1)
|
|
25
|
+
return curve[0][other];
|
|
26
|
+
const sorted = [...curve].sort((a, b) => a[axis] - b[axis]);
|
|
27
|
+
if (at <= sorted[0][axis])
|
|
28
|
+
return sorted[0][other];
|
|
29
|
+
const last = sorted[sorted.length - 1];
|
|
30
|
+
if (at >= last[axis])
|
|
31
|
+
return last[other];
|
|
32
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
33
|
+
const a = sorted[i], b = sorted[i + 1];
|
|
34
|
+
if (at >= a[axis] && at <= b[axis]) {
|
|
35
|
+
const span = b[axis] - a[axis];
|
|
36
|
+
if (span === 0)
|
|
37
|
+
return a[other];
|
|
38
|
+
const t = (at - a[axis]) / span;
|
|
39
|
+
return a[other] + t * (b[other] - a[other]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return last[other];
|
|
43
|
+
}
|
|
44
|
+
export function signedGapToCurve(point, curve, side) {
|
|
45
|
+
switch (side) {
|
|
46
|
+
case 'down':
|
|
47
|
+
return point.y - interpolateCurveAt(curve, point.x, 'x');
|
|
48
|
+
case 'up':
|
|
49
|
+
return interpolateCurveAt(curve, point.x, 'x') - point.y;
|
|
50
|
+
case 'right':
|
|
51
|
+
return point.x - interpolateCurveAt(curve, point.y, 'y');
|
|
52
|
+
case 'left':
|
|
53
|
+
return interpolateCurveAt(curve, point.y, 'y') - point.x;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function minSignedGap(offset, source, side) {
|
|
57
|
+
if (offset.length === 0)
|
|
58
|
+
throw new Error('minSignedGap: empty offset curve');
|
|
59
|
+
let min = Infinity;
|
|
60
|
+
for (const p of offset) {
|
|
61
|
+
const g = signedGapToCurve(p, source, side);
|
|
62
|
+
if (g < min)
|
|
63
|
+
min = g;
|
|
64
|
+
}
|
|
65
|
+
return min;
|
|
66
|
+
}
|
|
67
|
+
export function polygonArea(poly) {
|
|
68
|
+
if (poly.length < 3)
|
|
69
|
+
return 0;
|
|
70
|
+
let sum = 0;
|
|
71
|
+
for (let i = 0; i < poly.length; i++) {
|
|
72
|
+
const a = poly[i], b = poly[(i + 1) % poly.length];
|
|
73
|
+
sum += a.x * b.y - b.x * a.y;
|
|
74
|
+
}
|
|
75
|
+
return Math.abs(sum) / 2;
|
|
76
|
+
}
|
|
77
|
+
export function pointInBox(p, box) {
|
|
78
|
+
return p.x >= box.left && p.x <= box.right && p.y >= box.top && p.y <= box.bottom;
|
|
79
|
+
}
|
|
80
|
+
export function pointInPolygon(p, poly) {
|
|
81
|
+
let inside = false;
|
|
82
|
+
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
83
|
+
const a = poly[i], b = poly[j];
|
|
84
|
+
const intersects = a.y > p.y !== b.y > p.y && p.x < ((b.x - a.x) * (p.y - a.y)) / (b.y - a.y) + a.x;
|
|
85
|
+
if (intersects)
|
|
86
|
+
inside = !inside;
|
|
87
|
+
}
|
|
88
|
+
return inside;
|
|
89
|
+
}
|
|
90
|
+
function pointInRegion(p, region) {
|
|
91
|
+
return region.kind === 'box' ? pointInBox(p, region.box) : pointInPolygon(p, region.polygon);
|
|
92
|
+
}
|
|
93
|
+
export function segmentsIntersect(p1, p2, p3, p4) {
|
|
94
|
+
const o = (a, b, c) => Math.sign((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x));
|
|
95
|
+
const o1 = o(p1, p2, p3), o2 = o(p1, p2, p4), o3 = o(p3, p4, p1), o4 = o(p3, p4, p2);
|
|
96
|
+
return o1 !== o2 && o3 !== o4;
|
|
97
|
+
}
|
|
98
|
+
function regionEdges(region) {
|
|
99
|
+
if (region.kind === 'box') {
|
|
100
|
+
const { left: l, top: t, right: r, bottom: b } = region.box;
|
|
101
|
+
const c = [
|
|
102
|
+
{ x: l, y: t },
|
|
103
|
+
{ x: r, y: t },
|
|
104
|
+
{ x: r, y: b },
|
|
105
|
+
{ x: l, y: b },
|
|
106
|
+
];
|
|
107
|
+
return [
|
|
108
|
+
[c[0], c[1]],
|
|
109
|
+
[c[1], c[2]],
|
|
110
|
+
[c[2], c[3]],
|
|
111
|
+
[c[3], c[0]],
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
const poly = region.polygon;
|
|
115
|
+
const edges = [];
|
|
116
|
+
for (let i = 0; i < poly.length; i++)
|
|
117
|
+
edges.push([poly[i], poly[(i + 1) % poly.length]]);
|
|
118
|
+
return edges;
|
|
119
|
+
}
|
|
120
|
+
export function polylineIntersectsRegion(poly, region) {
|
|
121
|
+
for (const p of poly)
|
|
122
|
+
if (pointInRegion(p, region))
|
|
123
|
+
return true;
|
|
124
|
+
const edges = regionEdges(region);
|
|
125
|
+
for (let i = 0; i < poly.length - 1; i++) {
|
|
126
|
+
for (const [e1, e2] of edges) {
|
|
127
|
+
if (segmentsIntersect(poly[i], poly[i + 1], e1, e2))
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const NAMED = {
|
|
2
|
+
center: [0.5, 0.5],
|
|
3
|
+
'upper-left': [1 / 3, 1 / 3],
|
|
4
|
+
'upper-center': [0.5, 1 / 3],
|
|
5
|
+
'upper-right': [2 / 3, 1 / 3],
|
|
6
|
+
'center-left': [1 / 3, 0.5],
|
|
7
|
+
'center-right': [2 / 3, 0.5],
|
|
8
|
+
'lower-left': [1 / 3, 2 / 3],
|
|
9
|
+
'lower-center': [0.5, 2 / 3],
|
|
10
|
+
'lower-right': [2 / 3, 2 / 3],
|
|
11
|
+
};
|
|
12
|
+
export function frameBox(width, height) {
|
|
13
|
+
return { left: 0, top: 0, right: width, bottom: height };
|
|
14
|
+
}
|
|
15
|
+
export function gridPoint(box, name) {
|
|
16
|
+
let fx, fy;
|
|
17
|
+
if (name.startsWith('frac:')) {
|
|
18
|
+
const [a, b] = name.slice('frac:'.length).split(',');
|
|
19
|
+
fx = Number(a);
|
|
20
|
+
fy = Number(b);
|
|
21
|
+
if (Number.isNaN(fx) || Number.isNaN(fy))
|
|
22
|
+
throw new Error(`gridPoint: malformed '${name}' (want frac:fx,fy)`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const f = NAMED[name];
|
|
26
|
+
if (!f)
|
|
27
|
+
throw new Error(`gridPoint: unknown position '${name}' (want ${Object.keys(NAMED).join('|')} or frac:fx,fy)`);
|
|
28
|
+
[fx, fy] = f;
|
|
29
|
+
}
|
|
30
|
+
return { x: box.left + fx * (box.right - box.left), y: box.top + fy * (box.bottom - box.top) };
|
|
31
|
+
}
|
|
32
|
+
export function gridAnchor(box, name) {
|
|
33
|
+
return { kind: 'point', point: gridPoint(box, name) };
|
|
34
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { LANDMARK_GROUPS, LANDMARK_ANCHORS, LANDMARK_COUNT } from '../detection/landmark-spec.js';
|
|
2
|
+
const LEFT_EYE_LOWER = [263, 249, 390, 373, 374, 380, 381, 382, 362];
|
|
3
|
+
const RIGHT_EYE_LOWER = [33, 7, 163, 144, 145, 153, 154, 155, 133];
|
|
4
|
+
const LANDMARK_FEATURES = {
|
|
5
|
+
left_eye_lower: { as: 'polyline', indices: LEFT_EYE_LOWER },
|
|
6
|
+
right_eye_lower: { as: 'polyline', indices: RIGHT_EYE_LOWER },
|
|
7
|
+
left_eye: { as: 'polyline', indices: LANDMARK_GROUPS.leftEye },
|
|
8
|
+
right_eye: { as: 'polyline', indices: LANDMARK_GROUPS.rightEye },
|
|
9
|
+
lips_outer: { as: 'polyline', indices: LANDMARK_GROUPS.lipsOuter },
|
|
10
|
+
nose_tip: { as: 'point', index: LANDMARK_ANCHORS.noseTip },
|
|
11
|
+
chin: { as: 'point', index: LANDMARK_ANCHORS.chin },
|
|
12
|
+
between_eyes: { as: 'point', index: LANDMARK_ANCHORS.betweenEyes },
|
|
13
|
+
upper_lip_top: { as: 'point', index: LANDMARK_ANCHORS.upperLipTop },
|
|
14
|
+
lower_lip_bottom: { as: 'point', index: LANDMARK_ANCHORS.lowerLipBottom },
|
|
15
|
+
};
|
|
16
|
+
export const LANDMARK_FEATURE_NAMES = Object.keys(LANDMARK_FEATURES);
|
|
17
|
+
export function isLandmarkFeature(feature) {
|
|
18
|
+
return feature in LANDMARK_FEATURES;
|
|
19
|
+
}
|
|
20
|
+
export function landmarkAnchor(det, sel) {
|
|
21
|
+
const spec = LANDMARK_FEATURES[sel.feature];
|
|
22
|
+
if (!spec)
|
|
23
|
+
return null;
|
|
24
|
+
const meshed = (det.faces ?? []).filter((f) => f.features && f.features.points.length >= LANDMARK_COUNT);
|
|
25
|
+
const face = meshed[sel.face ?? 0];
|
|
26
|
+
if (!face || !face.features)
|
|
27
|
+
return null;
|
|
28
|
+
const pts = face.features.points;
|
|
29
|
+
if (spec.as === 'point') {
|
|
30
|
+
const p = pts[spec.index];
|
|
31
|
+
return p ? { kind: 'point', point: { x: p.x, y: p.y } } : null;
|
|
32
|
+
}
|
|
33
|
+
const polyline = [];
|
|
34
|
+
for (const i of spec.indices) {
|
|
35
|
+
const p = pts[i];
|
|
36
|
+
if (p)
|
|
37
|
+
polyline.push({ x: p.x, y: p.y });
|
|
38
|
+
}
|
|
39
|
+
return polyline.length >= 2 ? { kind: 'polyline', polyline } : null;
|
|
40
|
+
}
|