@pie-players/pie-tool-graph 0.3.42 → 0.3.44
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/dist/defineProperty-CyepwRr5.js +33 -0
- package/dist/dist-DIBQdekL.js +466 -0
- package/dist/dist-rF11IR1B.js +213 -0
- package/dist/index.d.ts +0 -1
- package/dist/tool-graph.js +3474 -1711
- package/package.json +6 -8
- package/dist/index.d.ts.map +0 -1
- package/tool-graph.svelte +0 -726
package/tool-graph.svelte
DELETED
|
@@ -1,726 +0,0 @@
|
|
|
1
|
-
<svelte:options
|
|
2
|
-
customElement={{
|
|
3
|
-
tag: 'pie-tool-graph',
|
|
4
|
-
shadow: 'open',
|
|
5
|
-
props: {
|
|
6
|
-
visible: { type: 'Boolean', attribute: 'visible' },
|
|
7
|
-
toolId: { type: 'String', attribute: 'tool-id' }
|
|
8
|
-
}
|
|
9
|
-
}}
|
|
10
|
-
/>
|
|
11
|
-
|
|
12
|
-
<script lang="ts">
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// Props
|
|
16
|
-
let {
|
|
17
|
-
visible = false,
|
|
18
|
-
toolId = 'graph'
|
|
19
|
-
}: {
|
|
20
|
-
visible?: boolean;
|
|
21
|
-
toolId?: string;
|
|
22
|
-
} = $props();
|
|
23
|
-
|
|
24
|
-
// Tool types (matching production implementation)
|
|
25
|
-
type Tool = 'selector' | 'point' | 'line' | 'delete';
|
|
26
|
-
|
|
27
|
-
// Data structures (matching production implementation)
|
|
28
|
-
interface Point {
|
|
29
|
-
id: number;
|
|
30
|
-
x: number; // Coordinate in the dynamic viewBox space
|
|
31
|
-
y: number; // Coordinate in the fixed 0-100 viewBox height
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface Line {
|
|
35
|
-
id: number;
|
|
36
|
-
p1Id: number;
|
|
37
|
-
p2Id: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface Coordinates {
|
|
41
|
-
x: number;
|
|
42
|
-
y: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// State
|
|
46
|
-
let canvasWrapperEl = $state<HTMLDivElement | undefined>();
|
|
47
|
-
let svgCanvasEl = $state<SVGSVGElement | undefined>();
|
|
48
|
-
|
|
49
|
-
// Tool state
|
|
50
|
-
let currentTool = $state<Tool>('selector');
|
|
51
|
-
let points = $state<Point[]>([]);
|
|
52
|
-
let lines = $state<Line[]>([]);
|
|
53
|
-
let nextId = $state(1);
|
|
54
|
-
let gridOpacity = $state(1);
|
|
55
|
-
let tempLineStartPointId = $state<number | null>(null);
|
|
56
|
-
let draggingPointId = $state<number | null>(null);
|
|
57
|
-
let currentPointerPos = $state<Coordinates | null>(null);
|
|
58
|
-
|
|
59
|
-
// Grid configuration (matching production implementation)
|
|
60
|
-
const MAJOR_VERTICAL_DIVISIONS = 5; // Fixed number of rows
|
|
61
|
-
const SUBGRID_DIVISIONS = 5; // 5x5 minor grid
|
|
62
|
-
const DESIRED_MAJOR_CELL_SIZE_SVG = 100 / MAJOR_VERTICAL_DIVISIONS; // 100 / 5 = 20 units
|
|
63
|
-
const DESIRED_MINOR_CELL_SIZE_SVG = DESIRED_MAJOR_CELL_SIZE_SVG / SUBGRID_DIVISIONS; // 20 / 5 = 4 units
|
|
64
|
-
|
|
65
|
-
// Container pixel dimensions (from ResizeObserver)
|
|
66
|
-
let containerPixelWidth = $state(0);
|
|
67
|
-
let containerPixelHeight = $state(0);
|
|
68
|
-
|
|
69
|
-
// Dynamic viewBox width (matching production implementation)
|
|
70
|
-
let viewBoxWidth = $derived.by(() => {
|
|
71
|
-
if (containerPixelHeight <= 0 || containerPixelWidth <= 0) {
|
|
72
|
-
return 100; // Default width until dimensions are known
|
|
73
|
-
}
|
|
74
|
-
// Calculate the vertical scaling factor: pixels per SVG unit
|
|
75
|
-
const scaleY = containerPixelHeight / 100; // (viewBox height is 100)
|
|
76
|
-
// Convert container pixel width back into SVG units using this scale
|
|
77
|
-
const requiredWidthSVG = containerPixelWidth / scaleY;
|
|
78
|
-
// Ensure a minimum width
|
|
79
|
-
return Math.max(100, requiredWidthSVG);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Tool definitions (matching production implementation)
|
|
83
|
-
const tools: Array<{ name: Tool; icon: string; label: string; title: string }> = [
|
|
84
|
-
{
|
|
85
|
-
name: 'selector',
|
|
86
|
-
icon: 'selector',
|
|
87
|
-
label: 'Selector',
|
|
88
|
-
title: 'Selector: Click and drag points to move them or associated lines.'
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: 'point',
|
|
92
|
-
icon: 'point',
|
|
93
|
-
label: 'Point',
|
|
94
|
-
title: 'Point: Click on the grid to add points.'
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
name: 'line',
|
|
98
|
-
icon: 'line',
|
|
99
|
-
label: 'Line',
|
|
100
|
-
title: 'Line: Click a starting point, then an ending point to draw a line.'
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: 'delete',
|
|
104
|
-
icon: 'delete',
|
|
105
|
-
label: 'Delete',
|
|
106
|
-
title: 'Delete: Click on a point to delete it and any connected lines.'
|
|
107
|
-
}
|
|
108
|
-
];
|
|
109
|
-
|
|
110
|
-
// Helper functions
|
|
111
|
-
function getUniqueId(): number {
|
|
112
|
-
return nextId++;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function getPointById(id: number | null): Point | undefined {
|
|
116
|
-
if (id === null) return undefined;
|
|
117
|
-
return points.find((p) => p.id === id);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function getSVGCoordinates(event: MouseEvent | PointerEvent): Coordinates | null {
|
|
121
|
-
if (!svgCanvasEl) return null;
|
|
122
|
-
const pt = svgCanvasEl.createSVGPoint();
|
|
123
|
-
pt.x = event.clientX;
|
|
124
|
-
pt.y = event.clientY;
|
|
125
|
-
const svgScreenCTM = svgCanvasEl.getScreenCTM();
|
|
126
|
-
if (!svgScreenCTM) return null;
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const transformedPt = pt.matrixTransform(svgScreenCTM.inverse());
|
|
130
|
-
|
|
131
|
-
// Clamp Y to 0-100 (fixed viewBox height)
|
|
132
|
-
transformedPt.y = Math.max(0, Math.min(100, transformedPt.y));
|
|
133
|
-
// Clamp X to 0 to current viewBoxWidth
|
|
134
|
-
transformedPt.x = Math.max(0, Math.min(viewBoxWidth, transformedPt.x));
|
|
135
|
-
|
|
136
|
-
return { x: transformedPt.x, y: transformedPt.y };
|
|
137
|
-
} catch (e) {
|
|
138
|
-
console.error('Error transforming point:', e);
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function findNearestPoint(coords: Coordinates, threshold: number = 5): Point | null {
|
|
144
|
-
const thresholdSq = threshold * threshold;
|
|
145
|
-
let nearest: Point | null = null;
|
|
146
|
-
let minDistSq = Infinity;
|
|
147
|
-
|
|
148
|
-
for (const point of points) {
|
|
149
|
-
const dx = point.x - coords.x;
|
|
150
|
-
const dy = point.y - coords.y;
|
|
151
|
-
const distSq = dx * dx + dy * dy;
|
|
152
|
-
|
|
153
|
-
if (distSq < minDistSq && distSq < thresholdSq) {
|
|
154
|
-
minDistSq = distSq;
|
|
155
|
-
nearest = point;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return nearest;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function isPointHighlighted(pointId: number): boolean {
|
|
162
|
-
return pointId === tempLineStartPointId || pointId === draggingPointId;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function activatePoint(pointId: number) {
|
|
166
|
-
if (currentTool === 'delete') {
|
|
167
|
-
points = points.filter((p) => p.id !== pointId);
|
|
168
|
-
lines = lines.filter((l) => l.p1Id !== pointId && l.p2Id !== pointId);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (currentTool === 'line') {
|
|
173
|
-
if (tempLineStartPointId === null) {
|
|
174
|
-
tempLineStartPointId = pointId;
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (tempLineStartPointId !== pointId) {
|
|
179
|
-
const exists = lines.some(
|
|
180
|
-
(l) =>
|
|
181
|
-
(l.p1Id === tempLineStartPointId && l.p2Id === pointId) ||
|
|
182
|
-
(l.p1Id === pointId && l.p2Id === tempLineStartPointId)
|
|
183
|
-
);
|
|
184
|
-
if (!exists) {
|
|
185
|
-
lines = [...lines, { id: getUniqueId(), p1Id: tempLineStartPointId, p2Id: pointId }];
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
tempLineStartPointId = null;
|
|
189
|
-
currentPointerPos = null;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Computed grid lines (matching production implementation)
|
|
194
|
-
let gridLines = $derived.by(() => {
|
|
195
|
-
const lines = {
|
|
196
|
-
majorVertical: [] as number[],
|
|
197
|
-
majorHorizontal: [] as number[],
|
|
198
|
-
minorVertical: [] as number[],
|
|
199
|
-
minorHorizontal: [] as number[]
|
|
200
|
-
};
|
|
201
|
-
const currentViewBoxWidth = viewBoxWidth;
|
|
202
|
-
|
|
203
|
-
// Horizontal Lines (Fixed Y, extend across current viewBox width)
|
|
204
|
-
for (let i = 0; i <= MAJOR_VERTICAL_DIVISIONS; i++) {
|
|
205
|
-
const yPos = i * DESIRED_MAJOR_CELL_SIZE_SVG; // 0, 20, 40, 60, 80, 100
|
|
206
|
-
lines.majorHorizontal.push(yPos);
|
|
207
|
-
if (i < MAJOR_VERTICAL_DIVISIONS) {
|
|
208
|
-
for (let j = 1; j < SUBGRID_DIVISIONS; j++) {
|
|
209
|
-
lines.minorHorizontal.push(yPos + j * DESIRED_MINOR_CELL_SIZE_SVG);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Vertical Lines (Fixed X spacing, up to current viewBox width)
|
|
215
|
-
let currentX = 0;
|
|
216
|
-
while (currentX <= currentViewBoxWidth) {
|
|
217
|
-
lines.majorVertical.push(currentX);
|
|
218
|
-
if (currentX < currentViewBoxWidth) {
|
|
219
|
-
for (let j = 1; j < SUBGRID_DIVISIONS; j++) {
|
|
220
|
-
const minorX = currentX + j * DESIRED_MINOR_CELL_SIZE_SVG;
|
|
221
|
-
if (minorX <= currentViewBoxWidth) {
|
|
222
|
-
lines.minorVertical.push(minorX);
|
|
223
|
-
} else {
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
if (DESIRED_MAJOR_CELL_SIZE_SVG <= 0) break;
|
|
229
|
-
currentX += DESIRED_MAJOR_CELL_SIZE_SVG;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return lines;
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Event handlers
|
|
236
|
-
function setTool(tool: Tool) {
|
|
237
|
-
currentTool = tool;
|
|
238
|
-
tempLineStartPointId = null;
|
|
239
|
-
draggingPointId = null;
|
|
240
|
-
currentPointerPos = null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function handleCanvasClick(event: MouseEvent) {
|
|
244
|
-
const coords = getSVGCoordinates(event);
|
|
245
|
-
if (!coords) return;
|
|
246
|
-
|
|
247
|
-
const nearestPoint = findNearestPoint(coords, DESIRED_MINOR_CELL_SIZE_SVG);
|
|
248
|
-
|
|
249
|
-
switch (currentTool) {
|
|
250
|
-
case 'point':
|
|
251
|
-
// Add point exactly where clicked in the current viewBox space
|
|
252
|
-
points = [...points, { id: getUniqueId(), x: coords.x, y: coords.y }];
|
|
253
|
-
break;
|
|
254
|
-
|
|
255
|
-
case 'line':
|
|
256
|
-
// If near an existing point, use it. Otherwise, create a new one.
|
|
257
|
-
const targetPoint = nearestPoint ?? { id: getUniqueId(), x: coords.x, y: coords.y };
|
|
258
|
-
if (!nearestPoint) {
|
|
259
|
-
points = [...points, targetPoint]; // Add if it's a new location
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (tempLineStartPointId === null) {
|
|
263
|
-
tempLineStartPointId = targetPoint.id;
|
|
264
|
-
} else {
|
|
265
|
-
if (tempLineStartPointId !== targetPoint.id) {
|
|
266
|
-
const exists = lines.some(
|
|
267
|
-
(l) =>
|
|
268
|
-
(l.p1Id === tempLineStartPointId && l.p2Id === targetPoint.id) ||
|
|
269
|
-
(l.p1Id === targetPoint.id && l.p2Id === tempLineStartPointId)
|
|
270
|
-
);
|
|
271
|
-
if (!exists) {
|
|
272
|
-
lines = [
|
|
273
|
-
...lines,
|
|
274
|
-
{
|
|
275
|
-
id: getUniqueId(),
|
|
276
|
-
p1Id: tempLineStartPointId,
|
|
277
|
-
p2Id: targetPoint.id
|
|
278
|
-
}
|
|
279
|
-
];
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
tempLineStartPointId = null;
|
|
283
|
-
currentPointerPos = null;
|
|
284
|
-
}
|
|
285
|
-
break;
|
|
286
|
-
|
|
287
|
-
case 'delete':
|
|
288
|
-
// Use a smaller threshold for precise deletion
|
|
289
|
-
const pointToDelete = findNearestPoint(coords, 2);
|
|
290
|
-
if (pointToDelete) {
|
|
291
|
-
// Remove the point
|
|
292
|
-
points = points.filter((p) => p.id !== pointToDelete.id);
|
|
293
|
-
// Remove lines connected to this point
|
|
294
|
-
lines = lines.filter(
|
|
295
|
-
(l) => l.p1Id !== pointToDelete.id && l.p2Id !== pointToDelete.id
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
break;
|
|
299
|
-
|
|
300
|
-
case 'selector':
|
|
301
|
-
tempLineStartPointId = null; // Cancel line drawing if clicking away
|
|
302
|
-
currentPointerPos = null;
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function handlePointPointerDown(pointId: number, event: PointerEvent) {
|
|
308
|
-
if (currentTool !== 'selector') return;
|
|
309
|
-
draggingPointId = pointId;
|
|
310
|
-
(event.target as Element).setPointerCapture(event.pointerId);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function handleCanvasMouseMove(event: PointerEvent) {
|
|
314
|
-
const coords = getSVGCoordinates(event);
|
|
315
|
-
if (!coords) return;
|
|
316
|
-
currentPointerPos = coords; // Update for temp line drawing
|
|
317
|
-
|
|
318
|
-
if (draggingPointId !== null && currentTool === 'selector') {
|
|
319
|
-
const point = getPointById(draggingPointId);
|
|
320
|
-
if (point) {
|
|
321
|
-
// Update stored coords (which are in the dynamic viewBox space)
|
|
322
|
-
point.x = coords.x;
|
|
323
|
-
point.y = coords.y; // Y is clamped 0-100 anyway
|
|
324
|
-
points = points; // Trigger reactivity
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function handlePointerUp(event: PointerEvent) {
|
|
330
|
-
if (draggingPointId !== null) {
|
|
331
|
-
if ((event.target as Element)?.hasPointerCapture(event.pointerId)) {
|
|
332
|
-
(event.target as Element).releasePointerCapture(event.pointerId);
|
|
333
|
-
}
|
|
334
|
-
draggingPointId = null;
|
|
335
|
-
}
|
|
336
|
-
// Don't reset currentPointerPos if still drawing a line
|
|
337
|
-
if (currentTool !== 'line' || tempLineStartPointId === null) {
|
|
338
|
-
currentPointerPos = null;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// ResizeObserver for dynamic viewBox width (matching production implementation)
|
|
343
|
-
let resizeObserver: ResizeObserver | null = null;
|
|
344
|
-
$effect(() => {
|
|
345
|
-
if (canvasWrapperEl) {
|
|
346
|
-
if (!resizeObserver) {
|
|
347
|
-
resizeObserver = new ResizeObserver((entries) => {
|
|
348
|
-
const entry = entries[0];
|
|
349
|
-
const { width: wrapperWidth, height: wrapperHeight } = entry.contentRect;
|
|
350
|
-
|
|
351
|
-
// Update pixel dimensions only if they actually changed
|
|
352
|
-
if (
|
|
353
|
-
Math.abs(containerPixelWidth - wrapperWidth) > 0.1 ||
|
|
354
|
-
Math.abs(containerPixelHeight - wrapperHeight) > 0.1
|
|
355
|
-
) {
|
|
356
|
-
containerPixelWidth = wrapperWidth;
|
|
357
|
-
containerPixelHeight = wrapperHeight;
|
|
358
|
-
}
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
resizeObserver.observe(canvasWrapperEl);
|
|
362
|
-
|
|
363
|
-
return () => {
|
|
364
|
-
if (resizeObserver) {
|
|
365
|
-
resizeObserver.disconnect();
|
|
366
|
-
}
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
</script>
|
|
372
|
-
|
|
373
|
-
{#if visible}
|
|
374
|
-
<div
|
|
375
|
-
class="pie-tool-graph"
|
|
376
|
-
role="dialog"
|
|
377
|
-
tabindex="-1"
|
|
378
|
-
aria-label="Graph Tool - Draw points and lines on a coordinate grid"
|
|
379
|
-
data-tool-id={toolId}
|
|
380
|
-
>
|
|
381
|
-
<!-- Toolbar (matching production implementation: lighter teal) -->
|
|
382
|
-
<div class="pie-tool-graph__toolbar">
|
|
383
|
-
<!-- Tool buttons -->
|
|
384
|
-
<div class="pie-tool-graph__tool-buttons">
|
|
385
|
-
{#each tools as tool (tool.name)}
|
|
386
|
-
<button
|
|
387
|
-
type="button"
|
|
388
|
-
class="pie-tool-graph__tool-button"
|
|
389
|
-
class:pie-tool-graph__tool-button--active={currentTool === tool.name}
|
|
390
|
-
onclick={() => setTool(tool.name)}
|
|
391
|
-
title={tool.title}
|
|
392
|
-
aria-label={tool.title}
|
|
393
|
-
aria-pressed={currentTool === tool.name}
|
|
394
|
-
>
|
|
395
|
-
<span class="pie-tool-graph__tool-icon" aria-hidden="true">
|
|
396
|
-
{#if tool.name === 'selector'}
|
|
397
|
-
<!-- Selector icon (swirling arrow) -->
|
|
398
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
399
|
-
<path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z" />
|
|
400
|
-
</svg>
|
|
401
|
-
{:else if tool.name === 'point'}
|
|
402
|
-
<!-- Point icon (pushpin) -->
|
|
403
|
-
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
404
|
-
<path
|
|
405
|
-
d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"
|
|
406
|
-
/>
|
|
407
|
-
</svg>
|
|
408
|
-
{:else if tool.name === 'line'}
|
|
409
|
-
<!-- Line icon (pencil) -->
|
|
410
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
411
|
-
<path
|
|
412
|
-
d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"
|
|
413
|
-
/>
|
|
414
|
-
</svg>
|
|
415
|
-
{:else if tool.name === 'delete'}
|
|
416
|
-
<!-- Delete icon (trash) -->
|
|
417
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
418
|
-
<path
|
|
419
|
-
d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
|
|
420
|
-
/>
|
|
421
|
-
</svg>
|
|
422
|
-
{/if}
|
|
423
|
-
</span>
|
|
424
|
-
<span class="pie-tool-graph__tool-label">{tool.label}</span>
|
|
425
|
-
</button>
|
|
426
|
-
{/each}
|
|
427
|
-
</div>
|
|
428
|
-
|
|
429
|
-
<!-- Grid opacity slider (matching production implementation) -->
|
|
430
|
-
<div class="pie-tool-graph__transparency-control">
|
|
431
|
-
<label for="grid-opacity">Grid:</label>
|
|
432
|
-
<input
|
|
433
|
-
type="range"
|
|
434
|
-
id="grid-opacity"
|
|
435
|
-
min="0"
|
|
436
|
-
max="1"
|
|
437
|
-
step="0.1"
|
|
438
|
-
bind:value={gridOpacity}
|
|
439
|
-
aria-label="Grid opacity"
|
|
440
|
-
/>
|
|
441
|
-
</div>
|
|
442
|
-
</div>
|
|
443
|
-
|
|
444
|
-
<!-- Canvas wrapper -->
|
|
445
|
-
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
446
|
-
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
447
|
-
<div
|
|
448
|
-
bind:this={canvasWrapperEl}
|
|
449
|
-
class="pie-tool-graph__canvas-wrapper"
|
|
450
|
-
role="img"
|
|
451
|
-
tabindex="0"
|
|
452
|
-
aria-label="Graph canvas - Use tools to add points and draw lines"
|
|
453
|
-
onclick={handleCanvasClick}
|
|
454
|
-
onkeydown={(e) => {
|
|
455
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
456
|
-
e.preventDefault();
|
|
457
|
-
handleCanvasClick(e as any);
|
|
458
|
-
}
|
|
459
|
-
}}
|
|
460
|
-
>
|
|
461
|
-
<svg
|
|
462
|
-
bind:this={svgCanvasEl}
|
|
463
|
-
class="pie-tool-graph__canvas"
|
|
464
|
-
viewBox="0 0 {viewBoxWidth} 100"
|
|
465
|
-
preserveAspectRatio="xMinYMin meet"
|
|
466
|
-
aria-hidden="true"
|
|
467
|
-
onpointermove={handleCanvasMouseMove}
|
|
468
|
-
onpointerup={handlePointerUp}
|
|
469
|
-
onpointerleave={handlePointerUp}
|
|
470
|
-
>
|
|
471
|
-
<!-- Grid Lines -->
|
|
472
|
-
<g class="pie-tool-graph__grid-lines" style="opacity: {gridOpacity}" aria-hidden="true">
|
|
473
|
-
<!-- Minor Horizontal Lines -->
|
|
474
|
-
{#each gridLines.minorHorizontal as yPos, index (index)}
|
|
475
|
-
<line
|
|
476
|
-
x1="0"
|
|
477
|
-
y1={yPos}
|
|
478
|
-
x2={viewBoxWidth}
|
|
479
|
-
y2={yPos}
|
|
480
|
-
class="pie-tool-graph__grid-line pie-tool-graph__grid-line--minor"
|
|
481
|
-
/>
|
|
482
|
-
{/each}
|
|
483
|
-
<!-- Major Horizontal Lines -->
|
|
484
|
-
{#each gridLines.majorHorizontal as yPos, index (index)}
|
|
485
|
-
<line
|
|
486
|
-
x1="0"
|
|
487
|
-
y1={yPos}
|
|
488
|
-
x2={viewBoxWidth}
|
|
489
|
-
y2={yPos}
|
|
490
|
-
class="pie-tool-graph__grid-line pie-tool-graph__grid-line--major"
|
|
491
|
-
/>
|
|
492
|
-
{/each}
|
|
493
|
-
|
|
494
|
-
<!-- Minor Vertical Lines -->
|
|
495
|
-
{#each gridLines.minorVertical as xPos, index (index)}
|
|
496
|
-
<line
|
|
497
|
-
x1={xPos}
|
|
498
|
-
y1="0"
|
|
499
|
-
x2={xPos}
|
|
500
|
-
y2="100"
|
|
501
|
-
class="pie-tool-graph__grid-line pie-tool-graph__grid-line--minor"
|
|
502
|
-
/>
|
|
503
|
-
{/each}
|
|
504
|
-
<!-- Major Vertical Lines -->
|
|
505
|
-
{#each gridLines.majorVertical as xPos, index (index)}
|
|
506
|
-
<line
|
|
507
|
-
x1={xPos}
|
|
508
|
-
y1="0"
|
|
509
|
-
x2={xPos}
|
|
510
|
-
y2="100"
|
|
511
|
-
class="pie-tool-graph__grid-line pie-tool-graph__grid-line--major"
|
|
512
|
-
/>
|
|
513
|
-
{/each}
|
|
514
|
-
</g>
|
|
515
|
-
|
|
516
|
-
<!-- Lines -->
|
|
517
|
-
<g class="pie-tool-graph__lines">
|
|
518
|
-
{#each lines as line (line.id)}
|
|
519
|
-
{@const p1 = getPointById(line.p1Id)}
|
|
520
|
-
{@const p2 = getPointById(line.p2Id)}
|
|
521
|
-
{#if p1 && p2}
|
|
522
|
-
<line
|
|
523
|
-
x1={p1.x}
|
|
524
|
-
y1={p1.y}
|
|
525
|
-
x2={p2.x}
|
|
526
|
-
y2={p2.y}
|
|
527
|
-
class="pie-tool-graph__user-line"
|
|
528
|
-
/>
|
|
529
|
-
{/if}
|
|
530
|
-
{/each}
|
|
531
|
-
</g>
|
|
532
|
-
|
|
533
|
-
<!-- Points -->
|
|
534
|
-
<g class="pie-tool-graph__points">
|
|
535
|
-
{#each points as point (point.id)}
|
|
536
|
-
<circle
|
|
537
|
-
cx={point.x}
|
|
538
|
-
cy={point.y}
|
|
539
|
-
r="2"
|
|
540
|
-
class="pie-tool-graph__user-point"
|
|
541
|
-
class:pie-tool-graph__user-point--highlight={isPointHighlighted(point.id)}
|
|
542
|
-
data-id={point.id}
|
|
543
|
-
role="button"
|
|
544
|
-
tabindex="0"
|
|
545
|
-
aria-label="Graph point {point.id}"
|
|
546
|
-
onpointerdown={(e) => {
|
|
547
|
-
e.stopPropagation();
|
|
548
|
-
handlePointPointerDown(point.id, e);
|
|
549
|
-
}}
|
|
550
|
-
onkeydown={(e) => {
|
|
551
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
552
|
-
e.preventDefault();
|
|
553
|
-
activatePoint(point.id);
|
|
554
|
-
}
|
|
555
|
-
}}
|
|
556
|
-
/>
|
|
557
|
-
{/each}
|
|
558
|
-
</g>
|
|
559
|
-
|
|
560
|
-
<!-- Temporary line feedback -->
|
|
561
|
-
{#if tempLineStartPointId && currentPointerPos}
|
|
562
|
-
{@const startPoint = getPointById(tempLineStartPointId)}
|
|
563
|
-
{#if startPoint}
|
|
564
|
-
<line
|
|
565
|
-
x1={startPoint.x}
|
|
566
|
-
y1={startPoint.y}
|
|
567
|
-
x2={currentPointerPos.x}
|
|
568
|
-
y2={currentPointerPos.y}
|
|
569
|
-
class="pie-tool-graph__temp-line"
|
|
570
|
-
/>
|
|
571
|
-
{/if}
|
|
572
|
-
{/if}
|
|
573
|
-
</svg>
|
|
574
|
-
</div>
|
|
575
|
-
</div>
|
|
576
|
-
|
|
577
|
-
{/if}
|
|
578
|
-
|
|
579
|
-
<style>
|
|
580
|
-
.pie-tool-graph {
|
|
581
|
-
position: relative;
|
|
582
|
-
background: var(--pie-background, #fff);
|
|
583
|
-
color: var(--pie-text, #111827);
|
|
584
|
-
width: 100%;
|
|
585
|
-
height: 100%;
|
|
586
|
-
min-height: 0;
|
|
587
|
-
overflow: hidden;
|
|
588
|
-
display: flex;
|
|
589
|
-
flex-direction: column;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/* Toolbar (matching production implementation: lighter teal) */
|
|
593
|
-
.pie-tool-graph__toolbar {
|
|
594
|
-
padding: 8px;
|
|
595
|
-
background: var(--pie-primary-light, #5a7fa3); /* Lighter teal-like color */
|
|
596
|
-
display: flex;
|
|
597
|
-
gap: 16px;
|
|
598
|
-
align-items: center;
|
|
599
|
-
flex-wrap: wrap;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
.pie-tool-graph__tool-buttons {
|
|
603
|
-
display: flex;
|
|
604
|
-
gap: 4px;
|
|
605
|
-
flex: 1;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
.pie-tool-graph__tool-button {
|
|
609
|
-
flex: 1;
|
|
610
|
-
display: flex;
|
|
611
|
-
flex-direction: column;
|
|
612
|
-
align-items: center;
|
|
613
|
-
gap: 4px;
|
|
614
|
-
padding: 8px 12px;
|
|
615
|
-
background: color-mix(in srgb, var(--pie-white, #fff) 20%, transparent);
|
|
616
|
-
border: 2px solid transparent;
|
|
617
|
-
border-radius: 4px;
|
|
618
|
-
cursor: pointer;
|
|
619
|
-
color: var(--pie-white, #fff);
|
|
620
|
-
font-size: 12px;
|
|
621
|
-
transition: all 0.2s;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
.pie-tool-graph__tool-button:hover {
|
|
625
|
-
background: color-mix(in srgb, var(--pie-white, #fff) 30%, transparent);
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
.pie-tool-graph__tool-button.pie-tool-graph__tool-button--active {
|
|
629
|
-
background: var(--pie-background, #fff);
|
|
630
|
-
color: var(--pie-primary-dark, #2c3e50);
|
|
631
|
-
border-color: var(--pie-primary-dark, #2c3e50);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
.pie-tool-graph__tool-icon {
|
|
635
|
-
width: 20px;
|
|
636
|
-
height: 20px;
|
|
637
|
-
display: flex;
|
|
638
|
-
align-items: center;
|
|
639
|
-
justify-content: center;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
.pie-tool-graph__tool-label {
|
|
643
|
-
font-size: 11px;
|
|
644
|
-
font-weight: 500;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
.pie-tool-graph__transparency-control {
|
|
648
|
-
display: flex;
|
|
649
|
-
align-items: center;
|
|
650
|
-
gap: 8px;
|
|
651
|
-
color: var(--pie-white, #fff);
|
|
652
|
-
font-size: 12px;
|
|
653
|
-
padding-left: 8px;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
.pie-tool-graph__transparency-control label {
|
|
657
|
-
font-weight: 500;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
.pie-tool-graph__transparency-control input[type='range'] {
|
|
661
|
-
width: 100px;
|
|
662
|
-
cursor: pointer;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
/* Canvas wrapper */
|
|
666
|
-
.pie-tool-graph__canvas-wrapper {
|
|
667
|
-
flex: 1;
|
|
668
|
-
background: var(--pie-background, #fff);
|
|
669
|
-
display: flex;
|
|
670
|
-
align-items: center;
|
|
671
|
-
justify-content: center;
|
|
672
|
-
overflow: hidden;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
.pie-tool-graph__canvas {
|
|
676
|
-
display: block;
|
|
677
|
-
width: 100%;
|
|
678
|
-
height: 100%;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
/* Grid lines (matching production implementation: dark gray) */
|
|
682
|
-
.pie-tool-graph__grid-line {
|
|
683
|
-
stroke: var(--pie-border-dark, #666);
|
|
684
|
-
vector-effect: non-scaling-stroke;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
.pie-tool-graph__grid-line--major {
|
|
688
|
-
stroke: var(--pie-border-dark, #333);
|
|
689
|
-
stroke-width: 0.75;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
.pie-tool-graph__grid-line--minor {
|
|
693
|
-
stroke: var(--pie-border-light, #ccc);
|
|
694
|
-
stroke-width: 0.5;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/* User drawing styles */
|
|
698
|
-
.pie-tool-graph__user-point {
|
|
699
|
-
cursor: pointer;
|
|
700
|
-
fill: var(--pie-primary, #3f51b5);
|
|
701
|
-
stroke: var(--pie-primary-dark, #2c3e50);
|
|
702
|
-
stroke-width: 0.5;
|
|
703
|
-
vector-effect: non-scaling-stroke;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
.pie-tool-graph__user-point.pie-tool-graph__user-point--highlight {
|
|
707
|
-
fill: var(--pie-missing, #ffc107);
|
|
708
|
-
stroke: var(--pie-missing-icon, #ff9800);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
.pie-tool-graph__user-line {
|
|
712
|
-
stroke: var(--pie-text, #333);
|
|
713
|
-
stroke-linecap: round;
|
|
714
|
-
stroke-width: 1;
|
|
715
|
-
vector-effect: non-scaling-stroke;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
.pie-tool-graph__temp-line {
|
|
719
|
-
pointer-events: none;
|
|
720
|
-
stroke: var(--pie-correct, #4caf50);
|
|
721
|
-
stroke-dasharray: 2, 2;
|
|
722
|
-
stroke-width: 0.75;
|
|
723
|
-
vector-effect: non-scaling-stroke;
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
</style>
|