|
1 | 1 | <script lang="ts"> |
2 | 2 | import { onMount } from "svelte"; |
3 | 3 |
|
| 4 | + const SELECTION_ENDPOINT_SIZE = 5; |
4 | 5 | const RULER_THICKNESS = 16; |
5 | 6 | const MAJOR_MARK_THICKNESS = 16; |
6 | 7 | const MINOR_MARK_THICKNESS = 6; |
|
19 | 20 | export let minorDivisions = 5; |
20 | 21 | export let microDivisions = 2; |
21 | 22 | export let cursorPosition: { x: number; y: number } | undefined = undefined; |
| 23 | + export let selectionQuad: [number, number][] | undefined = undefined; |
22 | 24 |
|
23 | 25 | let rulerInput: HTMLDivElement | undefined; |
24 | 26 | let rulerLength = 0; |
|
37 | 39 | $: svgPath = computeSvgPath(direction, effectiveOrigin, stretchedSpacing, stretchFactor, minorDivisions, microDivisions, rulerLength, crossAxisDirection); |
38 | 40 | $: svgTexts = computeSvgTexts(direction, effectiveOrigin, stretchedSpacing, numberInterval, rulerLength, trackedAxis, crossAxisDirection); |
39 | 41 | $: cursorIndicatorPath = computeCursorIndicator(direction, cursorPosition, crossAxisDirection); |
| 42 | + $: selectionExtent = computeSelectionExtent(direction, selectionQuad, crossAxisDirection); |
40 | 43 |
|
41 | 44 | function computeAxes(tilt: number): { horiz: Axis; vert: Axis } { |
42 | 45 | const normTilt = ((tilt % TAU) + TAU) % TAU; |
|
165 | 168 | return `M${sx},${sy}l${dx * length},${dy * length}`; |
166 | 169 | } |
167 | 170 |
|
| 171 | + function computeSelectionExtent(direction: RulerDirection, quad: [number, number][] | undefined, crossAxisDirection: [number, number]): { min: number; max: number } | undefined { |
| 172 | + if (!quad || quad.length === 0) return undefined; |
| 173 | +
|
| 174 | + const projected = quad.map(([x, y]) => projectOntoRuler(direction, x, y, crossAxisDirection)); |
| 175 | +
|
| 176 | + return { min: Math.min(...projected), max: Math.max(...projected) }; |
| 177 | + } |
| 178 | +
|
168 | 179 | export function resize() { |
169 | 180 | if (!rulerInput) return; |
170 | 181 |
|
|
190 | 201 | onMount(resize); |
191 | 202 | </script> |
192 | 203 |
|
193 | | -<div class={`ruler-input ${direction.toLowerCase()}`} bind:this={rulerInput}> |
194 | | - <svg style:width={svgBounds.width} style:height={svgBounds.height}> |
195 | | - <path d={svgPath} /> |
196 | | - {#each svgTexts as svgText} |
197 | | - <text transform={svgText.transform}>{svgText.text}</text> |
198 | | - {/each} |
199 | | - {#if cursorIndicatorPath} |
200 | | - <path class="cursor-indicator" d={cursorIndicatorPath} /> |
201 | | - {/if} |
202 | | - </svg> |
| 204 | +<div class="ruler-input"> |
| 205 | + <div class={`ruler-area ${direction === "Horizontal" ? "horizontal" : "vertical"}`} bind:this={rulerInput}> |
| 206 | + <svg style:width={svgBounds.width} style:height={svgBounds.height}> |
| 207 | + <path d={svgPath} /> |
| 208 | + {#each svgTexts as svgText} |
| 209 | + <text transform={svgText.transform}>{svgText.text}</text> |
| 210 | + {/each} |
| 211 | + {#if cursorIndicatorPath} |
| 212 | + <path class="cursor-indicator" d={cursorIndicatorPath} /> |
| 213 | + {/if} |
| 214 | + </svg> |
| 215 | + </div> |
| 216 | + {#if selectionExtent} |
| 217 | + {@const isVertical = direction === "Vertical"} |
| 218 | + {@const minPos = Math.round(selectionExtent.min)} |
| 219 | + {@const maxPos = Math.round(selectionExtent.max)} |
| 220 | + {@const half = Math.floor(SELECTION_ENDPOINT_SIZE / 2)} |
| 221 | + {@const overlap = Math.ceil(SELECTION_ENDPOINT_SIZE / 2)} |
| 222 | + <div class="selection-overlay-container" style:width={isVertical ? `${RULER_THICKNESS + overlap}px` : "100%"} style:height={isVertical ? "100%" : `${RULER_THICKNESS + overlap}px`}> |
| 223 | + <div |
| 224 | + class="selection-line" |
| 225 | + style:left={isVertical ? `${RULER_THICKNESS}px` : `${minPos}px`} |
| 226 | + style:top={isVertical ? `${minPos}px` : `${RULER_THICKNESS}px`} |
| 227 | + style:width={isVertical ? "1px" : `${maxPos - minPos}px`} |
| 228 | + style:height={isVertical ? `${maxPos - minPos}px` : "1px"} |
| 229 | + ></div> |
| 230 | + {#each [minPos, maxPos] as pos} |
| 231 | + <div |
| 232 | + class="selection-endpoint" |
| 233 | + style:left={isVertical ? `${RULER_THICKNESS - half}px` : `${pos - half}px`} |
| 234 | + style:top={isVertical ? `${pos - half}px` : `${RULER_THICKNESS - half}px`} |
| 235 | + style:width={`${SELECTION_ENDPOINT_SIZE}px`} |
| 236 | + style:height={`${SELECTION_ENDPOINT_SIZE}px`} |
| 237 | + ></div> |
| 238 | + {/each} |
| 239 | + </div> |
| 240 | + {/if} |
203 | 241 | </div> |
204 | 242 |
|
205 | 243 | <style lang="scss"> |
206 | 244 | .ruler-input { |
207 | 245 | flex: 1 1 100%; |
208 | | - background: var(--color-2-mildblack); |
209 | | - overflow: hidden; |
210 | 246 | position: relative; |
211 | 247 | box-sizing: border-box; |
212 | 248 |
|
213 | | - &.horizontal { |
214 | | - height: 16px; |
215 | | - border-bottom: 1px solid var(--color-5-dullgray); |
216 | | - } |
| 249 | + .ruler-area { |
| 250 | + background: var(--color-2-mildblack); |
| 251 | + width: 100%; |
| 252 | + height: 100%; |
| 253 | + position: relative; |
| 254 | + overflow: hidden; |
217 | 255 |
|
218 | | - &.vertical { |
219 | | - width: 16px; |
220 | | - border-right: 1px solid var(--color-5-dullgray); |
| 256 | + &.horizontal { |
| 257 | + height: 16px; |
| 258 | + border-bottom: 1px solid var(--color-5-dullgray); |
| 259 | + } |
221 | 260 |
|
222 | | - svg text { |
223 | | - text-anchor: end; |
| 261 | + &.vertical { |
| 262 | + width: 16px; |
| 263 | + border-right: 1px solid var(--color-5-dullgray); |
| 264 | +
|
| 265 | + svg text { |
| 266 | + text-anchor: end; |
| 267 | + } |
224 | 268 | } |
225 | | - } |
226 | 269 |
|
227 | | - svg { |
228 | | - position: absolute; |
| 270 | + svg { |
| 271 | + position: absolute; |
229 | 272 |
|
230 | | - path { |
231 | | - stroke-width: 1px; |
232 | | - stroke: var(--color-5-dullgray); |
| 273 | + path { |
| 274 | + stroke-width: 1px; |
| 275 | + stroke: var(--color-5-dullgray); |
233 | 276 |
|
234 | | - &.cursor-indicator { |
235 | | - stroke: var(--color-8-uppergray); |
| 277 | + &.cursor-indicator { |
| 278 | + stroke: var(--color-8-uppergray); |
| 279 | + } |
236 | 280 | } |
237 | | - } |
238 | 281 |
|
239 | | - text { |
240 | | - font-size: 12px; |
241 | | - fill: var(--color-8-uppergray); |
| 282 | + text { |
| 283 | + font-size: 12px; |
| 284 | + fill: var(--color-8-uppergray); |
| 285 | + } |
242 | 286 | } |
243 | 287 | } |
| 288 | +
|
| 289 | + .selection-overlay-container { |
| 290 | + overflow: hidden; |
| 291 | + position: absolute; |
| 292 | + z-index: 1; |
| 293 | + top: 0; |
| 294 | + left: 0; |
| 295 | + } |
| 296 | +
|
| 297 | + .selection-line { |
| 298 | + position: absolute; |
| 299 | + background: var(--color-8-uppergray); |
| 300 | + } |
| 301 | +
|
| 302 | + .selection-endpoint { |
| 303 | + position: absolute; |
| 304 | + background: var(--color-2-mildblack); |
| 305 | + border: 1px solid var(--color-overlay-blue); |
| 306 | + box-sizing: border-box; |
| 307 | + } |
244 | 308 | } |
245 | 309 | </style> |
0 commit comments