Skip to content

Commit e87673c

Browse files
mcdohGavin McDonald
andauthored
Lighter Tweaks (#6404)
* inverse selection opacity * fix drag handle borders and remove scrim from hit detection * short dash for hovered, long dash for selected * refactor drawing handles and scrim * don't abandon moving handlers * maintain bounding box location at the top left * dashed line over solid border * maintain aspect ratio with shift key * fix black flash on resize * scrim should not overlap border --------- Co-authored-by: Gavin McDonald <[email protected]>
1 parent 9fc5efe commit e87673c

File tree

7 files changed

+216
-40
lines changed

7 files changed

+216
-40
lines changed

app/packages/lighter/src/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export const DEFAULT_TEXT_PADDING = 4;
1717
* Default stroke width for overlays (matches Looker's STROKE_WIDTH)
1818
*/
1919
export const STROKE_WIDTH = 3;
20+
export const SELECTED_DASH_LENGTH = 8;
21+
export const HOVERED_DASH_LENGTH = 4;
2022

2123
/**
2224
* Default font settings
@@ -29,9 +31,12 @@ export const FONT_WEIGHT = "bold";
2931
* settings related to resize handles
3032
*/
3133
export const EDGE_THRESHOLD = 10;
34+
export const HANDLE_ALPHA = 0.9;
3235
export const HANDLE_FACTOR = 3;
3336
export const HANDLE_COLOR = 0xffffff;
3437
export const HANDLE_OUTLINE = 2;
38+
export const HANDLE_OFFSET_X = 6;
39+
export const HANDLE_OFFSET_Y = 3;
3540

3641
/**
3742
* Opacity of selected bounding boxes

app/packages/lighter/src/interaction/InteractionManager.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,15 @@ export interface InteractionHandler {
6666
* @param worldPoint - Screen point translated to viewport point.
6767
* @param event - The original pointer event.
6868
* @param scale - The current scaling factor of the renderer.
69+
* @param maintainAspectRatio - Maintain aspect ratio during resize (shift key held).
6970
* @returns True if the event was handled.
7071
*/
7172
onMove?(
7273
point: Point,
7374
worldPoint: Point,
7475
event: PointerEvent,
75-
sclae: number
76+
scale: number,
77+
maintainAspectRatio?: boolean
7678
): boolean;
7779

7880
/**
@@ -154,6 +156,7 @@ export class InteractionManager {
154156
private clickStartPoint?: Point;
155157
private lastClickTime = 0;
156158
private lastClickPoint?: Point;
159+
private maintainAspectRatio = false;
157160

158161
private canonicalMediaId?: string;
159162

@@ -201,6 +204,7 @@ export class InteractionManager {
201204
this.canvas.addEventListener("pointercancel", this.handlePointerCancel);
202205
this.canvas.addEventListener("pointerleave", this.handlePointerLeave);
203206
document.addEventListener("keydown", this.handleKeyDown);
207+
document.addEventListener("keyup", this.handleKeyUp);
204208
this.eventBus.on(LIGHTER_EVENTS.ZOOMED, this.handleZoomed);
205209
}
206210

@@ -266,9 +270,21 @@ export class InteractionManager {
266270
if (handler) {
267271
// Handle drag move
268272
if (!interactiveHandler) {
269-
handler.onMove?.(point, worldPoint, event, scale);
273+
handler.onMove?.(
274+
point,
275+
worldPoint,
276+
event,
277+
scale,
278+
this.maintainAspectRatio
279+
);
270280
} else {
271-
interactiveHandler.onMove?.(point, worldPoint, event, scale);
281+
interactiveHandler.onMove?.(
282+
point,
283+
worldPoint,
284+
event,
285+
scale,
286+
this.maintainAspectRatio
287+
);
272288
}
273289

274290
if (handler.isMoving?.()) {
@@ -301,7 +317,7 @@ export class InteractionManager {
301317
const point = this.getCanvasPoint(event);
302318
const worldPoint = this.renderer.screenToWorld(point);
303319
const scale = this.renderer.getScale();
304-
const handler = this.findHandlerAtPoint(point);
320+
const handler = this.findMovingHandler() || this.findHandlerAtPoint(point);
305321
const now = Date.now();
306322

307323
if (handler?.isMoving?.()) {
@@ -371,7 +387,7 @@ export class InteractionManager {
371387
};
372388

373389
/**
374-
* Handles keyboard events for undo/redo shortcuts.
390+
* Handles keyboard events for undo/redo shortcuts and shift modifier to maintain aspect ratio.
375391
* @param event - The keyboard event.
376392
*/
377393
private handleKeyDown = (event: KeyboardEvent): void => {
@@ -409,6 +425,30 @@ export class InteractionManager {
409425
this.undoRedoManager.redo();
410426
return;
411427
}
428+
429+
if (event.shiftKey) {
430+
this.maintainAspectRatio = event.shiftKey;
431+
return;
432+
}
433+
};
434+
435+
/**
436+
* Handles keyboard events for release of shift modifier to maintain aspect ratio.
437+
* @param event - The keyboard event.
438+
*/
439+
private handleKeyUp = (event: KeyboardEvent): void => {
440+
// Check if we're in an input field - don't handle shortcuts there
441+
const activeElement = document.activeElement;
442+
if (
443+
activeElement &&
444+
(activeElement.tagName === "INPUT" ||
445+
activeElement.tagName === "TEXTAREA" ||
446+
(activeElement as HTMLElement).contentEditable === "true")
447+
) {
448+
return;
449+
}
450+
451+
this.maintainAspectRatio = event.shiftKey;
412452
};
413453

414454
private handleClick(point: Point, event: PointerEvent, now: number): void {
@@ -664,6 +704,7 @@ export class InteractionManager {
664704
this.canvas.removeEventListener("pointercancel", this.handlePointerCancel);
665705
this.canvas.removeEventListener("pointerleave", this.handlePointerLeave);
666706
document.removeEventListener("keydown", this.handleKeyDown);
707+
document.removeEventListener("keyup", this.handleKeyUp);
667708
this.eventBus.off(LIGHTER_EVENTS.ZOOMED, this.handleZoomed);
668709
this.clearHandlers();
669710
}

app/packages/lighter/src/overlay/BoundingBoxOverlay.ts

Lines changed: 103 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {
77
LABEL_ARCHETYPE_PRIORITY,
88
STROKE_WIDTH,
99
EDGE_THRESHOLD,
10+
SELECTED_DASH_LENGTH,
11+
HOVERED_DASH_LENGTH,
12+
HANDLE_OFFSET_X,
13+
HANDLE_OFFSET_Y,
1014
} from "../constants";
1115
import { CONTAINS } from "../core/Scene2D";
1216
import type { Renderer2D } from "../renderer/Renderer2D";
@@ -19,6 +23,7 @@ import type {
1923
Rect,
2024
Spatial,
2125
} from "../types";
26+
import { parseColorWithAlpha } from "../utils/color";
2227
import {
2328
getInstanceStrokeStyles,
2429
getSimpleStrokeStyles,
@@ -167,7 +172,9 @@ export class BoundingBoxOverlay
167172
isSelected: this.isSelectedState,
168173
strokeColor: style.strokeStyle || "#ffffff",
169174
isHovered: this.isHoveredState,
170-
dashLength: 8,
175+
dashLength: this.isSelectedState
176+
? SELECTED_DASH_LENGTH
177+
: HOVERED_DASH_LENGTH,
171178
});
172179

173180
const mainStrokeStyle = {
@@ -176,9 +183,7 @@ export class BoundingBoxOverlay
176183
isSelected: this.isSelectedState,
177184
};
178185

179-
if (style.dashPattern && !overlayStrokeColor && !overlayDash) {
180-
mainStrokeStyle.dashPattern = style.dashPattern;
181-
}
186+
delete mainStrokeStyle.dashPattern;
182187

183188
renderer.drawRect(this.absoluteBounds, mainStrokeStyle, this.containerId);
184189

@@ -187,7 +192,7 @@ export class BoundingBoxOverlay
187192
this.absoluteBounds,
188193
{
189194
strokeStyle: hoverStrokeColor,
190-
lineWidth: style.lineWidth || 2,
195+
lineWidth: style.lineWidth || STROKE_WIDTH,
191196
},
192197
this.containerId
193198
);
@@ -203,15 +208,32 @@ export class BoundingBoxOverlay
203208
);
204209
}
205210

211+
if (this.isSelected() && style.strokeStyle) {
212+
const colorObj = parseColorWithAlpha(style.strokeStyle);
213+
const color = colorObj.color;
214+
215+
renderer.drawScrim(
216+
this.absoluteBounds,
217+
style.lineWidth || STROKE_WIDTH,
218+
this.containerId
219+
);
220+
renderer.drawHandles(
221+
this.absoluteBounds,
222+
style.lineWidth || STROKE_WIDTH,
223+
color,
224+
this.containerId
225+
);
226+
}
227+
206228
if (this.options.label && this.options.label.label?.length > 0) {
207229
const offset = style.lineWidth
208230
? style.lineWidth / renderer.getScale() / 2
209231
: 0;
210232

211233
const labelPosition = this.isSelected()
212234
? {
213-
x: this.absoluteBounds.x + offset * 6,
214-
y: this.absoluteBounds.y - offset * 3,
235+
x: this.absoluteBounds.x + offset * HANDLE_OFFSET_X,
236+
y: this.absoluteBounds.y - offset * HANDLE_OFFSET_Y,
215237
}
216238
: {
217239
x: this.absoluteBounds.x - offset,
@@ -380,14 +402,15 @@ export class BoundingBoxOverlay
380402
point: Point,
381403
worldPoint: Point,
382404
event: PointerEvent,
383-
scale: number
405+
scale: number,
406+
maintainAspectRatio?: boolean
384407
): boolean {
385408
this.calculateMoving(point, worldPoint, scale);
386409

387410
if (this.moveState === "DRAGGING") {
388411
return this.onDrag(point, event, scale);
389412
} else if (this.moveState.startsWith("RESIZE_")) {
390-
return this.onResize(point, event, scale);
413+
return this.onResize(point, event, scale, maintainAspectRatio);
391414
} else {
392415
return false;
393416
}
@@ -414,32 +437,96 @@ export class BoundingBoxOverlay
414437
return true;
415438
}
416439

417-
private onResize(point: Point, _event: PointerEvent, scale: number): boolean {
440+
private onResize(
441+
point: Point,
442+
_event: PointerEvent,
443+
scale: number,
444+
maintainAspectRatio: boolean = false
445+
): boolean {
418446
if (!this.moveStartPoint || !this.moveStartBounds) return false;
419447

420448
const delta = {
421449
x: (point.x - this.moveStartPoint.x) / scale,
422450
y: (point.y - this.moveStartPoint.y) / scale,
423451
};
424452

453+
let maintainX = 0;
454+
let maintainY = 0;
455+
456+
if (maintainAspectRatio) {
457+
const aspectRatio =
458+
this.moveStartBounds.width / this.moveStartBounds.height;
459+
460+
if (
461+
Math.abs(delta.x / this.absoluteBounds.width) >
462+
Math.abs(delta.y / this.absoluteBounds.height)
463+
) {
464+
maintainY = delta.x / aspectRatio;
465+
} else {
466+
maintainX = delta.y * aspectRatio;
467+
}
468+
}
469+
425470
let { x, y, width, height } = this.moveStartBounds;
426471

427472
if (["RESIZE_NW", "RESIZE_N", "RESIZE_NE"].includes(this.moveState)) {
428-
y += delta.y;
429-
height -= delta.y;
473+
maintainY = this.moveState === "RESIZE_NE" ? maintainY * -1 : maintainY;
474+
maintainY = this.moveState === "RESIZE_E" ? 0 : maintainY;
475+
476+
y += maintainY || delta.y;
477+
height -= maintainY || delta.y;
478+
479+
if (this.moveState === "RESIZE_N") {
480+
x += maintainX / 2;
481+
width -= maintainX;
482+
}
430483
}
431484

432485
if (["RESIZE_NW", "RESIZE_W", "RESIZE_SW"].includes(this.moveState)) {
433-
x += delta.x;
434-
width -= delta.x;
486+
maintainX = this.moveState === "RESIZE_SW" ? maintainX * -1 : maintainX;
487+
maintainX = this.moveState === "RESIZE_W" ? 0 : maintainX;
488+
489+
x += maintainX || delta.x;
490+
width -= maintainX || delta.x;
491+
492+
if (this.moveState === "RESIZE_W") {
493+
y += maintainY / 2;
494+
height -= maintainY;
495+
}
435496
}
436497

437498
if (["RESIZE_SW", "RESIZE_S", "RESIZE_SE"].includes(this.moveState)) {
438-
height += delta.y;
499+
maintainY = this.moveState === "RESIZE_SW" ? maintainY * -1 : maintainY;
500+
maintainY = this.moveState === "RESIZE_S" ? 0 : maintainY;
501+
502+
height += maintainY || delta.y;
503+
504+
if (this.moveState === "RESIZE_S") {
505+
x -= maintainX / 2;
506+
width += maintainX;
507+
}
439508
}
440509

441510
if (["RESIZE_NE", "RESIZE_E", "RESIZE_SE"].includes(this.moveState)) {
442-
width += delta.x;
511+
maintainX = this.moveState === "RESIZE_NE" ? maintainX * -1 : maintainX;
512+
maintainX = this.moveState === "RESIZE_E" ? 0 : maintainX;
513+
514+
width += maintainX || delta.x;
515+
516+
if (this.moveState === "RESIZE_E") {
517+
y -= maintainY / 2;
518+
height += maintainY;
519+
}
520+
}
521+
522+
if (width < 0) {
523+
width *= -1;
524+
x -= width;
525+
}
526+
527+
if (height < 0) {
528+
height *= -1;
529+
y -= height;
443530
}
444531

445532
// Update absolute bounds

app/packages/lighter/src/overlay/ClassificationOverlay.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
* Copyright 2017-2025, Voxel51, Inc.
33
*/
44

5-
import { FONT_SIZE, LABEL_ARCHETYPE_PRIORITY } from "../constants";
5+
import {
6+
FONT_SIZE,
7+
LABEL_ARCHETYPE_PRIORITY,
8+
SELECTED_DASH_LENGTH,
9+
} from "../constants";
610
import type { Renderer2D } from "../renderer/Renderer2D";
711
import type { Selectable } from "../selection/Selectable";
812
import type { Hoverable, Point, RawLookerLabel } from "../types";
@@ -67,7 +71,7 @@ export class ClassificationOverlay
6771
const { overlayStrokeColor, overlayDash } = getSimpleStrokeStyles({
6872
isSelected: this.isSelectedState,
6973
strokeColor: style.strokeStyle || "#000000",
70-
dashLength: 8,
74+
dashLength: this.isSelectedState ? SELECTED_DASH_LENGTH : undefined,
7175
});
7276

7377
// Draw the classification text

0 commit comments

Comments
 (0)