feat(measurements): larger canvas, always-visible handles, fewer clicks
- Canvas fills the viewport (h-[calc(100vh-12rem)] on desktop, -14rem on mobile), matching the datum-editor precedent. The side panel tracks the same height so both read as equal-height siblings. - Handles are now visible on every measurement, not just the selected one. Unselected: small (3px), low-alpha, faint white ring. Selected endpoints: 6.5px with a thick ring. Selected primary handles (ellipse center / angle vertex): 8px. The invisible grab radius is 14px so the tiny unselected dots are still easy to target. - Selected handles keep their palette color (previously they went white along with the lines, so a selected handle disappeared on light backgrounds). Matches DatumCanvas's look. - Hit-test priority is explicit: handles beat geometry, so a precision grab on an endpoint always wins over a line-body drag — including on an unselected measurement, which promotes to select-and-drag in a single gesture. - Removed the on-canvas delete button next to the selected label. The side-panel row × and Delete/Backspace still work. HitResult.kind drops its "delete" variant; the matching draw + dispatch blocks and the dedicated hit region are gone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e07ee9d204
commit
497e71d63c
@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue"
|
import { ref, computed, onMounted, onUnmounted, watch } from "vue"
|
||||||
|
import { useMediaQuery } from "@vueuse/core"
|
||||||
import { nanoid } from "nanoid"
|
import { nanoid } from "nanoid"
|
||||||
import type { Point } from "@/types"
|
import type { Point } from "@/types"
|
||||||
import { getDatumColor } from "@/lib/datums"
|
import { getDatumColor } from "@/lib/datums"
|
||||||
@ -9,6 +10,15 @@ const props = defineProps<{
|
|||||||
scalePxPerMm: number
|
scalePxPerMm: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const isMobile = useMediaQuery("(max-width: 767px)")
|
||||||
|
|
||||||
|
// Mirror the datum-editor precedent: leave more vertical room for the mobile
|
||||||
|
// toolbar/chrome than desktop. Keep the canvas and the side list the same
|
||||||
|
// height so they align on desktop.
|
||||||
|
const canvasHeightClass = computed(() =>
|
||||||
|
isMobile.value ? "h-[calc(100vh-14rem)]" : "h-[calc(100vh-12rem)]",
|
||||||
|
)
|
||||||
|
|
||||||
const containerRef = ref<HTMLDivElement | null>(null)
|
const containerRef = ref<HTMLDivElement | null>(null)
|
||||||
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
const overlayRef = ref<HTMLCanvasElement | null>(null)
|
const overlayRef = ref<HTMLCanvasElement | null>(null)
|
||||||
@ -311,8 +321,8 @@ function measurementTypeLabel(m: Measurement): string {
|
|||||||
return "Angle"
|
return "Angle"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anchor point in image space where we place the label/delete button. Chosen
|
// Anchor point in image space where we place the label. Chosen per type so
|
||||||
// per type so the label sits in a predictable, non-occluding position.
|
// the label sits in a predictable, non-occluding position.
|
||||||
function labelAnchor(m: Measurement): Point {
|
function labelAnchor(m: Measurement): Point {
|
||||||
if (m.type === "line") {
|
if (m.type === "line") {
|
||||||
return { x: (m.a.x + m.b.x) / 2, y: (m.a.y + m.b.y) / 2 }
|
return { x: (m.a.x + m.b.x) / 2, y: (m.a.y + m.b.y) / 2 }
|
||||||
@ -345,28 +355,13 @@ function labelRect(
|
|||||||
return { x, y, w, h, textX: anchor.x, textY: anchor.y - offsetY }
|
return { x, y, w, h, textX: anchor.x, textY: anchor.y - offsetY }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete button sits immediately to the right of the label. Same screen-space
|
|
||||||
// rect is used for hit testing and for drawing.
|
|
||||||
function deleteButtonRect(
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
m: Measurement,
|
|
||||||
): { x: number; y: number; size: number } {
|
|
||||||
const rect = labelRect(ctx, m)
|
|
||||||
const size = 18
|
|
||||||
return {
|
|
||||||
x: rect.x + rect.w + 4,
|
|
||||||
y: rect.y + (rect.h - size) / 2,
|
|
||||||
size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawMeasurement(
|
function drawMeasurement(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
m: Measurement,
|
m: Measurement,
|
||||||
isSelected: boolean,
|
isSelected: boolean,
|
||||||
) {
|
) {
|
||||||
const baseColor = getDatumColor(m.colorIndex)
|
const baseColor = getDatumColor(m.colorIndex)
|
||||||
const color = isSelected ? "#ffffff" : baseColor
|
const strokeColor = isSelected ? "#ffffff" : baseColor
|
||||||
const lineAlpha = isSelected ? 1.0 : 0.8
|
const lineAlpha = isSelected ? 1.0 : 0.8
|
||||||
const lineWidth = isSelected ? 3 : 2
|
const lineWidth = isSelected ? 3 : 2
|
||||||
|
|
||||||
@ -374,11 +369,11 @@ function drawMeasurement(
|
|||||||
ctx.globalAlpha = lineAlpha
|
ctx.globalAlpha = lineAlpha
|
||||||
|
|
||||||
if (m.type === "line") {
|
if (m.type === "line") {
|
||||||
drawLineGeometry(ctx, m, color, lineWidth, isSelected)
|
drawLineGeometry(ctx, m, strokeColor, baseColor, lineWidth, isSelected)
|
||||||
} else if (m.type === "ellipse") {
|
} else if (m.type === "ellipse") {
|
||||||
drawEllipseGeometry(ctx, m, color, lineWidth, isSelected)
|
drawEllipseGeometry(ctx, m, strokeColor, baseColor, lineWidth, isSelected)
|
||||||
} else {
|
} else {
|
||||||
drawAngleGeometry(ctx, m, color, lineWidth, isSelected)
|
drawAngleGeometry(ctx, m, strokeColor, baseColor, lineWidth, isSelected)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.globalAlpha = 1.0
|
ctx.globalAlpha = 1.0
|
||||||
@ -389,7 +384,8 @@ function drawMeasurement(
|
|||||||
function drawLineGeometry(
|
function drawLineGeometry(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
m: LineMeasurement,
|
m: LineMeasurement,
|
||||||
color: string,
|
strokeColor: string,
|
||||||
|
handleColor: string,
|
||||||
lineWidth: number,
|
lineWidth: number,
|
||||||
isSelected: boolean,
|
isSelected: boolean,
|
||||||
) {
|
) {
|
||||||
@ -398,19 +394,20 @@ function drawLineGeometry(
|
|||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(sa.x, sa.y)
|
ctx.moveTo(sa.x, sa.y)
|
||||||
ctx.lineTo(sb.x, sb.y)
|
ctx.lineTo(sb.x, sb.y)
|
||||||
ctx.strokeStyle = color
|
ctx.strokeStyle = strokeColor
|
||||||
ctx.lineWidth = lineWidth
|
ctx.lineWidth = lineWidth
|
||||||
ctx.setLineDash(isSelected ? [] : [6, 3])
|
ctx.setLineDash(isSelected ? [] : [6, 3])
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
ctx.setLineDash([])
|
ctx.setLineDash([])
|
||||||
drawHandle(ctx, sa, color, isSelected)
|
drawHandle(ctx, sa, handleColor, isSelected)
|
||||||
drawHandle(ctx, sb, color, isSelected)
|
drawHandle(ctx, sb, handleColor, isSelected)
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawEllipseGeometry(
|
function drawEllipseGeometry(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
m: EllipseMeasurement,
|
m: EllipseMeasurement,
|
||||||
color: string,
|
strokeColor: string,
|
||||||
|
handleColor: string,
|
||||||
lineWidth: number,
|
lineWidth: number,
|
||||||
isSelected: boolean,
|
isSelected: boolean,
|
||||||
) {
|
) {
|
||||||
@ -435,13 +432,12 @@ function drawEllipseGeometry(
|
|||||||
if (i === 0) ctx.moveTo(x, y)
|
if (i === 0) ctx.moveTo(x, y)
|
||||||
else ctx.lineTo(x, y)
|
else ctx.lineTo(x, y)
|
||||||
}
|
}
|
||||||
ctx.strokeStyle = color
|
ctx.strokeStyle = strokeColor
|
||||||
ctx.lineWidth = lineWidth
|
ctx.lineWidth = lineWidth
|
||||||
ctx.setLineDash(isSelected ? [] : [6, 3])
|
ctx.setLineDash(isSelected ? [] : [6, 3])
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
ctx.setLineDash([])
|
ctx.setLineDash([])
|
||||||
|
|
||||||
// Axis guides
|
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.globalAlpha *= 0.5
|
ctx.globalAlpha *= 0.5
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
@ -449,20 +445,21 @@ function drawEllipseGeometry(
|
|||||||
ctx.lineTo(a.x, a.y)
|
ctx.lineTo(a.x, a.y)
|
||||||
ctx.moveTo(c.x, c.y)
|
ctx.moveTo(c.x, c.y)
|
||||||
ctx.lineTo(b.x, b.y)
|
ctx.lineTo(b.x, b.y)
|
||||||
ctx.strokeStyle = color
|
ctx.strokeStyle = strokeColor
|
||||||
ctx.lineWidth = 1
|
ctx.lineWidth = 1
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
|
|
||||||
drawHandle(ctx, c, color, isSelected, true)
|
drawHandle(ctx, c, handleColor, isSelected, true)
|
||||||
drawHandle(ctx, a, color, isSelected)
|
drawHandle(ctx, a, handleColor, isSelected)
|
||||||
drawHandle(ctx, b, color, isSelected)
|
drawHandle(ctx, b, handleColor, isSelected)
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawAngleGeometry(
|
function drawAngleGeometry(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
m: AngleMeasurement,
|
m: AngleMeasurement,
|
||||||
color: string,
|
strokeColor: string,
|
||||||
|
handleColor: string,
|
||||||
lineWidth: number,
|
lineWidth: number,
|
||||||
isSelected: boolean,
|
isSelected: boolean,
|
||||||
) {
|
) {
|
||||||
@ -474,13 +471,12 @@ function drawAngleGeometry(
|
|||||||
ctx.moveTo(a.x, a.y)
|
ctx.moveTo(a.x, a.y)
|
||||||
ctx.lineTo(v.x, v.y)
|
ctx.lineTo(v.x, v.y)
|
||||||
ctx.lineTo(b.x, b.y)
|
ctx.lineTo(b.x, b.y)
|
||||||
ctx.strokeStyle = color
|
ctx.strokeStyle = strokeColor
|
||||||
ctx.lineWidth = lineWidth
|
ctx.lineWidth = lineWidth
|
||||||
ctx.setLineDash(isSelected ? [] : [6, 3])
|
ctx.setLineDash(isSelected ? [] : [6, 3])
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
ctx.setLineDash([])
|
ctx.setLineDash([])
|
||||||
|
|
||||||
// Arc sweep between the two arms to make the angle visually obvious.
|
|
||||||
const lenA = Math.hypot(a.x - v.x, a.y - v.y)
|
const lenA = Math.hypot(a.x - v.x, a.y - v.y)
|
||||||
const lenB = Math.hypot(b.x - v.x, b.y - v.y)
|
const lenB = Math.hypot(b.x - v.x, b.y - v.y)
|
||||||
const arcR = Math.max(16, Math.min(lenA, lenB) * 0.3)
|
const arcR = Math.max(16, Math.min(lenA, lenB) * 0.3)
|
||||||
@ -496,17 +492,26 @@ function drawAngleGeometry(
|
|||||||
ctx.globalAlpha *= 0.6
|
ctx.globalAlpha *= 0.6
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(v.x, v.y, arcR, thetaA, thetaA + delta, delta < 0)
|
ctx.arc(v.x, v.y, arcR, thetaA, thetaA + delta, delta < 0)
|
||||||
ctx.strokeStyle = color
|
ctx.strokeStyle = strokeColor
|
||||||
ctx.lineWidth = 1.5
|
ctx.lineWidth = 1.5
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
drawHandle(ctx, v, color, isSelected, true)
|
drawHandle(ctx, v, handleColor, isSelected, true)
|
||||||
drawHandle(ctx, a, color, isSelected)
|
drawHandle(ctx, a, handleColor, isSelected)
|
||||||
drawHandle(ctx, b, color, isSelected)
|
drawHandle(ctx, b, handleColor, isSelected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle rendering follows the datum-editor precedent: a filled color center
|
||||||
|
// ringed in white. Size and alpha depend on the measurement's selection
|
||||||
|
// state so the user always sees where to grab, but unselected handles stay
|
||||||
|
// visually quiet.
|
||||||
|
// unselected: 3 px radius @ 0.5 alpha
|
||||||
|
// selected primary (center/vertex): 8 px radius, full alpha, thicker ring
|
||||||
|
// selected secondary: 6.5 px radius, full alpha
|
||||||
|
// The invisible hit region (HANDLE_HIT_PX) is wider than any of these so
|
||||||
|
// grabbing is forgiving even on tiny unselected dots.
|
||||||
function drawHandle(
|
function drawHandle(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
s: Point,
|
s: Point,
|
||||||
@ -514,14 +519,27 @@ function drawHandle(
|
|||||||
isSelected: boolean,
|
isSelected: boolean,
|
||||||
primary = false,
|
primary = false,
|
||||||
) {
|
) {
|
||||||
const r = primary ? 6 : 5
|
ctx.save()
|
||||||
|
if (isSelected) {
|
||||||
|
const r = primary ? 8 : 6.5
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.arc(s.x, s.y, r, 0, Math.PI * 2)
|
ctx.arc(s.x, s.y, r, 0, Math.PI * 2)
|
||||||
ctx.fillStyle = color
|
ctx.fillStyle = color
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
ctx.strokeStyle = isSelected ? "#0b0b0b" : "#ffffff"
|
ctx.strokeStyle = "#ffffff"
|
||||||
ctx.lineWidth = 1.5
|
ctx.lineWidth = 2
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
} else {
|
||||||
|
ctx.globalAlpha = 0.5
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(s.x, s.y, 3, 0, Math.PI * 2)
|
||||||
|
ctx.fillStyle = color
|
||||||
|
ctx.fill()
|
||||||
|
ctx.strokeStyle = "#ffffff"
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawLabel(
|
function drawLabel(
|
||||||
@ -550,27 +568,6 @@ function drawLabel(
|
|||||||
ctx.textAlign = "start"
|
ctx.textAlign = "start"
|
||||||
ctx.textBaseline = "alphabetic"
|
ctx.textBaseline = "alphabetic"
|
||||||
ctx.restore()
|
ctx.restore()
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
const btn = deleteButtonRect(ctx, m)
|
|
||||||
ctx.save()
|
|
||||||
ctx.fillStyle = "#ef4444"
|
|
||||||
roundRect(ctx, btn.x, btn.y, btn.size, btn.size, 4)
|
|
||||||
ctx.fill()
|
|
||||||
ctx.strokeStyle = "#ffffff"
|
|
||||||
ctx.lineWidth = 1
|
|
||||||
ctx.stroke()
|
|
||||||
ctx.strokeStyle = "#ffffff"
|
|
||||||
ctx.lineWidth = 1.75
|
|
||||||
ctx.beginPath()
|
|
||||||
const pad = 5
|
|
||||||
ctx.moveTo(btn.x + pad, btn.y + pad)
|
|
||||||
ctx.lineTo(btn.x + btn.size - pad, btn.y + btn.size - pad)
|
|
||||||
ctx.moveTo(btn.x + btn.size - pad, btn.y + pad)
|
|
||||||
ctx.lineTo(btn.x + pad, btn.y + btn.size - pad)
|
|
||||||
ctx.stroke()
|
|
||||||
ctx.restore()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function roundRect(
|
function roundRect(
|
||||||
@ -681,7 +678,9 @@ function getCanvasXY(e: MouseEvent | Touch): { x: number; y: number } {
|
|||||||
|
|
||||||
// Hit-testing helpers. All thresholds are in screen pixels so they feel
|
// Hit-testing helpers. All thresholds are in screen pixels so they feel
|
||||||
// consistent to the user regardless of zoom level.
|
// consistent to the user regardless of zoom level.
|
||||||
const HANDLE_HIT_PX = 10
|
// Generous invisible hotspot around each handle so precision grabs feel
|
||||||
|
// forgiving on small unselected dots. Larger than any rendered handle.
|
||||||
|
const HANDLE_HIT_PX = 14
|
||||||
const LINE_HIT_PX = 6
|
const LINE_HIT_PX = 6
|
||||||
const ELLIPSE_HIT_PX = 7
|
const ELLIPSE_HIT_PX = 7
|
||||||
|
|
||||||
@ -733,8 +732,7 @@ interface HitResult {
|
|||||||
// "handle" means the user grabbed a specific control point.
|
// "handle" means the user grabbed a specific control point.
|
||||||
// "geometry" means they grabbed the line/curve/arms — whole-measurement drag.
|
// "geometry" means they grabbed the line/curve/arms — whole-measurement drag.
|
||||||
// "label" means they clicked the label — selection only (drag moves whole).
|
// "label" means they clicked the label — selection only (drag moves whole).
|
||||||
// "delete" means they clicked the delete X button.
|
kind: "handle" | "geometry" | "label"
|
||||||
kind: "handle" | "geometry" | "label" | "delete"
|
|
||||||
handleKey: string | null
|
handleKey: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -763,8 +761,8 @@ function hitTest(cursorScreen: Point): HitResult | null {
|
|||||||
const ctx = overlayRef.value?.getContext("2d")
|
const ctx = overlayRef.value?.getContext("2d")
|
||||||
if (!ctx) return null
|
if (!ctx) return null
|
||||||
|
|
||||||
// Check the selected measurement first — its label/delete button is
|
// Check the selected measurement first — its label is visually on top,
|
||||||
// visually on top, so its hit region should win ties.
|
// so its hit region should win ties.
|
||||||
const ordered: Measurement[] = []
|
const ordered: Measurement[] = []
|
||||||
const sel = measurements.value.find((m) => m.id === selectedId.value)
|
const sel = measurements.value.find((m) => m.id === selectedId.value)
|
||||||
if (sel) ordered.push(sel)
|
if (sel) ordered.push(sel)
|
||||||
@ -772,21 +770,9 @@ function hitTest(cursorScreen: Point): HitResult | null {
|
|||||||
if (m.id !== selectedId.value) ordered.push(m)
|
if (m.id !== selectedId.value) ordered.push(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 1: delete button on the selected measurement.
|
// Priority 1: handles (selected first, so you can always grab the active
|
||||||
if (sel) {
|
// measurement's handle even if it overlaps another). Handles beat geometry
|
||||||
const btn = deleteButtonRect(ctx, sel)
|
// so precision grabs on endpoints always win over a line-body grab.
|
||||||
if (
|
|
||||||
cursorScreen.x >= btn.x &&
|
|
||||||
cursorScreen.x <= btn.x + btn.size &&
|
|
||||||
cursorScreen.y >= btn.y &&
|
|
||||||
cursorScreen.y <= btn.y + btn.size
|
|
||||||
) {
|
|
||||||
return { measurementId: sel.id, kind: "delete", handleKey: null }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 2: handles (selected first, so you can always grab the active
|
|
||||||
// measurement's handle even if it overlaps another).
|
|
||||||
for (const m of ordered) {
|
for (const m of ordered) {
|
||||||
for (const h of getHandlePositions(m)) {
|
for (const h of getHandlePositions(m)) {
|
||||||
const s = imgToScreen(h.pt)
|
const s = imgToScreen(h.pt)
|
||||||
@ -796,7 +782,7 @@ function hitTest(cursorScreen: Point): HitResult | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 3: labels.
|
// Priority 2: labels.
|
||||||
for (const m of ordered) {
|
for (const m of ordered) {
|
||||||
const rect = labelRect(ctx, m)
|
const rect = labelRect(ctx, m)
|
||||||
if (
|
if (
|
||||||
@ -809,7 +795,7 @@ function hitTest(cursorScreen: Point): HitResult | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 4: geometry bodies.
|
// Priority 3: geometry bodies.
|
||||||
for (const m of ordered) {
|
for (const m of ordered) {
|
||||||
if (m.type === "line") {
|
if (m.type === "line") {
|
||||||
const sa = imgToScreen(m.a)
|
const sa = imgToScreen(m.a)
|
||||||
@ -1040,11 +1026,6 @@ function pointerDown(screenX: number, screenY: number): "measurement" | "pan" {
|
|||||||
return "pan"
|
return "pan"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hit.kind === "delete") {
|
|
||||||
deleteMeasurement(hit.measurementId)
|
|
||||||
return "measurement"
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedId.value = hit.measurementId
|
selectedId.value = hit.measurementId
|
||||||
const target = measurements.value.find((m) => m.id === hit.measurementId)
|
const target = measurements.value.find((m) => m.id === hit.measurementId)
|
||||||
if (!target) {
|
if (!target) {
|
||||||
@ -1547,12 +1528,17 @@ watch(() => props.scalePxPerMm, () => { drawOverlay() })
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Canvas + side list -->
|
<!-- Canvas + side list. The parent ResultViewer clamps width to
|
||||||
|
max-w-4xl; widening the canvas beyond that requires a parent
|
||||||
|
change (see ResultViewer.vue root container). -->
|
||||||
<div class="grid gap-3 md:grid-cols-[1fr_220px]">
|
<div class="grid gap-3 md:grid-cols-[1fr_220px]">
|
||||||
<div
|
<div
|
||||||
ref="containerRef"
|
ref="containerRef"
|
||||||
class="relative h-[500px] overflow-hidden rounded-lg border border-border bg-muted"
|
class="relative overflow-hidden rounded-lg border border-border bg-muted"
|
||||||
:class="activeTool !== 'none' ? 'cursor-crosshair' : 'cursor-grab'"
|
:class="[
|
||||||
|
canvasHeightClass,
|
||||||
|
activeTool !== 'none' ? 'cursor-crosshair' : 'cursor-grab',
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
ref="canvasRef"
|
ref="canvasRef"
|
||||||
@ -1576,7 +1562,8 @@ watch(() => props.scalePxPerMm, () => { drawOverlay() })
|
|||||||
|
|
||||||
<!-- Measurement list -->
|
<!-- Measurement list -->
|
||||||
<div
|
<div
|
||||||
class="flex max-h-[500px] flex-col gap-1 overflow-y-auto rounded-lg border border-border bg-muted/30 p-2"
|
class="flex flex-col gap-1 overflow-y-auto rounded-lg border border-border bg-muted/30 p-2"
|
||||||
|
:class="canvasHeightClass"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="measurementSummaries.length === 0"
|
v-if="measurementSummaries.length === 0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user