diff --git a/app/components/index.js b/app/components/index.js
index f6aeb776..6c7396f3 100644
--- a/app/components/index.js
+++ b/app/components/index.js
@@ -1,3 +1,8 @@
+import { addPolyfill } from 'get-box-quads-polyfill'
+
+// The polyfill preserves native GeometryUtils methods and only fills in missing ones.
+addPolyfill(window)
+
export { Handles } from './selection/handles.element'
export { Handle } from './selection/handle.element'
export { Hover } from './selection/hover.element'
diff --git a/app/components/selection/box-model.element.css b/app/components/selection/box-model.element.css
index 7d116455..fc664d37 100644
--- a/app/components/selection/box-model.element.css
+++ b/app/components/selection/box-model.element.css
@@ -1,18 +1,13 @@
:host [mask] {
+ inset: 0;
+ height: 100%;
pointer-events: none;
position: absolute;
+ width: 100%;
z-index: var(--layer-5);
- width: var(--width);
- height: var(--height);
- top: var(--top);
- left: var(--left);
- background-color: var(--bg);
- clip-path: polygon(
- 0% 0%, 0% 100%, var(--target-left) 100%,
- var(--target-left) var(--target-top),
- var(--offset-right) var(--target-top),
- var(--offset-right) var(--offset-bottom),
- 0 var(--offset-bottom), 0 100%,
- 100% 100%, 100% 0%
- );
-}
\ No newline at end of file
+}
+
+:host svg {
+ display: block;
+ overflow: visible;
+}
diff --git a/app/components/selection/box-model.element.js b/app/components/selection/box-model.element.js
index dbeb8c0f..89182149 100644
--- a/app/components/selection/box-model.element.js
+++ b/app/components/selection/box-model.element.js
@@ -1,4 +1,5 @@
import { BoxModelStyles } from '../styles.store'
+import { getBoxQuad, quadBounds, quadPath, sideMidpoint } from './quad'
export class BoxModel extends HTMLElement {
@@ -16,15 +17,21 @@ export class BoxModel extends HTMLElement {
set position(payload) {
this.$shadow.innerHTML = this.render(payload)
- if (!this.drawable.measurements) // && payload.color === 'pink'
- this.createMeasurements(payload)
+ this.createMeasurements({...payload, ...this.drawable.measurementQuads})
}
- render({mode, bounds, sides, color = 'pink'}) {
+ render({mode, bounds, sides, color = 'pink', element}) {
const total_height = bounds.height + sides.bottom + sides.top
const total_width = bounds.width + sides.right + sides.left
+ const borderQuad = getBoxQuad(element)
+ const borderBounds = quadBounds(borderQuad)
+ const origin = {x: borderBounds.left, y: borderBounds.top}
+ let outerQuad
+ let innerQuad
if (mode === 'padding') {
+ outerQuad = getBoxQuad(element, 'padding')
+ innerQuad = getBoxQuad(element, 'content')
this.drawable = {
height: bounds.height - (sides.borders.top + sides.borders.bottom),
width: bounds.width - (sides.borders.right + sides.borders.left),
@@ -34,6 +41,8 @@ export class BoxModel extends HTMLElement {
}
}
else if (mode === 'margin') {
+ outerQuad = getBoxQuad(element, 'margin')
+ innerQuad = borderQuad
this.drawable = {
height: total_height,
width: total_width,
@@ -43,6 +52,9 @@ export class BoxModel extends HTMLElement {
}
}
+ this.drawable.d = `${quadPath(outerQuad, origin)} ${quadPath(innerQuad, origin)}`
+ this.drawable.measurementQuads = {outerQuad, innerQuad, origin}
+
if (color === 'pink') {
this.drawable.bg = 'color(display-p3 1 0 1 / 15%)'
this.drawable.stripe = 'color(display-p3 1 0 1 / 80%)'
@@ -56,13 +68,14 @@ export class BoxModel extends HTMLElement {
return `
`
@@ -84,94 +97,27 @@ export class BoxModel extends HTMLElement {
this.style.setProperty('--offset-bottom', `${this.drawable.height - sides.bottom}px`)
}
- createMeasurements({mode, bounds, sides, color}) {
- const win_width = window.innerWidth
- const pill_height = 18
- const offset = 3
-
- if (mode === 'margin') {
- if (sides.top) {
- this.createMeasurement({
- x: (bounds.width / 2) - offset,
- y: (window.scrollY * -1) - sides.top,
- d: sides.top,
- q: 'top',
- v: true,
- color,
- })
- }
- if (sides.bottom) {
- this.createMeasurement({
- x: (bounds.width / 2) - offset,
- y: (window.scrollY * -1) + bounds.height,
- d: sides.bottom,
- q: 'bottom',
- v: true,
- color,
- })
- }
- if (sides.right) {
- this.createMeasurement({
- x: bounds.width,
- y: (window.scrollY * -1) + (bounds.height / 2) - offset,
- d: sides.right,
- q: 'right',
- v: false,
- color,
- })
- }
- if (sides.left) {
- this.createMeasurement({
- x: sides.left * -1,
- y: (window.scrollY * -1) + (bounds.height / 2) - offset,
- d: sides.left,
- q: 'left',
- v: false,
- color,
- })
- }
- }
- else if (mode === 'padding') {
- if (sides.top) {
- this.createMeasurement({
- x: (bounds.width / 2) - offset,
- y: (window.scrollY * -1) + sides.borders.top,
- d: sides.top,
- q: 'top',
- v: true,
- color,
- })
- }
- if (sides.bottom) {
- this.createMeasurement({
- x: (bounds.width / 2) - offset,
- y: (window.scrollY * -1) + (bounds.height - sides.bottom - sides.borders.bottom),
- d: sides.bottom,
- q: 'bottom',
- v: true,
- color,
- })
- }
- if (sides.right) {
- this.createMeasurement({
- x: bounds.width - sides.right - sides.borders.right,
- y: (window.scrollY * -1) + (bounds.height / 2) - offset,
- d: sides.right,
- q: 'right',
- v: false,
- color,
- })
- }
- if (sides.left) {
- this.createMeasurement({
- x: 0 + sides.borders.left,
- y: (window.scrollY * -1) + (bounds.height / 2) - offset,
- d: sides.left,
- q: 'left',
- v: false,
- color,
- })
- }
+ createMeasurements({sides, color, outerQuad, innerQuad, origin}) {
+ for (const side of ['top', 'right', 'bottom', 'left']) {
+ if (!sides[side]) continue
+
+ const start = sideMidpoint(outerQuad, side)
+ const end = sideMidpoint(innerQuad, side)
+ const dx = end.x - start.x
+ const dy = end.y - start.y
+
+ this.createMeasurement({
+ x: start.x - origin.x,
+ y: start.y - origin.y,
+ d: sides[side],
+ length: Math.hypot(dx, dy),
+ angle: Math.atan2(dy, dx),
+ q: side,
+ v: false,
+ local: true,
+ centered: true,
+ color,
+ })
}
}
diff --git a/app/components/selection/distance.element.css b/app/components/selection/distance.element.css
index 5c0d959c..f89f7013 100644
--- a/app/components/selection/distance.element.css
+++ b/app/components/selection/distance.element.css
@@ -30,6 +30,8 @@
align-items: center;
justify-content: var(--justify, 'flex-start');
flex-direction: var(--direction);
+ transform: rotate(var(--angle, 0rad));
+ transform-origin: top left;
}
:host > figure figcaption {
@@ -49,6 +51,7 @@
line-height: 1.1;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
padding: 0 1ex;
+ transform: rotate(var(--caption-angle, 0rad));
}
:host > figure span {
diff --git a/app/components/selection/distance.element.js b/app/components/selection/distance.element.js
index 36a68e89..81d95200 100644
--- a/app/components/selection/distance.element.js
+++ b/app/components/selection/distance.element.js
@@ -21,19 +21,23 @@ export class Distance extends HTMLElement {
this.$shadow.innerHTML = this.render(line_model, node_label_id)
}
- set styleProps({y,x,d,q,v = false, color}) {
- this.style.setProperty('--top', `${Math.round(y + window.scrollY)}px`)
+ set styleProps({y,x,d,q,v = false, color, local = false, length = d, angle = 0, centered = false}) {
+ this.style.setProperty('--top', `${Math.round(y + (local ? 0 : window.scrollY))}px`)
this.style.setProperty('--right', 'auto')
this.style.setProperty('--left', `${x}px`)
this.style.setProperty('--direction', v ? 'column' : 'row')
this.style.setProperty('--quadrant', q)
+ this.style.setProperty('--angle', `${angle}rad`)
+ this.style.setProperty('--caption-angle', `${angle * -1}rad`)
- if (q === 'left')
+ if (centered)
+ this.style.setProperty('--justify', 'center')
+ else if (q === 'left')
this.style.setProperty('--justify', 'flex-end')
v
- ? this.style.setProperty('--distance-h', `${d}px`)
- : this.style.setProperty('--distance-w', `${d}px`)
+ ? this.style.setProperty('--distance-h', `${length}px`)
+ : this.style.setProperty('--distance-w', `${length}px`)
v
? this.style.setProperty('--line-h', `var(--line-w)`)
diff --git a/app/components/selection/handle.element.css b/app/components/selection/handle.element.css
index 9d26d5a0..7c039f6b 100644
--- a/app/components/selection/handle.element.css
+++ b/app/components/selection/handle.element.css
@@ -4,6 +4,7 @@
display: grid;
grid-area: 1 / -1;
place-self: var(--align-self, center) var(--justify-self, center);
+ position: absolute;
transform: translate(var(--translate-x, 0), var(--translate-y, 0));
}
@@ -30,24 +31,8 @@
}
}
-:host([placement^="top"]) {
- --align-self: start;
- --translate-y: -50%;
-}
-
-:host([placement^="bottom"]) {
- --align-self: end;
- --translate-y: 50%;
-}
-
-:host([placement$="start"]) {
- --justify-self: start;
- --translate-x: -50%;
-}
-
-:host([placement$="end"]) {
- --justify-self: end;
- --translate-x: 50%;
+:host {
+ translate: -50% -50%;
}
:host([placement^="top"]),
diff --git a/app/components/selection/handle.element.js b/app/components/selection/handle.element.js
index 6721aaf9..607b9710 100644
--- a/app/components/selection/handle.element.js
+++ b/app/components/selection/handle.element.js
@@ -1,6 +1,5 @@
import $ from 'blingblingjs'
import { HandleStyles } from '../styles.store'
-import { clamp } from '../../utilities/numbers'
export class Handle extends HTMLElement {
@@ -8,14 +7,15 @@ export class Handle extends HTMLElement {
super()
this.$shadow = this.attachShadow({mode: 'closed'})
this.styles = [HandleStyles]
+ this.on_resize_start = this.on_element_resize_start.bind(this)
}
connectedCallback() {
this.$shadow.adoptedStyleSheets = this.styles
this.$shadow.innerHTML = this.render()
-
+
this.button = this.$shadow.querySelector('button')
- this.button.addEventListener('pointerdown', this.on_element_resize_start.bind(this))
+ this.button.addEventListener('pointerdown', this.on_resize_start)
this.placement = this.getAttribute('placement')
}
@@ -30,6 +30,9 @@ export class Handle extends HTMLElement {
}
}
+ /**
+ * @param {PointerEvent} e
+ */
on_element_resize_start(e) {
e.preventDefault()
e.stopPropagation()
@@ -39,11 +42,14 @@ export class Handle extends HTMLElement {
const placement = this.placement
const handlesEl = e.composedPath().find(el => el.tagName === 'VISBUG-HANDLES')
const nodeLabelId = handlesEl.getAttribute('data-label-id')
+ /** @type {Element[]} */
const [sourceEl] = $(`[data-label-id="${nodeLabelId}"]`)
if (!sourceEl) return
- const { x: initialX, y: initialY } = e
+ const t = sourceEl.convertPointFromNode(e, document.documentElement)
+
+ const { x: initialX, y: initialY } = t
const initialStyle = getComputedStyle(sourceEl)
const initialWidth = parseFloat(initialStyle.width)
const initialHeight = parseFloat(initialStyle.height)
@@ -62,92 +68,82 @@ export class Handle extends HTMLElement {
e.preventDefault()
e.stopPropagation()
- const newX = clamp(0, e.clientX, document.documentElement.clientWidth)
- const newY = clamp(0, e.clientY, document.documentElement.clientHeight)
-
+ const {x: newX, y: newY} = sourceEl.convertPointFromNode(e, document.documentElement)
+
const diffX = newX - initialX
const diffY = newY - initialY
+ const leftWidth = Math.max(0, initialWidth - diffX)
+ const rightWidth = Math.max(0, initialWidth + diffX)
+ const topHeight = Math.max(0, initialHeight - diffY)
+ const bottomHeight = Math.max(0, initialHeight + diffY)
+ const leftShift = initialWidth - leftWidth
+ const topShift = initialHeight - topHeight
switch (placement) {
case 'top-start': {
- const newWidth = initialWidth - diffX
- const newHeight = initialHeight - diffY
- const newTranslate = initialTransform.translate(diffX, diffY).transformPoint()
+ const newTransform = initialTransform.translate(leftShift, topShift)
requestAnimationFrame(() => {
- sourceEl.style.width = `${newWidth}px`
- sourceEl.style.height = `${newHeight}px`
- sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
+ sourceEl.style.width = `${leftWidth}px`
+ sourceEl.style.height = `${topHeight}px`
+ sourceEl.style.transform = newTransform.toString()
})
break
}
case 'top-center': {
- const newHeight = initialHeight - diffY
- const newTranslate = initialTransform.translate(0, diffY).transformPoint()
+ const newTransform = initialTransform.translate(0, topShift)
requestAnimationFrame(() => {
- sourceEl.style.height = `${newHeight}px`
- sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
+ sourceEl.style.height = `${topHeight}px`
+ sourceEl.style.transform = newTransform.toString()
})
break
}
case 'top-end': {
- const newWidth = initialWidth + diffX
- const newHeight = initialHeight - diffY
- const newTranslate = initialTransform.translate(0, diffY).transformPoint()
+ const newTransform = initialTransform.translate(0, topShift)
requestAnimationFrame(() => {
- sourceEl.style.width = `${newWidth}px`
- sourceEl.style.height = `${newHeight}px`
- sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
+ sourceEl.style.width = `${rightWidth}px`
+ sourceEl.style.height = `${topHeight}px`
+ sourceEl.style.transform = newTransform.toString()
})
break
}
case 'middle-start': {
- const newWidth = initialWidth - diffX
- const newTranslate = initialTransform.translate(diffX).transformPoint()
+ const newTransform = initialTransform.translate(leftShift)
requestAnimationFrame(() => {
- sourceEl.style.width = `${newWidth}px`
- sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
+ sourceEl.style.width = `${leftWidth}px`
+ sourceEl.style.transform = newTransform.toString()
})
break
}
case 'middle-end': {
- const newWidth = initialWidth + diffX
-
requestAnimationFrame(() => {
- sourceEl.style.width = `${newWidth}px`
+ sourceEl.style.width = `${rightWidth}px`
})
break
}
case 'bottom-start': {
- const newWidth = initialWidth - diffX
- const newHeight = initialHeight + diffY
- const newTranslate = initialTransform.translate(diffX, 0).transformPoint()
+ const newTransform = initialTransform.translate(leftShift, 0)
requestAnimationFrame(() => {
- sourceEl.style.width = `${newWidth}px`
- sourceEl.style.height = `${newHeight}px`
- sourceEl.style.transform = `translate(${newTranslate.x}px, ${newTranslate.y}px)`
+ sourceEl.style.width = `${leftWidth}px`
+ sourceEl.style.height = `${bottomHeight}px`
+ sourceEl.style.transform = newTransform.toString()
})
break
}
case 'bottom-center': {
- const newHeight = initialHeight + diffY
-
requestAnimationFrame(() => {
- sourceEl.style.height = `${newHeight}px`
+ sourceEl.style.height = `${bottomHeight}px`
})
break
}
case 'bottom-end': {
- const newWidth = initialWidth + diffX
- const newHeight = initialHeight + diffY
-
requestAnimationFrame(() => {
- sourceEl.style.width = `${newWidth}px`
- sourceEl.style.height = `${newHeight}px`
+ sourceEl.style.width = `${rightWidth}px`
+ sourceEl.style.height = `${bottomHeight}px`
})
break
}
@@ -166,7 +162,7 @@ export class Handle extends HTMLElement {
}
disconnectedCallback() {
- this.button.removeEventListener('pointerdown', this.on_element_resize_start.bind(this))
+ this.button && this.button.removeEventListener('pointerdown', this.on_resize_start)
}
render() {
diff --git a/app/components/selection/handles.element.css b/app/components/selection/handles.element.css
index 25085966..575c5d65 100644
--- a/app/components/selection/handles.element.css
+++ b/app/components/selection/handles.element.css
@@ -1,7 +1,7 @@
@import "../_variables.css";
:host {
- position: var(--position, 'absolute');
+ position: var(--position, absolute);
inset: var(--top) auto auto var(--left);
background: transparent;
border: none;
diff --git a/app/components/selection/handles.element.js b/app/components/selection/handles.element.js
index 90785ea2..d7a890c7 100644
--- a/app/components/selection/handles.element.js
+++ b/app/components/selection/handles.element.js
@@ -1,6 +1,6 @@
-import $ from 'blingblingjs'
import { HandlesStyles } from '../styles.store'
-import { isFixed } from '../../utilities/';
+import { isFixed } from '../../utilities/'
+import { getBoxQuad, quadBounds, quadPath, sideMidpoint } from './quad'
export class Handles extends HTMLElement {
@@ -8,39 +8,43 @@ export class Handles extends HTMLElement {
super()
this.$shadow = this.attachShadow({mode: 'closed'})
this.styles = [HandlesStyles]
- this.on_resize = this.on_window_resize.bind(this)
+ this.on_position_change = this.on_position_change.bind(this)
+ this.position_frame = null
+ this.source_el = null
}
connectedCallback() {
this.$shadow.adoptedStyleSheets = this.styles
this.setAttribute('popover', 'manual')
this.showPopover && this.showPopover()
- window.addEventListener('resize', this.on_window_resize)
+ window.addEventListener('resize', this.on_position_change)
+ window.addEventListener('scroll', this.on_position_change, true)
}
disconnectedCallback() {
- if (this.hidePopover && this.hidePopover()) this.hidePopover && this.hidePopover()
- window.removeEventListener('resize', this.on_window_resize)
+ this.hidePopover && this.hidePopover()
+ window.removeEventListener('resize', this.on_position_change)
+ window.removeEventListener('scroll', this.on_position_change, true)
+ this.position_frame && window.cancelAnimationFrame(this.position_frame)
}
- on_window_resize() {
- if (!this.$shadow) return
- window.requestAnimationFrame(() => {
- const node_label_id = this.$shadow.host.getAttribute('data-label-id')
- const [source_el] = $(`[data-label-id="${node_label_id}"]`)
+ on_position_change() {
+ if (this.position_frame || !this.source_el) return
- if (!source_el) return
+ this.position_frame = window.requestAnimationFrame(() => {
+ this.position_frame = null
+ if (!this.source_el.isConnected) return
this.position = {
- node_label_id,
- el: source_el,
- isFixed: isFixed(source_el),
+ node_label_id: this.getAttribute('data-label-id'),
+ el: this.source_el,
}
})
}
set position({el, node_label_id}) {
- this.$shadow.innerHTML = this.render(el.getBoundingClientRect(), node_label_id, isFixed(el))
+ this.source_el = el
+ this.$shadow.innerHTML = this.render(getBoxQuad(el), node_label_id, isFixed(el))
if (this._backdrop) {
this.backdrop = {
@@ -60,11 +64,31 @@ export class Handles extends HTMLElement {
: this.$shadow.appendChild(bd.element)
}
- render({ x, y, width, height, top, left }, node_label_id, isFixed) {
+ /**
+ *
+ * @param {DOMQuad} quad
+ * @param {string} node_label_id
+ * @param {boolean} isFixed
+ * @returns
+ */
+ render(quad, node_label_id, isFixed) {
this.$shadow.host.setAttribute('data-label-id', node_label_id)
+ const {left, top, width, height} = quadBounds(quad)
+ const origin = {x: left, y: top}
+ const positions = {
+ 'top-start': quad.p1,
+ 'top-center': sideMidpoint(quad, 'top'),
+ 'top-end': quad.p2,
+ 'middle-start': sideMidpoint(quad, 'left'),
+ 'middle-end': sideMidpoint(quad, 'right'),
+ 'bottom-start': quad.p4,
+ 'bottom-center': sideMidpoint(quad, 'bottom'),
+ 'bottom-end': quad.p3,
+ }
+
this.style.setProperty('--top', `${top + (isFixed ? 0 : window.scrollY)}px`)
- this.style.setProperty('--left', `${left}px`)
+ this.style.setProperty('--left', `${left + (isFixed ? 0 : window.scrollX)}px`)
this.style.setProperty('--position', isFixed ? 'fixed' : 'absolute')
this.style.setProperty('--width', `${width}px`)
this.style.setProperty('--height', `${height}px`)
@@ -76,16 +100,13 @@ export class Handles extends HTMLElement {
viewBox="0 0 ${width} ${height}"
version="1.1" xmlns="http://www.w3.org/2000/svg"
>
-
+
-
-
-
-
-
-
-
-
+ ${Object.entries(positions).map(([placement, point]) => `
+ `).join('')}
`
}
}
diff --git a/app/components/selection/hover.element.css b/app/components/selection/hover.element.css
index 82f824d9..24d16609 100644
--- a/app/components/selection/hover.element.css
+++ b/app/components/selection/hover.element.css
@@ -1,6 +1,7 @@
@import "../_variables.css";
-:host rect {
+:host rect,
+:host path {
width: 100%;
height: 100%;
vector-effect: non-scaling-stroke;
diff --git a/app/components/selection/hover.element.js b/app/components/selection/hover.element.js
index ee4175de..412b6373 100644
--- a/app/components/selection/hover.element.js
+++ b/app/components/selection/hover.element.js
@@ -1,5 +1,6 @@
import { Handles } from './handles.element'
import { HandlesStyles, HoverStyles } from '../styles.store'
+import { quadBounds, quadPath } from './quad'
export class Hover extends Handles {
@@ -14,17 +15,22 @@ export class Hover extends Handles {
disconnectedCallback() {}
- render({ width, height, top, left }, node_label_id, isFixed) {
+ render(quad, node_label_id, isFixed) {
+ const {width, height, top, left} = quadBounds(quad)
+
this.style.setProperty('--top', `${top + (isFixed ? 0 : window.scrollY)}px`)
- this.style.setProperty('--left', `${left}px`)
+ this.style.setProperty('--left', `${left + (isFixed ? 0 : window.scrollX)}px`)
this.style.setProperty('--position', isFixed ? 'fixed' : 'absolute')
+ this.style.setProperty('--width', `${width}px`)
+ this.style.setProperty('--height', `${height}px`)
return `
`
}
}
diff --git a/app/components/selection/quad.js b/app/components/selection/quad.js
new file mode 100644
index 00000000..01b94033
--- /dev/null
+++ b/app/components/selection/quad.js
@@ -0,0 +1,60 @@
+import { getBoxQuads as getPolyfillBoxQuads } from 'get-box-quads-polyfill'
+
+export const quadBounds = quad => {
+ const points = [quad.p1, quad.p2, quad.p3, quad.p4]
+ const left = Math.min(...points.map(point => point.x))
+ const right = Math.max(...points.map(point => point.x))
+ const top = Math.min(...points.map(point => point.y))
+ const bottom = Math.max(...points.map(point => point.y))
+
+ return {
+ left,
+ right,
+ top,
+ bottom,
+ width: right - left,
+ height: bottom - top,
+ }
+}
+
+export const quadPath = (quad, origin = {x: 0, y: 0}) =>
+ `M${[quad.p1, quad.p2, quad.p3, quad.p4]
+ .map(point => `${point.x - origin.x},${point.y - origin.y}`)
+ .join(' ')}Z`
+
+export const sideMidpoint = (quad, side) => {
+ const [start, end] = {
+ top: [quad.p1, quad.p2],
+ right: [quad.p2, quad.p3],
+ bottom: [quad.p4, quad.p3],
+ left: [quad.p1, quad.p4],
+ }[side]
+
+ return {
+ x: start.x + ((end.x - start.x) / 2),
+ y: start.y + ((end.y - start.y) / 2),
+ }
+}
+
+const rectToQuad = ({left, right, top, bottom}) => ({
+ p1: {x: left, y: top},
+ p2: {x: right, y: top},
+ p3: {x: right, y: bottom},
+ p4: {x: left, y: bottom},
+})
+
+export const getBoxQuad = (element, box = 'border') => {
+ try {
+ const [quad] = element.getBoxQuads({box})
+ if (quad) return quad
+ }
+ catch {}
+
+ try {
+ const [quad] = getPolyfillBoxQuads(element, {box})
+ if (quad) return quad
+ }
+ catch {}
+
+ return rectToQuad(element.getBoundingClientRect())
+}
diff --git a/app/components/selection/quad.test.js b/app/components/selection/quad.test.js
new file mode 100644
index 00000000..f68da220
--- /dev/null
+++ b/app/components/selection/quad.test.js
@@ -0,0 +1,56 @@
+import test from 'ava'
+import { getBoxQuad, quadBounds, quadPath, sideMidpoint } from './quad'
+
+const quad = {
+ p1: {x: 10, y: 20},
+ p2: {x: 50, y: 10},
+ p3: {x: 60, y: 40},
+ p4: {x: 20, y: 50},
+}
+
+test('calculates the axis-aligned bounds of a transformed quad', t => {
+ t.deepEqual(quadBounds(quad), {
+ left: 10,
+ right: 60,
+ top: 10,
+ bottom: 50,
+ width: 50,
+ height: 40,
+ })
+})
+
+test('creates a local SVG path for a transformed quad', t => {
+ t.is(quadPath(quad, {x: 10, y: 10}), 'M0,10 40,0 50,30 10,40Z')
+})
+
+test('calculates transformed side midpoints', t => {
+ t.deepEqual(sideMidpoint(quad, 'top'), {x: 30, y: 15})
+ t.deepEqual(sideMidpoint(quad, 'right'), {x: 55, y: 25})
+ t.deepEqual(sideMidpoint(quad, 'bottom'), {x: 40, y: 45})
+ t.deepEqual(sideMidpoint(quad, 'left'), {x: 15, y: 35})
+})
+
+test('uses getBoxQuads when the element provides a quad', t => {
+ const element = {
+ getBoxQuads: options => {
+ t.deepEqual(options, {box: 'padding'})
+ return [quad]
+ },
+ }
+
+ t.is(getBoxQuad(element, 'padding'), quad)
+})
+
+test('falls back to getBoundingClientRect for an empty quad list', t => {
+ const element = {
+ getBoxQuads: () => [],
+ getBoundingClientRect: () => ({left: 1, right: 5, top: 2, bottom: 8}),
+ }
+
+ t.deepEqual(getBoxQuad(element), {
+ p1: {x: 1, y: 2},
+ p2: {x: 5, y: 2},
+ p3: {x: 5, y: 8},
+ p4: {x: 1, y: 8},
+ })
+})
diff --git a/app/features/margin.js b/app/features/margin.js
index 8b66ff30..23dc1b52 100644
--- a/app/features/margin.js
+++ b/app/features/margin.js
@@ -108,11 +108,12 @@ export function createMarginVisual(el, hover = false) {
sides[side] = Math.round(val.toFixed(1) * 100) / 100
})
- boxdisplay.position = {
+ boxdisplay.position = {
mode: 'margin',
color: hover ? 'purple' : 'pink',
- bounds,
+ bounds,
sides,
+ element: el
}
}
diff --git a/app/features/padding.js b/app/features/padding.js
index 9f672b12..94c5e5ee 100644
--- a/app/features/padding.js
+++ b/app/features/padding.js
@@ -109,14 +109,15 @@ export function createPaddingVisual(el, hover = false) {
sides[side] = Math.round(val.toFixed(1) * 100) / 100
})
- boxdisplay.position = {
+ boxdisplay.position = {
mode: 'padding',
color: hover ? 'purple' : 'pink',
- bounds,
+ bounds,
sides: {
...sides,
borders: calculatedBorder,
},
+ element: el
}
}
diff --git a/package-lock.json b/package-lock.json
index 7f890141..3a01fd51 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"blingblingjs": "^2.3.0",
"colorjs.io": "^0.5.0",
"construct-style-sheets-polyfill": "^2.4.2",
+ "get-box-quads-polyfill": "^4.47.0",
"hotkeys-js": "^3.13.7",
"query-selector-shadow-dom": "^1.0.1"
},
@@ -8234,6 +8235,12 @@
"loader-utils": "^1.1.0"
}
},
+ "node_modules/get-box-quads-polyfill": {
+ "version": "4.47.0",
+ "resolved": "https://registry.npmjs.org/get-box-quads-polyfill/-/get-box-quads-polyfill-4.47.0.tgz",
+ "integrity": "sha512-e/zXXHWm0wYuizK9XsWHOjyxdrPNwv0qIkMAaJIOQ1eovlGWecjHGlNAjkLO4cWlABZdVN6XC3OncaRgoY/ong==",
+ "license": "MIT"
+ },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
diff --git a/package.json b/package.json
index dc0b3c3b..fce3bfcc 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"blingblingjs": "^2.3.0",
"colorjs.io": "^0.5.0",
"construct-style-sheets-polyfill": "^2.4.2",
+ "get-box-quads-polyfill": "^4.47.0",
"hotkeys-js": "^3.13.7",
"query-selector-shadow-dom": "^1.0.1"
},