@grida/hud 0.1.0 → 0.2.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/README.md +185 -19
- package/dist/cursor-BFGUuD2M.d.mts +64 -0
- package/dist/cursor-BieMVb71.mjs +57 -0
- package/dist/cursor-CIYvFshz.d.ts +64 -0
- package/dist/cursor-DsP9qtN2.js +80 -0
- package/dist/cursors/index.d.mts +98 -0
- package/dist/cursors/index.d.ts +98 -0
- package/dist/cursors/index.js +188 -0
- package/dist/cursors/index.mjs +185 -0
- package/dist/{index-CBqCh-ZM.d.mts → index-Cp0X4SV7.d.ts} +385 -172
- package/dist/{index-DRBeSiI2.d.ts → index-DhGdcuQz.d.mts} +385 -172
- package/dist/index.d.mts +106 -2
- package/dist/index.d.ts +106 -2
- package/dist/index.js +13 -1
- package/dist/index.mjs +3 -2
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +8 -3
- package/dist/react.mjs +8 -3
- package/dist/{surface-hUEeEVdL.mjs → surface-BvMmXoEl.mjs} +910 -247
- package/dist/{surface-CNlBaEXn.js → surface-ofSNTJ8H.js} +965 -248
- package/package.json +8 -2
|
@@ -20,9 +20,42 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
20
20
|
enumerable: true
|
|
21
21
|
}) : target, mod));
|
|
22
22
|
//#endregion
|
|
23
|
+
const require_cursor = require("./cursor-DsP9qtN2.js");
|
|
23
24
|
let _grida_cmath__measurement = require("@grida/cmath/_measurement");
|
|
24
25
|
let _grida_cmath = require("@grida/cmath");
|
|
25
26
|
_grida_cmath = __toESM(_grida_cmath);
|
|
27
|
+
//#region primitives/pixel-grid.ts
|
|
28
|
+
const DEFAULT_PIXEL_GRID_COLOR = "rgba(150, 150, 150, 0.15)";
|
|
29
|
+
const DEFAULT_PIXEL_GRID_STEPS = [1, 1];
|
|
30
|
+
function drawPixelGrid(p) {
|
|
31
|
+
const { ctx, transform, width, height, dpr, color = DEFAULT_PIXEL_GRID_COLOR, steps = DEFAULT_PIXEL_GRID_STEPS } = p;
|
|
32
|
+
ctx.save();
|
|
33
|
+
const [[sx, , tx], [, sy, ty]] = transform;
|
|
34
|
+
ctx.setTransform(sx * dpr, 0, 0, sy * dpr, tx * dpr, ty * dpr);
|
|
35
|
+
ctx.strokeStyle = color;
|
|
36
|
+
ctx.lineWidth = 1 / Math.max(Math.abs(sx * dpr), Math.abs(sy * dpr));
|
|
37
|
+
const minUserX = (0 - tx * dpr) / (sx * dpr);
|
|
38
|
+
const maxUserX = (width * dpr - tx * dpr) / (sx * dpr);
|
|
39
|
+
const minUserY = (0 - ty * dpr) / (sy * dpr);
|
|
40
|
+
const maxUserY = (height * dpr - ty * dpr) / (sy * dpr);
|
|
41
|
+
const [stepX, stepY] = steps;
|
|
42
|
+
const startX = Math.floor(minUserX / stepX) * stepX - 2 * stepX;
|
|
43
|
+
const endX = Math.ceil(maxUserX / stepX) * stepX + 2 * stepX;
|
|
44
|
+
const startY = Math.floor(minUserY / stepY) * stepY - 2 * stepY;
|
|
45
|
+
const endY = Math.ceil(maxUserY / stepY) * stepY + 2 * stepY;
|
|
46
|
+
ctx.beginPath();
|
|
47
|
+
for (let x = startX; x <= endX; x += stepX) {
|
|
48
|
+
ctx.moveTo(x, startY);
|
|
49
|
+
ctx.lineTo(x, endY);
|
|
50
|
+
}
|
|
51
|
+
for (let y = startY; y <= endY; y += stepY) {
|
|
52
|
+
ctx.moveTo(startX, y);
|
|
53
|
+
ctx.lineTo(endX, y);
|
|
54
|
+
}
|
|
55
|
+
ctx.stroke();
|
|
56
|
+
ctx.restore();
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
26
59
|
//#region primitives/canvas.ts
|
|
27
60
|
const DEFAULT_COLOR = "#f44336";
|
|
28
61
|
const DEFAULT_LABEL_FG = "#ffffff";
|
|
@@ -60,6 +93,7 @@ var HUDCanvas = class {
|
|
|
60
93
|
]];
|
|
61
94
|
this.width = 0;
|
|
62
95
|
this.height = 0;
|
|
96
|
+
this.pixelGrid = null;
|
|
63
97
|
this.ctx = canvas.getContext("2d");
|
|
64
98
|
this.dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
|
|
65
99
|
this.color = options?.color ?? DEFAULT_COLOR;
|
|
@@ -82,6 +116,31 @@ var HUDCanvas = class {
|
|
|
82
116
|
this.transform = transform;
|
|
83
117
|
}
|
|
84
118
|
/**
|
|
119
|
+
* Configure the back-most pixel-grid layer. Pass `null` to disable.
|
|
120
|
+
* Drawn before any HUD primitive, gated by `zoomThreshold`. See
|
|
121
|
+
* `PixelGridConfig.transform` for the two-transform contract.
|
|
122
|
+
*/
|
|
123
|
+
setPixelGrid(config) {
|
|
124
|
+
if (config === null) {
|
|
125
|
+
this.pixelGrid = null;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.pixelGrid = {
|
|
129
|
+
...config,
|
|
130
|
+
transform: config.transform ?? this.pixelGrid?.transform
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Update only the pixel grid's transform, without replacing the rest of
|
|
135
|
+
* the config. Cheap to call per camera tick.
|
|
136
|
+
*/
|
|
137
|
+
setPixelGridTransform(transform) {
|
|
138
|
+
if (this.pixelGrid) this.pixelGrid = {
|
|
139
|
+
...this.pixelGrid,
|
|
140
|
+
transform
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
85
144
|
* Clear the canvas and draw all primitives in `commands`.
|
|
86
145
|
* Pass `undefined` to clear without drawing (e.g. when no overlay is active).
|
|
87
146
|
*/
|
|
@@ -89,6 +148,17 @@ var HUDCanvas = class {
|
|
|
89
148
|
const ctx = this.ctx;
|
|
90
149
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
91
150
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
151
|
+
const pg = this.pixelGrid;
|
|
152
|
+
const pgTransform = pg?.transform ?? this.transform;
|
|
153
|
+
if (pg?.enabled && pgTransform[0][0] > pg.zoomThreshold) drawPixelGrid({
|
|
154
|
+
ctx,
|
|
155
|
+
transform: pgTransform,
|
|
156
|
+
width: this.width,
|
|
157
|
+
height: this.height,
|
|
158
|
+
dpr: this.dpr,
|
|
159
|
+
color: pg.color,
|
|
160
|
+
steps: pg.steps
|
|
161
|
+
});
|
|
92
162
|
if (!commands) return;
|
|
93
163
|
const { lines, rules, points, rects, polylines, screenRects } = commands;
|
|
94
164
|
if (rules && rules.length > 0) this.drawRules(rules);
|
|
@@ -114,12 +184,12 @@ var HUDCanvas = class {
|
|
|
114
184
|
drawRules(rules) {
|
|
115
185
|
const ctx = this.ctx;
|
|
116
186
|
this.applyScreenTransform();
|
|
117
|
-
ctx.strokeStyle = this.color;
|
|
118
187
|
ctx.lineWidth = DEFAULT_LINE_WIDTH;
|
|
119
|
-
for (const
|
|
120
|
-
const screenOffset = this.deltaToScreen(offset, axis);
|
|
188
|
+
for (const rule of rules) {
|
|
189
|
+
const screenOffset = this.deltaToScreen(rule.offset, rule.axis);
|
|
190
|
+
ctx.strokeStyle = rule.color ?? this.color;
|
|
121
191
|
ctx.beginPath();
|
|
122
|
-
if (axis === "x") {
|
|
192
|
+
if (rule.axis === "x") {
|
|
123
193
|
ctx.moveTo(screenOffset, 0);
|
|
124
194
|
ctx.lineTo(screenOffset, this.height);
|
|
125
195
|
} else {
|
|
@@ -173,17 +243,29 @@ var HUDCanvas = class {
|
|
|
173
243
|
const midY = (line.y1 + line.y2) / 2;
|
|
174
244
|
const lx = sx * midX + tx;
|
|
175
245
|
const ly = sy * midY + ty;
|
|
246
|
+
const angle = line.labelAngle ?? 0;
|
|
176
247
|
const isVertical = Math.abs(line.x2 - line.x1) < Math.abs(line.y2 - line.y1);
|
|
177
|
-
const
|
|
178
|
-
const
|
|
248
|
+
const baseOffsetX = isVertical ? LABEL_OFFSET : 0;
|
|
249
|
+
const baseOffsetY = isVertical ? 0 : LABEL_OFFSET;
|
|
250
|
+
const cos = Math.cos(angle);
|
|
251
|
+
const sin = Math.sin(angle);
|
|
252
|
+
const labelX = lx + baseOffsetX * cos - baseOffsetY * sin;
|
|
253
|
+
const labelY = ly + baseOffsetX * sin + baseOffsetY * cos;
|
|
179
254
|
const tw = ctx.measureText(line.label).width + LABEL_PADDING_X * 2;
|
|
180
255
|
const th = LABEL_FONT_HEIGHT + LABEL_PADDING_Y * 2;
|
|
256
|
+
if (angle !== 0) {
|
|
257
|
+
ctx.save();
|
|
258
|
+
ctx.translate(labelX, labelY);
|
|
259
|
+
ctx.rotate(angle);
|
|
260
|
+
ctx.translate(-labelX, -labelY);
|
|
261
|
+
}
|
|
181
262
|
ctx.fillStyle = line.color ?? this.color;
|
|
182
263
|
ctx.beginPath();
|
|
183
264
|
ctx.roundRect(labelX - tw / 2, labelY - th / 2, tw, th, LABEL_BORDER_RADIUS);
|
|
184
265
|
ctx.fill();
|
|
185
266
|
ctx.fillStyle = DEFAULT_LABEL_FG;
|
|
186
267
|
ctx.fillText(line.label, labelX, labelY);
|
|
268
|
+
if (angle !== 0) ctx.restore();
|
|
187
269
|
}
|
|
188
270
|
}
|
|
189
271
|
drawRects(rects) {
|
|
@@ -246,20 +328,29 @@ var HUDCanvas = class {
|
|
|
246
328
|
drawPoints(points) {
|
|
247
329
|
const ctx = this.ctx;
|
|
248
330
|
this.applyScreenTransform();
|
|
249
|
-
ctx.strokeStyle = this.color;
|
|
250
331
|
ctx.lineWidth = DEFAULT_LINE_WIDTH;
|
|
251
332
|
const half = CROSSHAIR_SIZE / 2;
|
|
252
333
|
const [[sx, , tx], [, sy, ty]] = this.transform;
|
|
253
|
-
|
|
254
|
-
for (const
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
334
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
335
|
+
for (const p of points) {
|
|
336
|
+
const c = p.color ?? this.color;
|
|
337
|
+
const arr = buckets.get(c);
|
|
338
|
+
if (arr) arr.push(p);
|
|
339
|
+
else buckets.set(c, [p]);
|
|
340
|
+
}
|
|
341
|
+
for (const [color, group] of buckets) {
|
|
342
|
+
ctx.strokeStyle = color;
|
|
343
|
+
ctx.beginPath();
|
|
344
|
+
for (const p of group) {
|
|
345
|
+
const scrX = sx * p.x + tx;
|
|
346
|
+
const scrY = sy * p.y + ty;
|
|
347
|
+
ctx.moveTo(scrX - half, scrY - half);
|
|
348
|
+
ctx.lineTo(scrX + half, scrY + half);
|
|
349
|
+
ctx.moveTo(scrX + half, scrY - half);
|
|
350
|
+
ctx.lineTo(scrX - half, scrY + half);
|
|
351
|
+
}
|
|
352
|
+
ctx.stroke();
|
|
261
353
|
}
|
|
262
|
-
ctx.stroke();
|
|
263
354
|
}
|
|
264
355
|
/**
|
|
265
356
|
* Draw rects whose **size is in screen-space** but whose **anchor is in
|
|
@@ -306,6 +397,15 @@ var HUDCanvas = class {
|
|
|
306
397
|
}
|
|
307
398
|
const doFill = r.fill !== false;
|
|
308
399
|
const doStroke = r.stroke !== false;
|
|
400
|
+
const angle = r.angle ?? 0;
|
|
401
|
+
if (angle !== 0) {
|
|
402
|
+
const cx = x + w / 2;
|
|
403
|
+
const cy = y + h / 2;
|
|
404
|
+
ctx.save();
|
|
405
|
+
ctx.translate(cx, cy);
|
|
406
|
+
ctx.rotate(angle);
|
|
407
|
+
ctx.translate(-cx, -cy);
|
|
408
|
+
}
|
|
309
409
|
if (doFill) {
|
|
310
410
|
ctx.fillStyle = r.fillColor ?? this.color;
|
|
311
411
|
ctx.fillRect(x, y, w, h);
|
|
@@ -314,28 +414,67 @@ var HUDCanvas = class {
|
|
|
314
414
|
ctx.strokeStyle = r.strokeColor ?? this.color;
|
|
315
415
|
ctx.strokeRect(x, y, w, h);
|
|
316
416
|
}
|
|
417
|
+
if (angle !== 0) ctx.restore();
|
|
317
418
|
}
|
|
318
419
|
}
|
|
319
420
|
};
|
|
320
421
|
//#endregion
|
|
422
|
+
//#region primitives/draw.ts
|
|
423
|
+
/**
|
|
424
|
+
* Filter a draw command list by semantic group.
|
|
425
|
+
*
|
|
426
|
+
* Ungrouped primitives are always kept. The function is intentionally shallow:
|
|
427
|
+
* primitives are immutable command objects on the hot draw path, so preserving
|
|
428
|
+
* object identity keeps this as a visibility pass rather than a rewrite.
|
|
429
|
+
*/
|
|
430
|
+
function filterHUDDrawByGroup(draw, filter) {
|
|
431
|
+
if (!draw) return void 0;
|
|
432
|
+
const hidden = new Set(filter.hidden ?? []);
|
|
433
|
+
if (hidden.size === 0) return draw;
|
|
434
|
+
const out = {};
|
|
435
|
+
out.lines = keepVisible(draw.lines, hidden);
|
|
436
|
+
out.rules = keepVisible(draw.rules, hidden);
|
|
437
|
+
out.points = keepVisible(draw.points, hidden);
|
|
438
|
+
out.rects = keepVisible(draw.rects, hidden);
|
|
439
|
+
out.polylines = keepVisible(draw.polylines, hidden);
|
|
440
|
+
out.screenRects = keepVisible(draw.screenRects, hidden);
|
|
441
|
+
return hasAny(out) ? out : void 0;
|
|
442
|
+
}
|
|
443
|
+
function keepVisible(items, hidden) {
|
|
444
|
+
if (!items || items.length === 0) return void 0;
|
|
445
|
+
const kept = items.filter((item) => !item.group || !hidden.has(item.group));
|
|
446
|
+
return kept.length > 0 ? kept : void 0;
|
|
447
|
+
}
|
|
448
|
+
function hasAny(draw) {
|
|
449
|
+
return (draw.lines?.length ?? 0) > 0 || (draw.rules?.length ?? 0) > 0 || (draw.points?.length ?? 0) > 0 || (draw.rects?.length ?? 0) > 0 || (draw.polylines?.length ?? 0) > 0 || (draw.screenRects?.length ?? 0) > 0;
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
321
452
|
//#region primitives/snap-guide.ts
|
|
322
453
|
/**
|
|
323
454
|
* Convert a `guide.SnapGuide` (the output of `guide.plot()`) into a
|
|
324
455
|
* generic {@link HUDDraw} command list.
|
|
325
456
|
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
457
|
+
* `color`, when supplied, is applied as the per-item stroke override
|
|
458
|
+
* for every emitted line, rule, and point. When absent, the HUD
|
|
459
|
+
* canvas's current color is used.
|
|
329
460
|
*/
|
|
330
|
-
function snapGuideToHUDDraw(sg) {
|
|
461
|
+
function snapGuideToHUDDraw(sg, color) {
|
|
331
462
|
if (!sg) return void 0;
|
|
332
463
|
return {
|
|
333
|
-
lines: sg.lines
|
|
464
|
+
lines: sg.lines.map((l) => ({
|
|
465
|
+
...l,
|
|
466
|
+
color
|
|
467
|
+
})),
|
|
334
468
|
rules: sg.rules.map(([axis, offset]) => ({
|
|
335
469
|
axis,
|
|
336
|
-
offset
|
|
470
|
+
offset,
|
|
471
|
+
color
|
|
337
472
|
})),
|
|
338
|
-
points: sg.points
|
|
473
|
+
points: sg.points.map(([x, y]) => ({
|
|
474
|
+
x,
|
|
475
|
+
y,
|
|
476
|
+
color
|
|
477
|
+
}))
|
|
339
478
|
};
|
|
340
479
|
}
|
|
341
480
|
//#endregion
|
|
@@ -472,15 +611,53 @@ function rectFromPoints(a, b) {
|
|
|
472
611
|
return _grida_cmath.default.rect.fromPoints([a, b]);
|
|
473
612
|
}
|
|
474
613
|
/**
|
|
475
|
-
* Apply a resize
|
|
614
|
+
* Apply a resize-handle drag to a `SelectionShape` and return the new shape.
|
|
476
615
|
*
|
|
477
|
-
* `dx, dy` is the total drag delta in document-space
|
|
616
|
+
* `dx, dy` is the total drag delta in **document-space**, measured from
|
|
478
617
|
* `anchor_doc` (the pointer-down point) to the current pointer.
|
|
479
618
|
*
|
|
619
|
+
* - For `kind: "rect"` shapes, the delta applies directly to the doc-space
|
|
620
|
+
* bbox. Same math as the legacy `Rect`-only `applyResize`; no behavior
|
|
621
|
+
* change for axis-aligned hosts.
|
|
622
|
+
* - For `kind: "transformed"` shapes, the doc-space delta is rotated into
|
|
623
|
+
* the shape's **local** frame via the matrix's linear part (the rotation
|
|
624
|
+
* component, translation dropped), then applied to `local`. The matrix
|
|
625
|
+
* itself is preserved — only `local.x/y/width/height` change. Net effect:
|
|
626
|
+
* dragging a corner of a rotated rect extends the artwork along its
|
|
627
|
+
* rotation axis, not along world axes.
|
|
628
|
+
* - For `kind: "line"` and `kind: "unresolved"` the shape is returned
|
|
629
|
+
* unchanged (lines have endpoint-knob gestures, not corner-resize).
|
|
630
|
+
*
|
|
480
631
|
* No constraints: width/height can go negative — host is responsible for
|
|
481
632
|
* normalizing if it cares (most callers clamp to a min-size).
|
|
482
633
|
*/
|
|
483
634
|
function applyResize(initial, direction, dx, dy) {
|
|
635
|
+
if (initial.kind === "rect") return {
|
|
636
|
+
kind: "rect",
|
|
637
|
+
rect: applyResizeRect(initial.rect, direction, dx, dy)
|
|
638
|
+
};
|
|
639
|
+
if (initial.kind === "transformed") {
|
|
640
|
+
const m = initial.matrix;
|
|
641
|
+
const linear = [[
|
|
642
|
+
m[0][0],
|
|
643
|
+
m[0][1],
|
|
644
|
+
0
|
|
645
|
+
], [
|
|
646
|
+
m[1][0],
|
|
647
|
+
m[1][1],
|
|
648
|
+
0
|
|
649
|
+
]];
|
|
650
|
+
const inv_linear = _grida_cmath.default.transform.invert(linear);
|
|
651
|
+
const [ldx, ldy] = _grida_cmath.default.vector2.transform([dx, dy], inv_linear);
|
|
652
|
+
return {
|
|
653
|
+
kind: "transformed",
|
|
654
|
+
local: applyResizeRect(initial.local, direction, ldx, ldy),
|
|
655
|
+
matrix: m
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
return initial;
|
|
659
|
+
}
|
|
660
|
+
function applyResizeRect(initial, direction, dx, dy) {
|
|
484
661
|
let { x, y, width, height } = initial;
|
|
485
662
|
switch (direction) {
|
|
486
663
|
case "n":
|
|
@@ -526,16 +703,19 @@ function applyResize(initial, direction, dx, dy) {
|
|
|
526
703
|
};
|
|
527
704
|
}
|
|
528
705
|
//#endregion
|
|
529
|
-
//#region event/
|
|
530
|
-
/**
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
return
|
|
706
|
+
//#region event/shape.ts
|
|
707
|
+
/**
|
|
708
|
+
* Compute the axis-aligned bounding box of a `SelectionShape` in doc-space.
|
|
709
|
+
* Used for layout math the chrome builder needs (e.g. handle positions).
|
|
710
|
+
*
|
|
711
|
+
* Throws on `kind: "unresolved"` — the chrome builder must resolve those
|
|
712
|
+
* via `shapeOf` first.
|
|
713
|
+
*/
|
|
714
|
+
function shapeBounds(shape) {
|
|
715
|
+
if (shape.kind === "rect") return shape.rect;
|
|
716
|
+
if (shape.kind === "transformed") return _grida_cmath.default.rect.transform(shape.local, shape.matrix);
|
|
717
|
+
if (shape.kind === "line") return _grida_cmath.default.rect.fromPoints([shape.p1, shape.p2]);
|
|
718
|
+
throw new Error(`shapeBounds: cannot bound unresolved shape (id=${shape.id})`);
|
|
539
719
|
}
|
|
540
720
|
//#endregion
|
|
541
721
|
//#region event/click-tracker.ts
|
|
@@ -579,9 +759,10 @@ function nowMs() {
|
|
|
579
759
|
/**
|
|
580
760
|
* Registry of overlay UI hit regions.
|
|
581
761
|
*
|
|
582
|
-
* Regions are appended in
|
|
583
|
-
*
|
|
584
|
-
*
|
|
762
|
+
* Regions are appended in declaration order. `hitTest` resolves by
|
|
763
|
+
* **lowest priority value** at the hit point; declaration order
|
|
764
|
+
* serves as tie-break only (later push wins on equal priorities,
|
|
765
|
+
* preserving the prior "topmost wins on overlap" feel).
|
|
585
766
|
*/
|
|
586
767
|
var HitRegions = class {
|
|
587
768
|
constructor() {
|
|
@@ -594,11 +775,18 @@ var HitRegions = class {
|
|
|
594
775
|
this.regions.push(region);
|
|
595
776
|
}
|
|
596
777
|
hitTest(point) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
778
|
+
return this.hitTestRegion(point)?.action ?? null;
|
|
779
|
+
}
|
|
780
|
+
/** Returns the full region (label + priority) — used by tests and
|
|
781
|
+
* debug tooling that want to assert on `label`. `hitTest` delegates here. */
|
|
782
|
+
hitTestRegion(point) {
|
|
783
|
+
let best = null;
|
|
784
|
+
for (const r of this.regions) {
|
|
785
|
+
const test_point = r.inverse_transform ? _grida_cmath.default.vector2.transform(point, r.inverse_transform) : point;
|
|
786
|
+
if (!_grida_cmath.default.rect.containsPoint(r.rect, test_point)) continue;
|
|
787
|
+
if (best === null || r.priority <= best.priority) best = r;
|
|
600
788
|
}
|
|
601
|
-
return
|
|
789
|
+
return best;
|
|
602
790
|
}
|
|
603
791
|
isEmpty() {
|
|
604
792
|
return this.regions.length === 0;
|
|
@@ -611,6 +799,44 @@ var HitRegions = class {
|
|
|
611
799
|
//#endregion
|
|
612
800
|
//#region event/decision.ts
|
|
613
801
|
/**
|
|
802
|
+
* Selection-intent decision module — pure functions, ZERO side effects.
|
|
803
|
+
*
|
|
804
|
+
* Mental model:
|
|
805
|
+
*
|
|
806
|
+
* The HUD is an event router over real overlay layers. The host creates
|
|
807
|
+
* overlays (resize knob, rotate region, endpoint knob, translate body,
|
|
808
|
+
* etc.); the router decides what each one does on pointer-down by overlay
|
|
809
|
+
* type, falling back to a scene-content pick when no overlay claims.
|
|
810
|
+
* Pointer events are synthesized on top of raw pointer input — no native
|
|
811
|
+
* `click`, no DOM ordering.
|
|
812
|
+
*
|
|
813
|
+
* Architecture:
|
|
814
|
+
*
|
|
815
|
+
* PointerDownInput ─► classifyScenario() ─► Scenario ─► dispatch() ─► PointerDownDecision
|
|
816
|
+
* (recognizer) (declarative table)
|
|
817
|
+
*
|
|
818
|
+
* The `Scenario` enum is a **descriptive label** for each `(overlay hit,
|
|
819
|
+
* pick result, selection, modifiers)` combination — useful for tests and
|
|
820
|
+
* readable dispatch — NOT a contract the HUD imposes on hosts. The
|
|
821
|
+
* contract is per-overlay routing semantics (e.g. resize knob always
|
|
822
|
+
* starts a gesture; translate body always defers) and the Tier-1 → Tier-2
|
|
823
|
+
* fallback when no overlay claims the point.
|
|
824
|
+
*
|
|
825
|
+
* Adding a new UX rule:
|
|
826
|
+
*
|
|
827
|
+
* 1. Add a new `Scenario` constant (or reuse one).
|
|
828
|
+
* 2. Add/update the recognizer branch in {@link classifyScenario}.
|
|
829
|
+
* 3. Add/update the dispatch branch in {@link decidePointerDown}.
|
|
830
|
+
* 4. Add tests pinning the classification and the dispatch.
|
|
831
|
+
* 5. Update the working-group doc.
|
|
832
|
+
*
|
|
833
|
+
* Working-group spec (implementation-agnostic):
|
|
834
|
+
* https://grida.co/docs/wg/feat-editor/ux-surface/selection-intent
|
|
835
|
+
*
|
|
836
|
+
* UX-narrative sibling:
|
|
837
|
+
* https://grida.co/docs/wg/feat-editor/ux-surface/selection
|
|
838
|
+
*/
|
|
839
|
+
/**
|
|
614
840
|
* Recognize which scenario a pointer-down belongs to. Total over inputs.
|
|
615
841
|
* Pure, no I/O. The single source of truth for "which atomic intent did the
|
|
616
842
|
* user just express?"
|
|
@@ -677,7 +903,7 @@ function dispatch(scenario, input) {
|
|
|
677
903
|
kind: "start_resize",
|
|
678
904
|
ids: a.ids,
|
|
679
905
|
direction: a.direction,
|
|
680
|
-
|
|
906
|
+
initial_shape: a.initial_shape
|
|
681
907
|
};
|
|
682
908
|
}
|
|
683
909
|
case "HandleRotate": {
|
|
@@ -686,7 +912,7 @@ function dispatch(scenario, input) {
|
|
|
686
912
|
kind: "start_rotate",
|
|
687
913
|
ids: a.ids,
|
|
688
914
|
corner: a.corner,
|
|
689
|
-
|
|
915
|
+
initial_shape: a.initial_shape
|
|
690
916
|
};
|
|
691
917
|
}
|
|
692
918
|
case "HandleEndpoint": {
|
|
@@ -787,11 +1013,13 @@ function decideIdleCursor(input) {
|
|
|
787
1013
|
if (ui_action) switch (ui_action.kind) {
|
|
788
1014
|
case "resize_handle": return {
|
|
789
1015
|
kind: "resize",
|
|
790
|
-
direction: ui_action.direction
|
|
1016
|
+
direction: ui_action.direction,
|
|
1017
|
+
baseAngle: shape_screen_angle_rad(ui_action.initial_shape)
|
|
791
1018
|
};
|
|
792
1019
|
case "rotate_handle": return {
|
|
793
1020
|
kind: "rotate",
|
|
794
|
-
corner: ui_action.corner
|
|
1021
|
+
corner: ui_action.corner,
|
|
1022
|
+
baseAngle: shape_screen_angle_rad(ui_action.initial_shape)
|
|
795
1023
|
};
|
|
796
1024
|
case "translate_handle": return "move";
|
|
797
1025
|
case "select_node":
|
|
@@ -800,6 +1028,24 @@ function decideIdleCursor(input) {
|
|
|
800
1028
|
if (hovered_id && selection_ids.includes(hovered_id)) return "move";
|
|
801
1029
|
return "default";
|
|
802
1030
|
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Doc-space rotation of a `SelectionShape` in radians. For `transformed`
|
|
1033
|
+
* shapes this is the angle baked into `matrix`; for `rect`/`line` it's 0.
|
|
1034
|
+
*
|
|
1035
|
+
* Used by `decideIdleCursor` and by the rotate-gesture cursor compositor
|
|
1036
|
+
* so the resize / rotate cursor always tilts to match the selection's
|
|
1037
|
+
* orientation — without requiring the HUD's camera to be axis-aligned
|
|
1038
|
+
* for the doc-space angle to be the right thing to render (the renderer
|
|
1039
|
+
* draws the cursor in screen px, and in svg-editor the camera contributes
|
|
1040
|
+
* scale + translate only, so doc-space == screen-space rotation).
|
|
1041
|
+
*
|
|
1042
|
+
* For hosts that ROTATE the HUD camera, this should compose with the
|
|
1043
|
+
* camera's angle at consume time. Not relevant for svg-editor.
|
|
1044
|
+
*/
|
|
1045
|
+
function shape_screen_angle_rad(shape) {
|
|
1046
|
+
if (shape.kind !== "transformed") return 0;
|
|
1047
|
+
return _grida_cmath.default.transform.angle(shape.matrix) * Math.PI / 180;
|
|
1048
|
+
}
|
|
803
1049
|
//#endregion
|
|
804
1050
|
//#region event/transform.ts
|
|
805
1051
|
const IDENTITY = [[
|
|
@@ -1004,16 +1250,17 @@ var SurfaceState = class {
|
|
|
1004
1250
|
const g = this.gesture;
|
|
1005
1251
|
const dx = point_doc[0] - g.anchor_doc[0];
|
|
1006
1252
|
const dy = point_doc[1] - g.anchor_doc[1];
|
|
1007
|
-
const
|
|
1253
|
+
const next_shape = applyResize(g.initial_shape, g.direction, dx, dy);
|
|
1008
1254
|
this.gesture = {
|
|
1009
1255
|
...g,
|
|
1010
|
-
|
|
1256
|
+
current_shape: next_shape
|
|
1011
1257
|
};
|
|
1012
1258
|
deps.emitIntent({
|
|
1013
1259
|
kind: "resize",
|
|
1014
1260
|
ids: g.ids,
|
|
1015
1261
|
anchor: g.direction,
|
|
1016
|
-
rect:
|
|
1262
|
+
rect: shapeBounds(next_shape),
|
|
1263
|
+
shape: next_shape,
|
|
1017
1264
|
phase: "preview"
|
|
1018
1265
|
});
|
|
1019
1266
|
response.needsRedraw = true;
|
|
@@ -1026,12 +1273,18 @@ var SurfaceState = class {
|
|
|
1026
1273
|
...g,
|
|
1027
1274
|
current_angle: angle
|
|
1028
1275
|
};
|
|
1276
|
+
const delta = angle - g.anchor_angle;
|
|
1029
1277
|
deps.emitIntent({
|
|
1030
1278
|
kind: "rotate",
|
|
1031
1279
|
ids: g.ids,
|
|
1032
|
-
angle:
|
|
1280
|
+
angle: delta,
|
|
1033
1281
|
phase: "preview"
|
|
1034
1282
|
});
|
|
1283
|
+
this.setCursor({
|
|
1284
|
+
kind: "rotate",
|
|
1285
|
+
corner: g.corner,
|
|
1286
|
+
baseAngle: g.initial_cursor_angle + delta
|
|
1287
|
+
}, response);
|
|
1035
1288
|
response.needsRedraw = true;
|
|
1036
1289
|
return response;
|
|
1037
1290
|
}
|
|
@@ -1083,23 +1336,30 @@ var SurfaceState = class {
|
|
|
1083
1336
|
kind: "resize",
|
|
1084
1337
|
ids: [...decision.ids],
|
|
1085
1338
|
direction: decision.direction,
|
|
1086
|
-
|
|
1339
|
+
initial_shape: decision.initial_shape,
|
|
1087
1340
|
anchor_doc: point_doc,
|
|
1088
|
-
|
|
1341
|
+
current_shape: decision.initial_shape
|
|
1089
1342
|
};
|
|
1090
1343
|
response.needsRedraw = true;
|
|
1091
1344
|
return response;
|
|
1092
1345
|
case "start_rotate": {
|
|
1093
|
-
const [cx, cy] = _grida_cmath.default.rect.getCenter(decision.
|
|
1346
|
+
const [cx, cy] = _grida_cmath.default.rect.getCenter(shapeBounds(decision.initial_shape));
|
|
1094
1347
|
const angle = Math.atan2(point_doc[1] - cy, point_doc[0] - cx);
|
|
1348
|
+
const initial_cursor_angle = decision.initial_shape.kind === "transformed" ? _grida_cmath.default.transform.angle(decision.initial_shape.matrix) * Math.PI / 180 : 0;
|
|
1095
1349
|
this.gesture = {
|
|
1096
1350
|
kind: "rotate",
|
|
1097
1351
|
ids: [...decision.ids],
|
|
1098
1352
|
corner: decision.corner,
|
|
1099
1353
|
center_doc: [cx, cy],
|
|
1100
1354
|
anchor_angle: angle,
|
|
1101
|
-
current_angle: angle
|
|
1355
|
+
current_angle: angle,
|
|
1356
|
+
initial_cursor_angle
|
|
1102
1357
|
};
|
|
1358
|
+
this.setCursor({
|
|
1359
|
+
kind: "rotate",
|
|
1360
|
+
corner: decision.corner,
|
|
1361
|
+
baseAngle: initial_cursor_angle
|
|
1362
|
+
}, response);
|
|
1103
1363
|
response.needsRedraw = true;
|
|
1104
1364
|
return response;
|
|
1105
1365
|
}
|
|
@@ -1183,7 +1443,7 @@ var SurfaceState = class {
|
|
|
1183
1443
|
});
|
|
1184
1444
|
this.gesture = IDLE;
|
|
1185
1445
|
response.needsRedraw = true;
|
|
1186
|
-
if (cursorEquals(this.cursor, "move")) this.setCursor("default", response);
|
|
1446
|
+
if (require_cursor.cursorEquals(this.cursor, "move")) this.setCursor("default", response);
|
|
1187
1447
|
break;
|
|
1188
1448
|
}
|
|
1189
1449
|
case "marquee": {
|
|
@@ -1205,7 +1465,8 @@ var SurfaceState = class {
|
|
|
1205
1465
|
kind: "resize",
|
|
1206
1466
|
ids: g.ids,
|
|
1207
1467
|
anchor: g.direction,
|
|
1208
|
-
rect: g.
|
|
1468
|
+
rect: shapeBounds(g.current_shape),
|
|
1469
|
+
shape: g.current_shape,
|
|
1209
1470
|
phase: "commit"
|
|
1210
1471
|
});
|
|
1211
1472
|
this.gesture = IDLE;
|
|
@@ -1276,7 +1537,7 @@ var SurfaceState = class {
|
|
|
1276
1537
|
return response;
|
|
1277
1538
|
}
|
|
1278
1539
|
setCursor(next, response) {
|
|
1279
|
-
if (!cursorEquals(this.cursor, next)) {
|
|
1540
|
+
if (!require_cursor.cursorEquals(this.cursor, next)) {
|
|
1280
1541
|
this.cursor = next;
|
|
1281
1542
|
response.cursorChanged = true;
|
|
1282
1543
|
}
|
|
@@ -1302,20 +1563,6 @@ function mergeStyle(base, partial) {
|
|
|
1302
1563
|
};
|
|
1303
1564
|
}
|
|
1304
1565
|
//#endregion
|
|
1305
|
-
//#region event/shape.ts
|
|
1306
|
-
/**
|
|
1307
|
-
* Compute the axis-aligned bounding box of a `SelectionShape` in doc-space.
|
|
1308
|
-
* Used for layout math the chrome builder needs (e.g. handle positions).
|
|
1309
|
-
*
|
|
1310
|
-
* Throws on `kind: "unresolved"` — the chrome builder must resolve those
|
|
1311
|
-
* via `shapeOf` first.
|
|
1312
|
-
*/
|
|
1313
|
-
function shapeBounds(shape) {
|
|
1314
|
-
if (shape.kind === "rect") return shape.rect;
|
|
1315
|
-
if (shape.kind === "line") return _grida_cmath.default.rect.fromPoints([shape.p1, shape.p2]);
|
|
1316
|
-
throw new Error(`shapeBounds: cannot bound unresolved shape (id=${shape.id})`);
|
|
1317
|
-
}
|
|
1318
|
-
//#endregion
|
|
1319
1566
|
//#region event/overlay.ts
|
|
1320
1567
|
/**
|
|
1321
1568
|
* Minimum hit-target size in screen-px.
|
|
@@ -1331,25 +1578,237 @@ const MIN_HIT_SIZE = 16;
|
|
|
1331
1578
|
*/
|
|
1332
1579
|
const MIN_CHROME_VISIBLE_SIZE = 12;
|
|
1333
1580
|
//#endregion
|
|
1334
|
-
//#region
|
|
1335
|
-
const
|
|
1581
|
+
//#region event/selection-controls.ts
|
|
1582
|
+
const CORNER_DIRS = [
|
|
1336
1583
|
"nw",
|
|
1337
1584
|
"ne",
|
|
1338
1585
|
"se",
|
|
1339
1586
|
"sw"
|
|
1340
1587
|
];
|
|
1341
|
-
const
|
|
1588
|
+
const EDGE_DIRS = [
|
|
1342
1589
|
"n",
|
|
1343
1590
|
"e",
|
|
1344
1591
|
"s",
|
|
1345
1592
|
"w"
|
|
1346
1593
|
];
|
|
1347
|
-
const
|
|
1348
|
-
"nw",
|
|
1349
|
-
"ne",
|
|
1350
|
-
"se",
|
|
1351
|
-
"sw"
|
|
1352
|
-
|
|
1594
|
+
const CORNER_LABEL = {
|
|
1595
|
+
nw: "resize_handle:nw",
|
|
1596
|
+
ne: "resize_handle:ne",
|
|
1597
|
+
se: "resize_handle:se",
|
|
1598
|
+
sw: "resize_handle:sw"
|
|
1599
|
+
};
|
|
1600
|
+
const EDGE_LABEL = {
|
|
1601
|
+
n: "resize_edge:n",
|
|
1602
|
+
e: "resize_edge:e",
|
|
1603
|
+
s: "resize_edge:s",
|
|
1604
|
+
w: "resize_edge:w"
|
|
1605
|
+
};
|
|
1606
|
+
const ROTATE_LABEL = {
|
|
1607
|
+
nw: "rotate:nw",
|
|
1608
|
+
ne: "rotate:ne",
|
|
1609
|
+
se: "rotate:se",
|
|
1610
|
+
sw: "rotate:sw"
|
|
1611
|
+
};
|
|
1612
|
+
const HUDHitPriority = {
|
|
1613
|
+
ENDPOINT_HANDLE: 10,
|
|
1614
|
+
RESIZE_HANDLE_EDGE_SMALL: 22,
|
|
1615
|
+
TRANSLATE_BODY_SMALL: 25,
|
|
1616
|
+
RESIZE_HANDLE_EDGE: 30,
|
|
1617
|
+
RESIZE_HANDLE_CORNER: 31,
|
|
1618
|
+
TRANSLATE_BODY: 40,
|
|
1619
|
+
ROTATE_HANDLE: 50
|
|
1620
|
+
};
|
|
1621
|
+
/** The principle constant — the minimum guaranteed length for the body
|
|
1622
|
+
* interior on each axis AND for each side strip along its parallel
|
|
1623
|
+
* axis. Tunable; everything else derives. */
|
|
1624
|
+
const MIN_GUARANTEED_INTERACTIVE_DIM = 20;
|
|
1625
|
+
/** Below this axis dim, body promotes above corner. Derived from the
|
|
1626
|
+
* principle. */
|
|
1627
|
+
const BODY_FLIP_THRESHOLD = 36;
|
|
1628
|
+
/**
|
|
1629
|
+
* @returns { corner, edge } — lengths in screen-px summing to `total`
|
|
1630
|
+
* (corner * 2 + edge === total). Each is `>= 0`.
|
|
1631
|
+
*
|
|
1632
|
+
* Three phases:
|
|
1633
|
+
* - **comfortable** (`total >= 2 * corner_preferred + edge_min`):
|
|
1634
|
+
* corners at preferred, edge takes the surplus.
|
|
1635
|
+
* - **squeezed** (`total >= edge_min`): edge at its min, corners share
|
|
1636
|
+
* the remainder.
|
|
1637
|
+
* - **tiny** (`total < edge_min`): edge takes everything, corners 0.
|
|
1638
|
+
*/
|
|
1639
|
+
function negotiateAxis(total, corner_preferred, edge_min) {
|
|
1640
|
+
if (total <= 0) return {
|
|
1641
|
+
corner: 0,
|
|
1642
|
+
edge: 0
|
|
1643
|
+
};
|
|
1644
|
+
if (total >= corner_preferred * 2 + edge_min) return {
|
|
1645
|
+
corner: corner_preferred,
|
|
1646
|
+
edge: total - corner_preferred * 2
|
|
1647
|
+
};
|
|
1648
|
+
if (total >= edge_min) return {
|
|
1649
|
+
corner: (total - edge_min) / 2,
|
|
1650
|
+
edge: edge_min
|
|
1651
|
+
};
|
|
1652
|
+
return {
|
|
1653
|
+
corner: 0,
|
|
1654
|
+
edge: total
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Compute the selection control layout for a screen-space rect.
|
|
1659
|
+
*
|
|
1660
|
+
* Pure: no DOM, no global state. Same inputs → same zones.
|
|
1661
|
+
*
|
|
1662
|
+
* The perimeter ring straddles the bbox edge with `extension =
|
|
1663
|
+
* hit_size / 2` overhang outside. Along each axis the run of length
|
|
1664
|
+
* `axis_dim + 2 * extension` is split via {@link negotiateAxis} into
|
|
1665
|
+
* `[corner | edge | corner]`. The 4 corners and 4 edges in 2D then tile
|
|
1666
|
+
* the ring as a strict 3×3 grid of cells — **non-overlapping**. Body
|
|
1667
|
+
* sits at rect_screen and may overlap with the ring's inside-bbox half
|
|
1668
|
+
* in comfortable mode; priority resolves those overlaps.
|
|
1669
|
+
*
|
|
1670
|
+
* See the comment block in this file for the full principle.
|
|
1671
|
+
*/
|
|
1672
|
+
function computeSelectionControlLayout(rect_screen, opts) {
|
|
1673
|
+
const zones = [];
|
|
1674
|
+
const w_violated = rect_screen.width < BODY_FLIP_THRESHOLD;
|
|
1675
|
+
const h_violated = rect_screen.height < BODY_FLIP_THRESHOLD;
|
|
1676
|
+
const small_mode = w_violated || h_violated;
|
|
1677
|
+
const controls_visible = rect_screen.width >= 12 && rect_screen.height >= 12;
|
|
1678
|
+
if (rect_screen.width >= 1 || rect_screen.height >= 1) zones.push({
|
|
1679
|
+
rect: rect_screen,
|
|
1680
|
+
priority: small_mode ? HUDHitPriority.TRANSLATE_BODY_SMALL : HUDHitPriority.TRANSLATE_BODY,
|
|
1681
|
+
role: { kind: "translate" },
|
|
1682
|
+
label: "translate"
|
|
1683
|
+
});
|
|
1684
|
+
if (!controls_visible) return {
|
|
1685
|
+
zones,
|
|
1686
|
+
controls_visible,
|
|
1687
|
+
small_mode
|
|
1688
|
+
};
|
|
1689
|
+
const hit_size = Math.max(opts.handle_size + 4, 16);
|
|
1690
|
+
const extension = hit_size / 2;
|
|
1691
|
+
const total_x = rect_screen.width + extension * 2;
|
|
1692
|
+
const total_y = rect_screen.height + extension * 2;
|
|
1693
|
+
const { corner: cx, edge: ex } = negotiateAxis(total_x, hit_size, 20);
|
|
1694
|
+
const { corner: cy, edge: ey } = negotiateAxis(total_y, hit_size, 20);
|
|
1695
|
+
const left = rect_screen.x - extension;
|
|
1696
|
+
const top = rect_screen.y - extension;
|
|
1697
|
+
const mid_x = left + cx;
|
|
1698
|
+
const mid_y = top + cy;
|
|
1699
|
+
const right_x = mid_x + ex;
|
|
1700
|
+
const right_y = mid_y + ey;
|
|
1701
|
+
const cornerRects = {
|
|
1702
|
+
nw: {
|
|
1703
|
+
x: left,
|
|
1704
|
+
y: top,
|
|
1705
|
+
width: cx,
|
|
1706
|
+
height: cy
|
|
1707
|
+
},
|
|
1708
|
+
ne: {
|
|
1709
|
+
x: right_x,
|
|
1710
|
+
y: top,
|
|
1711
|
+
width: cx,
|
|
1712
|
+
height: cy
|
|
1713
|
+
},
|
|
1714
|
+
sw: {
|
|
1715
|
+
x: left,
|
|
1716
|
+
y: right_y,
|
|
1717
|
+
width: cx,
|
|
1718
|
+
height: cy
|
|
1719
|
+
},
|
|
1720
|
+
se: {
|
|
1721
|
+
x: right_x,
|
|
1722
|
+
y: right_y,
|
|
1723
|
+
width: cx,
|
|
1724
|
+
height: cy
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
const edgeRects = {
|
|
1728
|
+
n: {
|
|
1729
|
+
x: mid_x,
|
|
1730
|
+
y: top,
|
|
1731
|
+
width: ex,
|
|
1732
|
+
height: cy
|
|
1733
|
+
},
|
|
1734
|
+
s: {
|
|
1735
|
+
x: mid_x,
|
|
1736
|
+
y: right_y,
|
|
1737
|
+
width: ex,
|
|
1738
|
+
height: cy
|
|
1739
|
+
},
|
|
1740
|
+
w: {
|
|
1741
|
+
x: left,
|
|
1742
|
+
y: mid_y,
|
|
1743
|
+
width: cx,
|
|
1744
|
+
height: ey
|
|
1745
|
+
},
|
|
1746
|
+
e: {
|
|
1747
|
+
x: right_x,
|
|
1748
|
+
y: mid_y,
|
|
1749
|
+
width: cx,
|
|
1750
|
+
height: ey
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
for (const dir of CORNER_DIRS) {
|
|
1754
|
+
const rect = cornerRects[dir];
|
|
1755
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
1756
|
+
zones.push({
|
|
1757
|
+
rect,
|
|
1758
|
+
priority: HUDHitPriority.RESIZE_HANDLE_CORNER,
|
|
1759
|
+
role: {
|
|
1760
|
+
kind: "resize_corner",
|
|
1761
|
+
direction: dir
|
|
1762
|
+
},
|
|
1763
|
+
label: CORNER_LABEL[dir]
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
const edge_priority = {
|
|
1767
|
+
n: w_violated ? HUDHitPriority.RESIZE_HANDLE_EDGE_SMALL : HUDHitPriority.RESIZE_HANDLE_EDGE,
|
|
1768
|
+
s: w_violated ? HUDHitPriority.RESIZE_HANDLE_EDGE_SMALL : HUDHitPriority.RESIZE_HANDLE_EDGE,
|
|
1769
|
+
e: h_violated ? HUDHitPriority.RESIZE_HANDLE_EDGE_SMALL : HUDHitPriority.RESIZE_HANDLE_EDGE,
|
|
1770
|
+
w: h_violated ? HUDHitPriority.RESIZE_HANDLE_EDGE_SMALL : HUDHitPriority.RESIZE_HANDLE_EDGE
|
|
1771
|
+
};
|
|
1772
|
+
for (const dir of EDGE_DIRS) {
|
|
1773
|
+
const rect = edgeRects[dir];
|
|
1774
|
+
if (rect.width <= 0 || rect.height <= 0) continue;
|
|
1775
|
+
zones.push({
|
|
1776
|
+
rect,
|
|
1777
|
+
priority: edge_priority[dir],
|
|
1778
|
+
role: {
|
|
1779
|
+
kind: "resize_edge",
|
|
1780
|
+
direction: dir
|
|
1781
|
+
},
|
|
1782
|
+
label: EDGE_LABEL[dir]
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
if (opts.show_rotation) for (const dir of CORNER_DIRS) {
|
|
1786
|
+
const resize = cornerRects[dir];
|
|
1787
|
+
if (resize.width <= 0 || resize.height <= 0) continue;
|
|
1788
|
+
const [dx, dy] = _grida_cmath.default.compass.cardinal_direction_vector[dir];
|
|
1789
|
+
zones.push({
|
|
1790
|
+
rect: {
|
|
1791
|
+
x: resize.x + (dx > 0 ? 0 : -16),
|
|
1792
|
+
y: resize.y + (dy > 0 ? 0 : -16),
|
|
1793
|
+
width: resize.width + 16,
|
|
1794
|
+
height: resize.height + 16
|
|
1795
|
+
},
|
|
1796
|
+
priority: HUDHitPriority.ROTATE_HANDLE,
|
|
1797
|
+
role: {
|
|
1798
|
+
kind: "rotate",
|
|
1799
|
+
corner: dir
|
|
1800
|
+
},
|
|
1801
|
+
label: ROTATE_LABEL[dir]
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
return {
|
|
1805
|
+
zones,
|
|
1806
|
+
controls_visible,
|
|
1807
|
+
small_mode
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
//#endregion
|
|
1811
|
+
//#region surface/chrome.ts
|
|
1353
1812
|
/**
|
|
1354
1813
|
* Build the per-frame surface chrome.
|
|
1355
1814
|
*
|
|
@@ -1359,34 +1818,44 @@ const ROTATION_CORNERS = [
|
|
|
1359
1818
|
* - `decoration` — pure visual `HUDDraw` (selection outlines, hover outline,
|
|
1360
1819
|
* marquee, line outlines). Not interactable.
|
|
1361
1820
|
*
|
|
1821
|
+
* Priority is data, not iteration order. Each `OverlayElement` carries its
|
|
1822
|
+
* own `priority` (lower wins) and a stable `label`. The `HitRegions`
|
|
1823
|
+
* registry resolves overlapping regions by priority, not push order. See
|
|
1824
|
+
* `event/selection-controls.ts` for the canonical priority ladder.
|
|
1825
|
+
*
|
|
1362
1826
|
* The Surface fans `overlays` into `HitRegions` (for events) and merges
|
|
1363
1827
|
* their render shapes into `decoration` (for the canvas draw call).
|
|
1364
1828
|
*/
|
|
1365
1829
|
function buildChrome(input) {
|
|
1366
|
-
const { state, shapeOf, style } = input;
|
|
1830
|
+
const { state, shapeOf, style, groups } = input;
|
|
1367
1831
|
const transform = state.getTransform();
|
|
1368
1832
|
const overlays = [];
|
|
1369
1833
|
const decoration_rects = [];
|
|
1370
1834
|
const decoration_lines = [];
|
|
1371
|
-
const
|
|
1372
|
-
const hover_id =
|
|
1835
|
+
const decoration_polylines = [];
|
|
1836
|
+
const hover_id = state.gesture.kind === "idle" ? state.getEffectiveHover() : null;
|
|
1373
1837
|
if (hover_id) {
|
|
1374
1838
|
const shape = shapeOf(hover_id);
|
|
1375
|
-
if (shape) pushShapeOutline(shape, decoration_rects, decoration_lines, {
|
|
1839
|
+
if (shape) pushShapeOutline(shape, decoration_rects, decoration_lines, decoration_polylines, {
|
|
1376
1840
|
dashed: false,
|
|
1377
|
-
strokeWidth: style.hoverOutlineWidth
|
|
1841
|
+
strokeWidth: style.hoverOutlineWidth,
|
|
1842
|
+
group: groups?.hover
|
|
1378
1843
|
});
|
|
1379
1844
|
}
|
|
1380
|
-
|
|
1845
|
+
for (const group of state.getSelectionGroups()) {
|
|
1381
1846
|
const shape = resolveGroupShape(group, shapeOf);
|
|
1382
1847
|
if (!shape) continue;
|
|
1383
|
-
pushShapeOutline(shape, decoration_rects, decoration_lines, {
|
|
1848
|
+
pushShapeOutline(shape, decoration_rects, decoration_lines, decoration_polylines, {
|
|
1384
1849
|
dashed: false,
|
|
1385
|
-
strokeWidth: style.selectionOutlineWidth
|
|
1850
|
+
strokeWidth: style.selectionOutlineWidth,
|
|
1851
|
+
group: groups?.selection
|
|
1386
1852
|
});
|
|
1387
|
-
|
|
1388
|
-
if (shape.kind === "
|
|
1389
|
-
else if (shape.kind === "line")
|
|
1853
|
+
if (shape.kind === "rect") pushRectChrome(shape.rect, group.ids, transform, style, groups?.selectionControls, overlays);
|
|
1854
|
+
else if (shape.kind === "transformed") pushTransformedChrome(shape.local, shape.matrix, group.ids, transform, style, groups?.selectionControls, overlays);
|
|
1855
|
+
else if (shape.kind === "line") {
|
|
1856
|
+
pushLineBody(shape, group.ids, transform, groups?.selectionControls, overlays);
|
|
1857
|
+
pushLineEndpoints(group.ids[0], shape.p1, shape.p2, style, groups?.selectionControls, overlays);
|
|
1858
|
+
}
|
|
1390
1859
|
}
|
|
1391
1860
|
if (state.gesture.kind === "marquee") {
|
|
1392
1861
|
const g = state.gesture;
|
|
@@ -1395,20 +1864,35 @@ function buildChrome(input) {
|
|
|
1395
1864
|
...mr,
|
|
1396
1865
|
stroke: true,
|
|
1397
1866
|
fill: true,
|
|
1398
|
-
fillOpacity: .15
|
|
1867
|
+
fillOpacity: .15,
|
|
1868
|
+
group: groups?.marquee
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
if (state.gesture.kind === "resize") {
|
|
1872
|
+
const shape = state.gesture.current_shape;
|
|
1873
|
+
if (shape.kind === "transformed") {
|
|
1874
|
+
const corners = _grida_cmath.default.rect.toCorners(shape.local).map((p) => _grida_cmath.default.vector2.transform(p, shape.matrix));
|
|
1875
|
+
decoration_polylines.push({
|
|
1876
|
+
points: [...corners, corners[0]],
|
|
1877
|
+
stroke: true,
|
|
1878
|
+
fill: false,
|
|
1879
|
+
dashed: true,
|
|
1880
|
+
group: groups?.transformPreview
|
|
1881
|
+
});
|
|
1882
|
+
} else decoration_rects.push({
|
|
1883
|
+
...shapeBounds(shape),
|
|
1884
|
+
stroke: true,
|
|
1885
|
+
fill: false,
|
|
1886
|
+
dashed: true,
|
|
1887
|
+
group: groups?.transformPreview
|
|
1399
1888
|
});
|
|
1400
1889
|
}
|
|
1401
|
-
if (state.gesture.kind === "resize") decoration_rects.push({
|
|
1402
|
-
...state.gesture.current_rect,
|
|
1403
|
-
stroke: true,
|
|
1404
|
-
fill: false,
|
|
1405
|
-
dashed: true
|
|
1406
|
-
});
|
|
1407
1890
|
return {
|
|
1408
1891
|
overlays,
|
|
1409
1892
|
decoration: {
|
|
1410
1893
|
rects: decoration_rects.length > 0 ? decoration_rects : void 0,
|
|
1411
|
-
lines: decoration_lines.length > 0 ? decoration_lines : void 0
|
|
1894
|
+
lines: decoration_lines.length > 0 ? decoration_lines : void 0,
|
|
1895
|
+
polylines: decoration_polylines.length > 0 ? decoration_polylines : void 0
|
|
1412
1896
|
}
|
|
1413
1897
|
};
|
|
1414
1898
|
}
|
|
@@ -1416,168 +1900,262 @@ function resolveGroupShape(group, shapeOf) {
|
|
|
1416
1900
|
if (group.shape.kind === "unresolved") return shapeOf(group.shape.id);
|
|
1417
1901
|
return group.shape;
|
|
1418
1902
|
}
|
|
1419
|
-
function
|
|
1420
|
-
const
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
out.push({
|
|
1424
|
-
action: {
|
|
1425
|
-
kind: "translate_handle",
|
|
1426
|
-
ids
|
|
1427
|
-
},
|
|
1428
|
-
hit: {
|
|
1429
|
-
kind: "screen_aabb",
|
|
1430
|
-
rect: rect_screen
|
|
1431
|
-
},
|
|
1432
|
-
cursor: "move"
|
|
1903
|
+
function pushRectChrome(rect_doc, ids, transform, style, group, out) {
|
|
1904
|
+
const layout = computeSelectionControlLayout(_grida_cmath.default.rect.transform(rect_doc, transform), {
|
|
1905
|
+
handle_size: style.handleSize,
|
|
1906
|
+
show_rotation: style.showRotationHandles && ids.length >= 1
|
|
1433
1907
|
});
|
|
1908
|
+
for (const zone of layout.zones) {
|
|
1909
|
+
const el = zoneToOverlay(zone, rect_doc, ids, style, group, layout.controls_visible);
|
|
1910
|
+
if (el) out.push(el);
|
|
1911
|
+
}
|
|
1434
1912
|
}
|
|
1435
|
-
function
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1913
|
+
function zoneToOverlay(zone, rect_doc, ids, style, group, controls_visible) {
|
|
1914
|
+
switch (zone.role.kind) {
|
|
1915
|
+
case "translate": return {
|
|
1916
|
+
label: zone.label,
|
|
1917
|
+
group,
|
|
1918
|
+
action: {
|
|
1919
|
+
kind: "translate_handle",
|
|
1920
|
+
ids
|
|
1921
|
+
},
|
|
1922
|
+
hit: {
|
|
1923
|
+
kind: "screen_aabb",
|
|
1924
|
+
rect: zone.rect
|
|
1925
|
+
},
|
|
1926
|
+
priority: zone.priority,
|
|
1927
|
+
cursor: "move"
|
|
1928
|
+
};
|
|
1929
|
+
case "resize_corner": {
|
|
1930
|
+
const dir = zone.role.direction;
|
|
1931
|
+
const size = style.handleSize;
|
|
1932
|
+
const anchor_doc = _grida_cmath.default.rect.getCardinalPoint(rect_doc, dir);
|
|
1933
|
+
return {
|
|
1934
|
+
label: zone.label,
|
|
1935
|
+
group,
|
|
1936
|
+
action: {
|
|
1937
|
+
kind: "resize_handle",
|
|
1938
|
+
direction: dir,
|
|
1939
|
+
ids,
|
|
1940
|
+
initial_shape: {
|
|
1941
|
+
kind: "rect",
|
|
1942
|
+
rect: rect_doc
|
|
1943
|
+
}
|
|
1944
|
+
},
|
|
1945
|
+
hit: {
|
|
1946
|
+
kind: "screen_aabb",
|
|
1947
|
+
rect: zone.rect
|
|
1948
|
+
},
|
|
1949
|
+
render: controls_visible ? {
|
|
1950
|
+
kind: "screen_rect",
|
|
1951
|
+
anchor_doc,
|
|
1952
|
+
width: size,
|
|
1953
|
+
height: size,
|
|
1954
|
+
placement: "center",
|
|
1955
|
+
fill: true,
|
|
1956
|
+
stroke: true,
|
|
1957
|
+
fillColor: style.handleFill,
|
|
1958
|
+
strokeColor: style.handleStroke
|
|
1959
|
+
} : void 0,
|
|
1960
|
+
priority: zone.priority,
|
|
1961
|
+
cursor: {
|
|
1962
|
+
kind: "resize",
|
|
1963
|
+
direction: dir
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
case "resize_edge": return {
|
|
1968
|
+
label: zone.label,
|
|
1969
|
+
group,
|
|
1444
1970
|
action: {
|
|
1445
1971
|
kind: "resize_handle",
|
|
1446
|
-
direction:
|
|
1972
|
+
direction: zone.role.direction,
|
|
1447
1973
|
ids,
|
|
1448
|
-
|
|
1974
|
+
initial_shape: {
|
|
1975
|
+
kind: "rect",
|
|
1976
|
+
rect: rect_doc
|
|
1977
|
+
}
|
|
1449
1978
|
},
|
|
1450
1979
|
hit: {
|
|
1451
|
-
kind: "
|
|
1452
|
-
|
|
1453
|
-
width: hit_size,
|
|
1454
|
-
height: hit_size,
|
|
1455
|
-
placement: "center"
|
|
1456
|
-
},
|
|
1457
|
-
render: {
|
|
1458
|
-
kind: "screen_rect",
|
|
1459
|
-
anchor_doc,
|
|
1460
|
-
width: size,
|
|
1461
|
-
height: size,
|
|
1462
|
-
placement: "center",
|
|
1463
|
-
fill: true,
|
|
1464
|
-
stroke: true,
|
|
1465
|
-
fillColor: style.handleFill,
|
|
1466
|
-
strokeColor: style.handleStroke
|
|
1980
|
+
kind: "screen_aabb",
|
|
1981
|
+
rect: zone.rect
|
|
1467
1982
|
},
|
|
1983
|
+
priority: zone.priority,
|
|
1468
1984
|
cursor: {
|
|
1469
1985
|
kind: "resize",
|
|
1470
|
-
direction:
|
|
1986
|
+
direction: zone.role.direction
|
|
1471
1987
|
}
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
const strip = edge_strips[dir];
|
|
1477
|
-
out.push({
|
|
1988
|
+
};
|
|
1989
|
+
case "rotate": return {
|
|
1990
|
+
label: zone.label,
|
|
1991
|
+
group,
|
|
1478
1992
|
action: {
|
|
1479
|
-
kind: "
|
|
1480
|
-
|
|
1993
|
+
kind: "rotate_handle",
|
|
1994
|
+
corner: zone.role.corner,
|
|
1481
1995
|
ids,
|
|
1482
|
-
|
|
1996
|
+
initial_shape: {
|
|
1997
|
+
kind: "rect",
|
|
1998
|
+
rect: rect_doc
|
|
1999
|
+
}
|
|
1483
2000
|
},
|
|
1484
2001
|
hit: {
|
|
1485
2002
|
kind: "screen_aabb",
|
|
1486
|
-
rect:
|
|
2003
|
+
rect: zone.rect
|
|
1487
2004
|
},
|
|
2005
|
+
priority: zone.priority,
|
|
1488
2006
|
cursor: {
|
|
1489
|
-
kind: "
|
|
1490
|
-
|
|
2007
|
+
kind: "rotate",
|
|
2008
|
+
corner: zone.role.corner
|
|
1491
2009
|
}
|
|
1492
|
-
}
|
|
2010
|
+
};
|
|
1493
2011
|
}
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
2012
|
+
}
|
|
2013
|
+
function pushTransformedChrome(local, matrix, ids, camera, style, group, out) {
|
|
2014
|
+
const local_to_screen = _grida_cmath.default.transform.multiply(camera, matrix);
|
|
2015
|
+
const scale_xy = _grida_cmath.default.transform.getScale(local_to_screen);
|
|
2016
|
+
const angle_deg = _grida_cmath.default.transform.angle(local_to_screen);
|
|
2017
|
+
const angle_rad = angle_deg * Math.PI / 180;
|
|
2018
|
+
const screen_w = local.width * scale_xy[0];
|
|
2019
|
+
const screen_h = local.height * scale_xy[1];
|
|
2020
|
+
const local_center = [local.x + local.width / 2, local.y + local.height / 2];
|
|
2021
|
+
const screen_center = _grida_cmath.default.vector2.transform(local_center, local_to_screen);
|
|
2022
|
+
const layout = computeSelectionControlLayout({
|
|
2023
|
+
x: screen_center[0] - screen_w / 2,
|
|
2024
|
+
y: screen_center[1] - screen_h / 2,
|
|
2025
|
+
width: screen_w,
|
|
2026
|
+
height: screen_h
|
|
2027
|
+
}, {
|
|
2028
|
+
handle_size: style.handleSize,
|
|
2029
|
+
show_rotation: style.showRotationHandles && ids.length >= 1
|
|
2030
|
+
});
|
|
2031
|
+
const inverse_transform = _grida_cmath.default.transform.rotate(_grida_cmath.default.transform.identity, -angle_deg, screen_center);
|
|
2032
|
+
const initial_shape = {
|
|
2033
|
+
kind: "transformed",
|
|
2034
|
+
local,
|
|
2035
|
+
matrix
|
|
2036
|
+
};
|
|
2037
|
+
for (const zone of layout.zones) {
|
|
2038
|
+
const hit = {
|
|
2039
|
+
kind: "screen_obb",
|
|
2040
|
+
rect: zone.rect,
|
|
2041
|
+
inverse_transform
|
|
2042
|
+
};
|
|
2043
|
+
switch (zone.role.kind) {
|
|
2044
|
+
case "translate":
|
|
2045
|
+
out.push({
|
|
2046
|
+
label: zone.label,
|
|
2047
|
+
group,
|
|
2048
|
+
action: {
|
|
2049
|
+
kind: "translate_handle",
|
|
2050
|
+
ids
|
|
2051
|
+
},
|
|
2052
|
+
hit,
|
|
2053
|
+
priority: zone.priority,
|
|
2054
|
+
cursor: "move"
|
|
2055
|
+
});
|
|
2056
|
+
break;
|
|
2057
|
+
case "resize_corner": {
|
|
2058
|
+
const dir = zone.role.direction;
|
|
2059
|
+
const size = style.handleSize;
|
|
2060
|
+
const cardinal_local = _grida_cmath.default.rect.getCardinalPoint(local, dir);
|
|
2061
|
+
const anchor_doc = _grida_cmath.default.vector2.transform(cardinal_local, matrix);
|
|
2062
|
+
out.push({
|
|
2063
|
+
label: zone.label,
|
|
2064
|
+
group,
|
|
2065
|
+
action: {
|
|
2066
|
+
kind: "resize_handle",
|
|
2067
|
+
direction: dir,
|
|
2068
|
+
ids,
|
|
2069
|
+
initial_shape
|
|
2070
|
+
},
|
|
2071
|
+
hit,
|
|
2072
|
+
render: layout.controls_visible ? {
|
|
2073
|
+
kind: "screen_rect",
|
|
2074
|
+
anchor_doc,
|
|
2075
|
+
width: size,
|
|
2076
|
+
height: size,
|
|
2077
|
+
placement: "center",
|
|
2078
|
+
fill: true,
|
|
2079
|
+
stroke: true,
|
|
2080
|
+
fillColor: style.handleFill,
|
|
2081
|
+
strokeColor: style.handleStroke,
|
|
2082
|
+
angle: angle_rad
|
|
2083
|
+
} : void 0,
|
|
2084
|
+
priority: zone.priority,
|
|
2085
|
+
cursor: {
|
|
2086
|
+
kind: "resize",
|
|
2087
|
+
direction: dir
|
|
1514
2088
|
}
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
2089
|
+
});
|
|
2090
|
+
break;
|
|
2091
|
+
}
|
|
2092
|
+
case "resize_edge":
|
|
2093
|
+
out.push({
|
|
2094
|
+
label: zone.label,
|
|
2095
|
+
group,
|
|
2096
|
+
action: {
|
|
2097
|
+
kind: "resize_handle",
|
|
2098
|
+
direction: zone.role.direction,
|
|
2099
|
+
ids,
|
|
2100
|
+
initial_shape
|
|
2101
|
+
},
|
|
2102
|
+
hit,
|
|
2103
|
+
priority: zone.priority,
|
|
2104
|
+
cursor: {
|
|
2105
|
+
kind: "resize",
|
|
2106
|
+
direction: zone.role.direction
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
break;
|
|
2110
|
+
case "rotate":
|
|
2111
|
+
out.push({
|
|
2112
|
+
label: zone.label,
|
|
2113
|
+
group,
|
|
2114
|
+
action: {
|
|
2115
|
+
kind: "rotate_handle",
|
|
2116
|
+
corner: zone.role.corner,
|
|
2117
|
+
ids,
|
|
2118
|
+
initial_shape
|
|
2119
|
+
},
|
|
2120
|
+
hit,
|
|
2121
|
+
priority: zone.priority,
|
|
2122
|
+
cursor: {
|
|
2123
|
+
kind: "rotate",
|
|
2124
|
+
corner: zone.role.corner
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
break;
|
|
1521
2128
|
}
|
|
1522
2129
|
}
|
|
1523
2130
|
}
|
|
1524
|
-
function
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
2131
|
+
function pushLineBody(shape, ids, transform, group, out) {
|
|
2132
|
+
const bounds_doc = shapeBounds(shape);
|
|
2133
|
+
const rect_screen = _grida_cmath.default.rect.transform(bounds_doc, transform);
|
|
2134
|
+
if (rect_screen.width < 1 && rect_screen.height < 1) return;
|
|
2135
|
+
const hitW = Math.max(rect_screen.width, 16);
|
|
2136
|
+
const hitH = Math.max(rect_screen.height, 16);
|
|
2137
|
+
const hitRect = {
|
|
2138
|
+
x: rect_screen.x - (hitW - rect_screen.width) / 2,
|
|
2139
|
+
y: rect_screen.y - (hitH - rect_screen.height) / 2,
|
|
2140
|
+
width: hitW,
|
|
2141
|
+
height: hitH
|
|
1534
2142
|
};
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
function edgeStripsScreen(rect_screen, thickness) {
|
|
1542
|
-
const { x, y, width, height } = rect_screen;
|
|
1543
|
-
const inset = thickness / 2;
|
|
1544
|
-
const half = thickness / 2;
|
|
1545
|
-
return {
|
|
1546
|
-
n: {
|
|
1547
|
-
x: x + inset,
|
|
1548
|
-
y: y - half,
|
|
1549
|
-
width: Math.max(0, width - inset * 2),
|
|
1550
|
-
height: thickness
|
|
1551
|
-
},
|
|
1552
|
-
s: {
|
|
1553
|
-
x: x + inset,
|
|
1554
|
-
y: y + height - half,
|
|
1555
|
-
width: Math.max(0, width - inset * 2),
|
|
1556
|
-
height: thickness
|
|
2143
|
+
out.push({
|
|
2144
|
+
label: "translate",
|
|
2145
|
+
group,
|
|
2146
|
+
action: {
|
|
2147
|
+
kind: "translate_handle",
|
|
2148
|
+
ids
|
|
1557
2149
|
},
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
width: thickness,
|
|
1562
|
-
height: Math.max(0, height - inset * 2)
|
|
2150
|
+
hit: {
|
|
2151
|
+
kind: "screen_aabb",
|
|
2152
|
+
rect: hitRect
|
|
1563
2153
|
},
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
width: thickness,
|
|
1568
|
-
height: Math.max(0, height - inset * 2)
|
|
1569
|
-
}
|
|
1570
|
-
};
|
|
1571
|
-
}
|
|
1572
|
-
function rotationOffsetScreen(corner) {
|
|
1573
|
-
switch (corner) {
|
|
1574
|
-
case "nw": return [-12, -12];
|
|
1575
|
-
case "ne": return [12, -12];
|
|
1576
|
-
case "se": return [12, 12];
|
|
1577
|
-
case "sw": return [-12, 12];
|
|
1578
|
-
}
|
|
2154
|
+
priority: HUDHitPriority.TRANSLATE_BODY,
|
|
2155
|
+
cursor: "move"
|
|
2156
|
+
});
|
|
1579
2157
|
}
|
|
1580
|
-
function pushLineEndpoints(id, p1, p2, style, out) {
|
|
2158
|
+
function pushLineEndpoints(id, p1, p2, style, group, out) {
|
|
1581
2159
|
const size = style.handleSize;
|
|
1582
2160
|
const hit_size = Math.max(size + 4, 16);
|
|
1583
2161
|
const endpoints = [{
|
|
@@ -1588,6 +2166,8 @@ function pushLineEndpoints(id, p1, p2, style, out) {
|
|
|
1588
2166
|
pos: p2
|
|
1589
2167
|
}];
|
|
1590
2168
|
for (const ep of endpoints) out.push({
|
|
2169
|
+
label: `endpoint:${ep.which}`,
|
|
2170
|
+
group,
|
|
1591
2171
|
action: {
|
|
1592
2172
|
kind: "endpoint_handle",
|
|
1593
2173
|
endpoint: ep.which,
|
|
@@ -1613,38 +2193,57 @@ function pushLineEndpoints(id, p1, p2, style, out) {
|
|
|
1613
2193
|
fillColor: style.handleFill,
|
|
1614
2194
|
strokeColor: style.handleStroke
|
|
1615
2195
|
},
|
|
2196
|
+
priority: HUDHitPriority.ENDPOINT_HANDLE,
|
|
1616
2197
|
cursor: "pointer"
|
|
1617
2198
|
});
|
|
1618
2199
|
}
|
|
1619
|
-
function pushShapeOutline(shape, rects, lines, opts) {
|
|
2200
|
+
function pushShapeOutline(shape, rects, lines, polylines, opts) {
|
|
1620
2201
|
if (shape.kind === "rect") rects.push({
|
|
1621
2202
|
...shape.rect,
|
|
1622
2203
|
stroke: true,
|
|
1623
2204
|
fill: false,
|
|
1624
2205
|
dashed: opts.dashed,
|
|
1625
|
-
strokeWidth: opts.strokeWidth
|
|
2206
|
+
strokeWidth: opts.strokeWidth,
|
|
2207
|
+
group: opts.group
|
|
1626
2208
|
});
|
|
1627
|
-
else if (shape.kind === "
|
|
2209
|
+
else if (shape.kind === "transformed") {
|
|
2210
|
+
const corners_doc = _grida_cmath.default.rect.toCorners(shape.local).map((p) => _grida_cmath.default.vector2.transform(p, shape.matrix));
|
|
2211
|
+
polylines.push({
|
|
2212
|
+
points: [...corners_doc, corners_doc[0]],
|
|
2213
|
+
stroke: true,
|
|
2214
|
+
fill: false,
|
|
2215
|
+
dashed: opts.dashed,
|
|
2216
|
+
group: opts.group
|
|
2217
|
+
});
|
|
2218
|
+
} else if (shape.kind === "line") lines.push({
|
|
1628
2219
|
x1: shape.p1[0],
|
|
1629
2220
|
y1: shape.p1[1],
|
|
1630
2221
|
x2: shape.p2[0],
|
|
1631
2222
|
y2: shape.p2[1],
|
|
1632
2223
|
dashed: opts.dashed,
|
|
1633
|
-
strokeWidth: opts.strokeWidth
|
|
2224
|
+
strokeWidth: opts.strokeWidth,
|
|
2225
|
+
group: opts.group
|
|
1634
2226
|
});
|
|
1635
2227
|
}
|
|
1636
2228
|
/**
|
|
1637
2229
|
* Fan a list of `OverlayElement`s into per-primitive render arrays and into
|
|
1638
2230
|
* the hit-region registry. Returns the additional render primitives that
|
|
1639
2231
|
* should be merged with the decoration `HUDDraw`.
|
|
2232
|
+
*
|
|
2233
|
+
* Priority and label are forwarded verbatim from each overlay element to
|
|
2234
|
+
* the registered HitRegion — the registry resolves overlaps by priority.
|
|
1640
2235
|
*/
|
|
1641
2236
|
function fanOverlays(overlays, transform, regions) {
|
|
1642
2237
|
regions.clear();
|
|
1643
2238
|
const screenRects = [];
|
|
1644
2239
|
for (const el of overlays) {
|
|
2240
|
+
const projected = projectHit(el.hit, transform);
|
|
1645
2241
|
regions.push({
|
|
1646
|
-
rect:
|
|
1647
|
-
|
|
2242
|
+
rect: projected.rect,
|
|
2243
|
+
inverse_transform: projected.inverse_transform,
|
|
2244
|
+
action: el.action,
|
|
2245
|
+
priority: el.priority,
|
|
2246
|
+
label: el.label
|
|
1648
2247
|
});
|
|
1649
2248
|
if (!el.render) continue;
|
|
1650
2249
|
if (el.render.kind === "screen_rect") screenRects.push({
|
|
@@ -1656,13 +2255,19 @@ function fanOverlays(overlays, transform, regions) {
|
|
|
1656
2255
|
fill: el.render.fill,
|
|
1657
2256
|
stroke: el.render.stroke,
|
|
1658
2257
|
fillColor: el.render.fillColor,
|
|
1659
|
-
strokeColor: el.render.strokeColor
|
|
2258
|
+
strokeColor: el.render.strokeColor,
|
|
2259
|
+
angle: el.render.angle,
|
|
2260
|
+
group: el.group
|
|
1660
2261
|
});
|
|
1661
2262
|
}
|
|
1662
2263
|
return { screenRects };
|
|
1663
2264
|
}
|
|
1664
|
-
function
|
|
1665
|
-
if (hit.kind === "screen_aabb") return hit.rect;
|
|
2265
|
+
function projectHit(hit, transform) {
|
|
2266
|
+
if (hit.kind === "screen_aabb") return { rect: hit.rect };
|
|
2267
|
+
if (hit.kind === "screen_obb") return {
|
|
2268
|
+
rect: hit.rect,
|
|
2269
|
+
inverse_transform: hit.inverse_transform
|
|
2270
|
+
};
|
|
1666
2271
|
const [sx, sy] = docToScreen(transform, hit.anchor_doc[0], hit.anchor_doc[1]);
|
|
1667
2272
|
const placement = hit.placement ?? "center";
|
|
1668
2273
|
let x = sx;
|
|
@@ -1689,12 +2294,12 @@ function projectHitAABB(hit, transform) {
|
|
|
1689
2294
|
y = sy - hit.height;
|
|
1690
2295
|
break;
|
|
1691
2296
|
}
|
|
1692
|
-
return {
|
|
2297
|
+
return { rect: {
|
|
1693
2298
|
x,
|
|
1694
2299
|
y,
|
|
1695
2300
|
width: hit.width,
|
|
1696
2301
|
height: hit.height
|
|
1697
|
-
};
|
|
2302
|
+
} };
|
|
1698
2303
|
}
|
|
1699
2304
|
/**
|
|
1700
2305
|
* Merge a base `HUDDraw` (chrome decoration) with optional host-fed extras
|
|
@@ -1733,11 +2338,25 @@ var Surface = class {
|
|
|
1733
2338
|
constructor(canvas, options) {
|
|
1734
2339
|
this.width = 0;
|
|
1735
2340
|
this.height = 0;
|
|
2341
|
+
this.cursor_renderer = null;
|
|
1736
2342
|
this.opts = options;
|
|
1737
2343
|
this.style = mergeStyle(DEFAULT_STYLE, options.style);
|
|
1738
|
-
this.
|
|
2344
|
+
this.colorOverride = options.color;
|
|
2345
|
+
this.hudCanvas = new HUDCanvas(canvas, { color: this.colorOverride ?? this.style.chromeColor });
|
|
1739
2346
|
this.state = new SurfaceState();
|
|
1740
2347
|
if (options.readonly !== void 0) this.state.setReadonly(options.readonly);
|
|
2348
|
+
if (options.pixelGrid) this.hudCanvas.setPixelGrid(options.pixelGrid);
|
|
2349
|
+
}
|
|
2350
|
+
/** Configure / disable the back-most pixel-grid layer. */
|
|
2351
|
+
setPixelGrid(config) {
|
|
2352
|
+
this.hudCanvas.setPixelGrid(config);
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Update just the pixel grid's transform. Cheap to call per camera tick.
|
|
2356
|
+
* No-op when no pixel-grid config is set.
|
|
2357
|
+
*/
|
|
2358
|
+
setPixelGridTransform(transform) {
|
|
2359
|
+
this.hudCanvas.setPixelGridTransform(transform);
|
|
1741
2360
|
}
|
|
1742
2361
|
setSize(w, h) {
|
|
1743
2362
|
this.width = w;
|
|
@@ -1763,7 +2382,15 @@ var Surface = class {
|
|
|
1763
2382
|
}
|
|
1764
2383
|
setStyle(partial) {
|
|
1765
2384
|
this.style = mergeStyle(this.style, partial);
|
|
1766
|
-
this.hudCanvas.setColor(this.style.chromeColor);
|
|
2385
|
+
this.hudCanvas.setColor(this.colorOverride ?? this.style.chromeColor);
|
|
2386
|
+
}
|
|
2387
|
+
/**
|
|
2388
|
+
* Set or clear the host color override. `null` clears the override and
|
|
2389
|
+
* lets `style.chromeColor` win on the next paint.
|
|
2390
|
+
*/
|
|
2391
|
+
setColor(color) {
|
|
2392
|
+
this.colorOverride = color ?? void 0;
|
|
2393
|
+
this.hudCanvas.setColor(this.colorOverride ?? this.style.chromeColor);
|
|
1767
2394
|
}
|
|
1768
2395
|
setReadonly(v) {
|
|
1769
2396
|
this.state.setReadonly(v);
|
|
@@ -1800,11 +2427,13 @@ var Surface = class {
|
|
|
1800
2427
|
state: this.state,
|
|
1801
2428
|
shapeOf: this.opts.shapeOf,
|
|
1802
2429
|
style: this.style,
|
|
2430
|
+
groups: this.opts.groups,
|
|
1803
2431
|
width: this.width,
|
|
1804
2432
|
height: this.height
|
|
1805
2433
|
});
|
|
1806
|
-
const
|
|
1807
|
-
|
|
2434
|
+
const hidden = this.opts.visibility?.({ gesture: this.state.gesture })?.hidden;
|
|
2435
|
+
const { screenRects } = fanOverlays(filterOverlaysByGroup(overlays, hidden), this.state.getTransform(), this.state.hitRegions());
|
|
2436
|
+
this.hudCanvas.draw(filterHUDDrawByGroup(mergeDraws(decoration, extra, screenRects), { hidden }));
|
|
1808
2437
|
}
|
|
1809
2438
|
/** Convenience: clear the canvas (e.g. when the host stops the surface). */
|
|
1810
2439
|
clear() {
|
|
@@ -1823,23 +2452,87 @@ var Surface = class {
|
|
|
1823
2452
|
cursor() {
|
|
1824
2453
|
return this.state.cursor;
|
|
1825
2454
|
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Resolve the current cursor to a CSS `cursor:` value. Runs the
|
|
2457
|
+
* installed renderer (or the built-in `cursorToCss` if none installed).
|
|
2458
|
+
*
|
|
2459
|
+
* Host wires it like:
|
|
2460
|
+
*
|
|
2461
|
+
* const r = surface.dispatch(event);
|
|
2462
|
+
* if (r.cursorChanged) el.style.cursor = surface.cursorCss();
|
|
2463
|
+
*
|
|
2464
|
+
* Saves the host from re-importing `cursorToCss` after every dispatch
|
|
2465
|
+
* and gives one place to change behavior when a renderer is swapped in.
|
|
2466
|
+
*/
|
|
2467
|
+
cursorCss() {
|
|
2468
|
+
return (this.cursor_renderer ?? require_cursor.cursorToCss)(this.state.cursor);
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Install (or clear) a custom cursor renderer.
|
|
2472
|
+
*
|
|
2473
|
+
* `null` restores the built-in `cursorToCss` behavior (native CSS
|
|
2474
|
+
* keywords for every variant). Pass `cursors.defaultRenderer()` from
|
|
2475
|
+
* `@grida/hud/cursors` for the bundled SVG cursor set.
|
|
2476
|
+
*
|
|
2477
|
+
* Re-callable mid-session; the next `cursorCss()` reads the new value.
|
|
2478
|
+
*/
|
|
2479
|
+
setCursorRenderer(fn) {
|
|
2480
|
+
this.cursor_renderer = fn;
|
|
2481
|
+
}
|
|
1826
2482
|
modifiers() {
|
|
1827
2483
|
return this.state.modifiers;
|
|
1828
2484
|
}
|
|
1829
2485
|
};
|
|
2486
|
+
function filterOverlaysByGroup(overlays, hidden) {
|
|
2487
|
+
const hidden_set = new Set(hidden ?? []);
|
|
2488
|
+
if (hidden_set.size === 0) return overlays;
|
|
2489
|
+
return overlays.filter((overlay) => {
|
|
2490
|
+
return !overlay.group || !hidden_set.has(overlay.group);
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
1830
2493
|
//#endregion
|
|
2494
|
+
Object.defineProperty(exports, "BODY_FLIP_THRESHOLD", {
|
|
2495
|
+
enumerable: true,
|
|
2496
|
+
get: function() {
|
|
2497
|
+
return BODY_FLIP_THRESHOLD;
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
Object.defineProperty(exports, "DEFAULT_PIXEL_GRID_COLOR", {
|
|
2501
|
+
enumerable: true,
|
|
2502
|
+
get: function() {
|
|
2503
|
+
return DEFAULT_PIXEL_GRID_COLOR;
|
|
2504
|
+
}
|
|
2505
|
+
});
|
|
2506
|
+
Object.defineProperty(exports, "DEFAULT_PIXEL_GRID_STEPS", {
|
|
2507
|
+
enumerable: true,
|
|
2508
|
+
get: function() {
|
|
2509
|
+
return DEFAULT_PIXEL_GRID_STEPS;
|
|
2510
|
+
}
|
|
2511
|
+
});
|
|
1831
2512
|
Object.defineProperty(exports, "HUDCanvas", {
|
|
1832
2513
|
enumerable: true,
|
|
1833
2514
|
get: function() {
|
|
1834
2515
|
return HUDCanvas;
|
|
1835
2516
|
}
|
|
1836
2517
|
});
|
|
2518
|
+
Object.defineProperty(exports, "HUDHitPriority", {
|
|
2519
|
+
enumerable: true,
|
|
2520
|
+
get: function() {
|
|
2521
|
+
return HUDHitPriority;
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
1837
2524
|
Object.defineProperty(exports, "MIN_CHROME_VISIBLE_SIZE", {
|
|
1838
2525
|
enumerable: true,
|
|
1839
2526
|
get: function() {
|
|
1840
2527
|
return MIN_CHROME_VISIBLE_SIZE;
|
|
1841
2528
|
}
|
|
1842
2529
|
});
|
|
2530
|
+
Object.defineProperty(exports, "MIN_GUARANTEED_INTERACTIVE_DIM", {
|
|
2531
|
+
enumerable: true,
|
|
2532
|
+
get: function() {
|
|
2533
|
+
return MIN_GUARANTEED_INTERACTIVE_DIM;
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
1843
2536
|
Object.defineProperty(exports, "MIN_HIT_SIZE", {
|
|
1844
2537
|
enumerable: true,
|
|
1845
2538
|
get: function() {
|
|
@@ -1864,6 +2557,24 @@ Object.defineProperty(exports, "__toESM", {
|
|
|
1864
2557
|
return __toESM;
|
|
1865
2558
|
}
|
|
1866
2559
|
});
|
|
2560
|
+
Object.defineProperty(exports, "computeSelectionControlLayout", {
|
|
2561
|
+
enumerable: true,
|
|
2562
|
+
get: function() {
|
|
2563
|
+
return computeSelectionControlLayout;
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
Object.defineProperty(exports, "drawPixelGrid", {
|
|
2567
|
+
enumerable: true,
|
|
2568
|
+
get: function() {
|
|
2569
|
+
return drawPixelGrid;
|
|
2570
|
+
}
|
|
2571
|
+
});
|
|
2572
|
+
Object.defineProperty(exports, "filterHUDDrawByGroup", {
|
|
2573
|
+
enumerable: true,
|
|
2574
|
+
get: function() {
|
|
2575
|
+
return filterHUDDrawByGroup;
|
|
2576
|
+
}
|
|
2577
|
+
});
|
|
1867
2578
|
Object.defineProperty(exports, "lassoToHUDDraw", {
|
|
1868
2579
|
enumerable: true,
|
|
1869
2580
|
get: function() {
|
|
@@ -1882,6 +2593,12 @@ Object.defineProperty(exports, "measurementToHUDDraw", {
|
|
|
1882
2593
|
return measurementToHUDDraw;
|
|
1883
2594
|
}
|
|
1884
2595
|
});
|
|
2596
|
+
Object.defineProperty(exports, "negotiateAxis", {
|
|
2597
|
+
enumerable: true,
|
|
2598
|
+
get: function() {
|
|
2599
|
+
return negotiateAxis;
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
1885
2602
|
Object.defineProperty(exports, "snapGuideToHUDDraw", {
|
|
1886
2603
|
enumerable: true,
|
|
1887
2604
|
get: function() {
|