@tscircuit/schematic-trace-solver 0.0.132 → 0.0.134

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.
@@ -31,6 +31,7 @@ import {
31
31
  tracePathCrossesAnyBounds,
32
32
  tracePathIntersectsBounds,
33
33
  } from "./geometry"
34
+ import { orderRoutedLabelsBeforeOverlappingPortLabels } from "./orderRoutedLabelsBeforeOverlappingPortLabels"
34
35
  import { getPinMap, getTracePins, toNetLabelPlacementPatch } from "./traces"
35
36
  import type {
36
37
  AvailableNetOrientationSolverParams,
@@ -120,7 +121,12 @@ export class AvailableNetOrientationSolver extends BaseSolver {
120
121
  }
121
122
  this.sortCrowdedTopBankByPin(indices)
122
123
 
123
- const blockedStandaloneLabelIndex = indices.find((index) => {
124
+ const orderedIndices = orderRoutedLabelsBeforeOverlappingPortLabels({
125
+ labelIndices: indices,
126
+ netLabelPlacements: this.outputNetLabelPlacements,
127
+ })
128
+
129
+ const blockedStandaloneLabelIndex = orderedIndices.find((index) => {
124
130
  const label = this.outputNetLabelPlacements[index]!
125
131
  const orientations = this.getAvailableOrientations(label)
126
132
  return (
@@ -131,7 +137,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
131
137
  this.labelIntersectsDifferentNetTrace(label)
132
138
  )
133
139
  })
134
- if (blockedStandaloneLabelIndex === undefined) return indices
140
+ if (blockedStandaloneLabelIndex === undefined) return orderedIndices
135
141
  this.blockedStandaloneLabelIndex = blockedStandaloneLabelIndex
136
142
  this.blockedStandaloneLabelTargetAnchorX = null
137
143
 
@@ -142,8 +148,8 @@ export class AvailableNetOrientationSolver extends BaseSolver {
142
148
  const affectedSlots: number[] = []
143
149
  const labelsToOrder: number[] = []
144
150
 
145
- for (let slot = 0; slot < indices.length; slot++) {
146
- const index = indices[slot]!
151
+ for (let slot = 0; slot < orderedIndices.length; slot++) {
152
+ const index = orderedIndices[slot]!
147
153
  const label = this.outputNetLabelPlacements[index]!
148
154
  const orientations = this.getAvailableOrientations(label)
149
155
  if (
@@ -182,10 +188,10 @@ export class AvailableNetOrientationSolver extends BaseSolver {
182
188
  ).x
183
189
  }
184
190
  for (let i = 0; i < affectedSlots.length; i++) {
185
- indices[affectedSlots[i]!] = labelsToOrder[i]!
191
+ orderedIndices[affectedSlots[i]!] = labelsToOrder[i]!
186
192
  }
187
193
 
188
- return indices
194
+ return orderedIndices
189
195
  }
190
196
 
191
197
  private sortCrowdedTopBankByPin(indices: number[]) {
@@ -0,0 +1,47 @@
1
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
2
+ import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
3
+ import { rectsOverlap } from "./geometry"
4
+
5
+ const isPortOnlyLabel = (label: NetLabelPlacement) =>
6
+ label.mspConnectionPairIds.length === 0
7
+
8
+ export const orderRoutedLabelsBeforeOverlappingPortLabels = ({
9
+ labelIndices,
10
+ netLabelPlacements,
11
+ }: {
12
+ labelIndices: number[]
13
+ netLabelPlacements: NetLabelPlacement[]
14
+ }) => {
15
+ const orderedLabelIndices = [...labelIndices]
16
+
17
+ for (const routedLabelIndex of labelIndices) {
18
+ const routedLabel = netLabelPlacements[routedLabelIndex]!
19
+ if (isPortOnlyLabel(routedLabel)) continue
20
+
21
+ const routedBounds = getRectBounds(
22
+ routedLabel.center,
23
+ routedLabel.width,
24
+ routedLabel.height,
25
+ )
26
+ const blockingPosition = orderedLabelIndices.findIndex((labelIndex) => {
27
+ const portOnlyLabel = netLabelPlacements[labelIndex]!
28
+ if (!isPortOnlyLabel(portOnlyLabel)) return false
29
+ if (portOnlyLabel.globalConnNetId === routedLabel.globalConnNetId) {
30
+ return false
31
+ }
32
+ const portOnlyBounds = getRectBounds(
33
+ portOnlyLabel.center,
34
+ portOnlyLabel.width,
35
+ portOnlyLabel.height,
36
+ )
37
+ return rectsOverlap(routedBounds, portOnlyBounds)
38
+ })
39
+ const routedPosition = orderedLabelIndices.indexOf(routedLabelIndex)
40
+ if (blockingPosition === -1 || blockingPosition > routedPosition) continue
41
+
42
+ orderedLabelIndices.splice(routedPosition, 1)
43
+ orderedLabelIndices.splice(blockingPosition, 0, routedLabelIndex)
44
+ }
45
+
46
+ return orderedLabelIndices
47
+ }
@@ -1,4 +1,5 @@
1
1
  import { getConnectivityMapsFromInputProblem } from "lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem"
2
+ import type { Point } from "@tscircuit/math-utils"
2
3
  import {
3
4
  DEFAULT_MAX_MSP_PAIR_DISTANCE,
4
5
  type MspConnectionPair,
@@ -20,13 +21,118 @@ import { isLabeledPeripheralConnection } from "../MspConnectionPairSolver/isLabe
20
21
 
21
22
  const NEAREST_NEIGHBOR_COUNT = 3
22
23
 
23
- const distance = (p1: InputPin, p2: InputPin) => {
24
+ const distance = (p1: Point, p2: Point) => {
24
25
  return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2))
25
26
  }
26
27
 
27
28
  const manhattanDistance = (p1: InputPin, p2: InputPin) =>
28
29
  Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)
29
30
 
31
+ const EPS = 1e-9
32
+
33
+ const between = (value: number, first: number, second: number) =>
34
+ value >= Math.min(first, second) - EPS &&
35
+ value <= Math.max(first, second) + EPS
36
+
37
+ const getAxisAlignedIntersection = (
38
+ firstStart: Point,
39
+ firstEnd: Point,
40
+ secondStart: Point,
41
+ secondEnd: Point,
42
+ ) => {
43
+ const firstIsHorizontal = Math.abs(firstStart.y - firstEnd.y) <= EPS
44
+ const firstIsVertical = Math.abs(firstStart.x - firstEnd.x) <= EPS
45
+ const secondIsHorizontal = Math.abs(secondStart.y - secondEnd.y) <= EPS
46
+ const secondIsVertical = Math.abs(secondStart.x - secondEnd.x) <= EPS
47
+
48
+ if (firstIsHorizontal && secondIsVertical) {
49
+ const point = { x: secondStart.x, y: firstStart.y }
50
+ return between(point.x, firstStart.x, firstEnd.x) &&
51
+ between(point.y, secondStart.y, secondEnd.y)
52
+ ? point
53
+ : null
54
+ }
55
+ if (firstIsVertical && secondIsHorizontal) {
56
+ const point = { x: firstStart.x, y: secondStart.y }
57
+ return between(point.y, firstStart.y, firstEnd.y) &&
58
+ between(point.x, secondStart.x, secondEnd.x)
59
+ ? point
60
+ : null
61
+ }
62
+ if (
63
+ firstIsHorizontal &&
64
+ secondIsHorizontal &&
65
+ Math.abs(firstStart.y - secondStart.y) <= EPS
66
+ ) {
67
+ const overlapMin = Math.max(
68
+ Math.min(firstStart.x, firstEnd.x),
69
+ Math.min(secondStart.x, secondEnd.x),
70
+ )
71
+ const overlapMax = Math.min(
72
+ Math.max(firstStart.x, firstEnd.x),
73
+ Math.max(secondStart.x, secondEnd.x),
74
+ )
75
+ if (overlapMin > overlapMax + EPS) return null
76
+ return {
77
+ x: Math.max(overlapMin, Math.min(firstStart.x, overlapMax)),
78
+ y: firstStart.y,
79
+ }
80
+ }
81
+ if (
82
+ firstIsVertical &&
83
+ secondIsVertical &&
84
+ Math.abs(firstStart.x - secondStart.x) <= EPS
85
+ ) {
86
+ const overlapMin = Math.max(
87
+ Math.min(firstStart.y, firstEnd.y),
88
+ Math.min(secondStart.y, secondEnd.y),
89
+ )
90
+ const overlapMax = Math.min(
91
+ Math.max(firstStart.y, firstEnd.y),
92
+ Math.max(secondStart.y, secondEnd.y),
93
+ )
94
+ if (overlapMin > overlapMax + EPS) return null
95
+ return {
96
+ x: firstStart.x,
97
+ y: Math.max(overlapMin, Math.min(firstStart.y, overlapMax)),
98
+ }
99
+ }
100
+ return null
101
+ }
102
+
103
+ const clipPathAtFirstIntersection = (
104
+ tracePath: SolvedTracePath["tracePath"],
105
+ existingTraces: SolvedTracePath[],
106
+ ) => {
107
+ for (let pathIndex = 0; pathIndex < tracePath.length - 1; pathIndex++) {
108
+ const pathStart = tracePath[pathIndex]!
109
+ const pathEnd = tracePath[pathIndex + 1]!
110
+ const intersections = existingTraces.flatMap((trace) =>
111
+ trace.tracePath.slice(0, -1).flatMap((traceStart, traceIndex) => {
112
+ const intersection = getAxisAlignedIntersection(
113
+ pathStart,
114
+ pathEnd,
115
+ traceStart,
116
+ trace.tracePath[traceIndex + 1]!,
117
+ )
118
+ return intersection ? [intersection] : []
119
+ }),
120
+ )
121
+ intersections.sort(
122
+ (first, second) =>
123
+ distance(pathStart, first) - distance(pathStart, second),
124
+ )
125
+ const intersection = intersections[0]
126
+ if (!intersection) continue
127
+ const clippedPath = tracePath.slice(0, pathIndex + 1)
128
+ if (distance(clippedPath.at(-1)!, intersection) > EPS) {
129
+ clippedPath.push(intersection)
130
+ }
131
+ return clippedPath
132
+ }
133
+ return null
134
+ }
135
+
30
136
  export class LongDistancePairSolver extends BaseSolver {
31
137
  public solvedLongDistanceTraces: SolvedTracePath[] = []
32
138
  private queuedCandidatePairs: Array<
@@ -196,14 +302,42 @@ export class LongDistancePairSolver extends BaseSolver {
196
302
  } else if (this.subSolver?.solved) {
197
303
  const newTracePath = this.subSolver.solvedTracePath
198
304
  if (newTracePath && this.currentCandidatePair) {
305
+ const [p1, p2] = this.currentCandidatePair
306
+ const globalConnNetId = this.netConnMap.getNetConnectedToId(p1.pinId)!
307
+ const sameNetTraces = this.allSolvedTraces.filter(
308
+ (trace) => trace.globalConnNetId === globalConnNetId,
309
+ )
310
+ const inputNetConnection = this.inputProblem.netConnections.find(
311
+ (connection) =>
312
+ connection.pinIds.length === 3 &&
313
+ connection.pinIds.includes(p1.pinId) &&
314
+ connection.pinIds.includes(p2.pinId),
315
+ )
316
+ const sourceChip = this.chipMap[p1.chipId]
317
+ const targetRailTrace = sameNetTraces.find(
318
+ (trace) =>
319
+ trace.pinIds.includes(p2.pinId) &&
320
+ trace.pins.every((pin) => pin.chipId === p2.chipId),
321
+ )
322
+ const canJoinThreePinNet =
323
+ inputNetConnection !== undefined &&
324
+ sourceChip?.pins.length === 2 &&
325
+ targetRailTrace !== undefined
326
+ const junctionTracePath = canJoinThreePinNet
327
+ ? clipPathAtFirstIntersection(newTracePath, sameNetTraces)
328
+ : null
329
+ const acceptedTracePath = junctionTracePath ?? newTracePath
330
+ const tracesToCheck = junctionTracePath
331
+ ? this.allSolvedTraces.filter(
332
+ (trace) => trace.globalConnNetId !== globalConnNetId,
333
+ )
334
+ : this.allSolvedTraces
199
335
  const isTraceClear = !doesTraceOverlapWithExistingTraces(
200
- newTracePath,
201
- this.allSolvedTraces,
336
+ acceptedTracePath,
337
+ tracesToCheck,
202
338
  )
203
339
 
204
340
  if (isTraceClear) {
205
- const [p1, p2] = this.currentCandidatePair
206
- const globalConnNetId = this.netConnMap.getNetConnectedToId(p1.pinId)!
207
341
  const mspPairId = `${p1.pinId}-${p2.pinId}`
208
342
 
209
343
  const newSolvedTrace: SolvedTracePath = {
@@ -211,7 +345,7 @@ export class LongDistancePairSolver extends BaseSolver {
211
345
  dcConnNetId: globalConnNetId,
212
346
  globalConnNetId,
213
347
  pins: [p1, p2],
214
- tracePath: newTracePath,
348
+ tracePath: acceptedTracePath,
215
349
  mspConnectionPairIds: [mspPairId],
216
350
  pinIds: [p1.pinId, p2.pinId],
217
351
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.132",
4
+ "version": "0.0.134",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -0,0 +1,66 @@
1
+ <svg width="640" height="640" viewBox="0 0 640 640" xmlns="http://www.w3.org/2000/svg"><rect width="100%" height="100%" fill="white"/><g><polyline data-points="0.4,1.5 2.5,1.5" data-type="line" data-label="" points="339.77220956719816,237.08428246013665 473.71298405466973,237.08428246013665" fill="none" stroke="hsl(238, 100%, 50%, 0.8)" stroke-width="1px"/></g><g><polyline data-points="-1.7,1.65 -1.7,1.3499999999999999" data-type="line" data-label="" points="205.83143507972665,227.51708428246013 205.83143507972665,246.65148063781322" fill="none" stroke="hsl(170, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="-1.7,1.65 -0.4,1.5" data-type="line" data-label="" points="205.83143507972665,227.51708428246013 288.74715261959,237.08428246013665" fill="none" stroke="hsl(170, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="-1.7,1.3499999999999999 -0.4,1.5" data-type="line" data-label="" points="205.83143507972665,246.65148063781322 288.74715261959,237.08428246013665" fill="none" stroke="hsl(170, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="-1.7,-1.35 -1.7,-1.65" data-type="line" data-label="" points="205.83143507972665,418.8610478359909 205.83143507972665,437.99544419134395" fill="none" stroke="hsl(124, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="-1.7,-1.35 3.5,1.5" data-type="line" data-label="" points="205.83143507972665,418.8610478359909 537.49430523918,237.08428246013665" fill="none" stroke="hsl(124, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="-1.7,-1.65 3.5,1.5" data-type="line" data-label="" points="205.83143507972665,437.99544419134395 537.49430523918,237.08428246013665" fill="none" stroke="hsl(124, 100%, 50%, 0.8)" stroke-width="1px" stroke-dasharray="4 2"/></g><g><polyline data-points="0.4,1.5 2.5,1.5" data-type="line" data-label="" points="339.77220956719816,237.08428246013665 473.71298405466973,237.08428246013665" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="-1.7,1.3499999999999999 -1.5,1.3499999999999999 -1.5,1.65 -1.7,1.65" data-type="line" data-label="" points="205.83143507972665,246.65148063781322 218.5876993166287,246.65148063781322 218.5876993166287,227.51708428246013 205.83143507972665,227.51708428246013" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="-1.7,-1.65 -1.5,-1.65 -1.5,-1.35 -1.7,-1.35" data-type="line" data-label="" points="205.83143507972665,437.99544419134395 218.5876993166287,437.99544419134395 218.5876993166287,418.8610478359909 205.83143507972665,418.8610478359909" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="3.5,1.5 3.7,1.5 3.7,-1.35 -1.5,-1.35" data-type="line" data-label="" points="537.49430523918,237.08428246013665 550.2505694760821,237.08428246013665 550.2505694760821,418.8610478359909 218.5876993166287,418.8610478359909" fill="none" stroke="purple" stroke-width="1px"/></g><g><polyline data-points="-0.4,1.5 -1.5,1.5" data-type="line" data-label="" points="288.74715261959,237.08428246013665 218.5876993166287,237.08428246013665" fill="none" stroke="purple" stroke-width="1px"/></g><g><rect data-type="rect" data-label="J1" data-x="-3" data-y="0" x="40" y="205.19362186788152" width="165.83143507972665" height="255.12528473804105" fill="hsl(183, 100%, 50%, 0.8)" stroke="black" stroke-width="0.015678571428571427"/></g><g><rect data-type="rect" data-label="R1" data-x="0" data-y="1.5" x="288.74715261959" y="221.13895216400908" width="51.025056947608164" height="31.89066059225513" fill="hsl(71, 100%, 50%, 0.8)" stroke="black" stroke-width="0.015678571428571427"/></g><g><rect data-type="rect" data-label="D1" data-x="3" data-y="1.5" x="473.71298405466973" y="211.57175398633254" width="63.78132118451026" height="51.02505694760822" fill="hsl(357, 100%, 50%, 0.8)" stroke="black" stroke-width="0.015678571428571427"/></g><g><rect data-type="rect" data-label="J1 TYPE-C-31-M-12" data-x="-3" data-y="2.3" x="71.89066059225513" y="179.68109339407744" width="102.05011389521638" height="12.75626423690207" fill="rgba(160, 0, 220, 0.14)" stroke="black" stroke-width="0.015678571428571427"/></g><g><rect data-type="rect" data-label="R1 1kΩ" data-x="0" data-y="1.95" x="301.50341685649204" y="202.004555808656" width="25.512528473804082" height="12.75626423690207" fill="rgba(160, 0, 220, 0.14)" stroke="black" stroke-width="0.015678571428571427"/></g><g><rect data-type="rect" data-label="D1 white" data-x="3" data-y="2.15" x="489.6583143507973" y="189.24829157175398" width="31.89066059225513" height="12.756264236902041" fill="rgba(160, 0, 220, 0.14)" stroke="black" stroke-width="0.015678571428571427"/></g><g><rect data-type="rect" data-label="netId: VBUS
2
+ globalConnNetId: connectivity_net1" data-x="-0.95" data-y="1.74" x="238.3599088838269" y="206.46924829157177" width="30.615034168564904" height="30.615034168564904" fill="#ef444466" stroke="#ef4444" stroke-width="0.015678571428571427"/></g><g><rect data-type="rect" data-label="netId: J1_GND1
3
+ globalConnNetId: connectivity_net2" data-x="4.09" data-y="1.5" x="550.2505694760821" y="221.7767653758542" width="49.74943052391802" height="30.615034168564904" fill="#00000066" stroke="#000000" stroke-width="0.015678571428571427"/></g><g><rect data-type="rect" data-label="netId: VBUS_LED
4
+ globalConnNetId: connectivity_net0" data-x="1.45" data-y="1.725" x="400.3644646924829" y="208.38268792710704" width="12.756264236902098" height="28.701594533029606" fill="hsl(40, 100%, 50%, 0.35)" stroke="black" stroke-width="0.015678571428571427"/></g><g><circle data-type="point" data-label="J1.VBUS1
5
+ x+" data-x="-1.7" data-y="1.65" cx="205.83143507972665" cy="227.51708428246013" r="3" fill="hsl(208, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="J1.VBUS2
6
+ x+" data-x="-1.7" data-y="1.3499999999999999" cx="205.83143507972665" cy="246.65148063781322" r="3" fill="hsl(209, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="J1.CC1
7
+ x+" data-x="-1.7" data-y="1.0499999999999998" cx="205.83143507972665" cy="265.7858769931663" r="3" fill="hsl(322, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="J1.CC2
8
+ x+" data-x="-1.7" data-y="0.75" cx="205.83143507972665" cy="284.92027334851934" r="3" fill="hsl(323, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="J1.DP1
9
+ x+" data-x="-1.7" data-y="0.44999999999999996" cx="205.83143507972665" cy="304.05466970387243" r="3" fill="hsl(246, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="J1.DP2
10
+ x+" data-x="-1.7" data-y="0.1499999999999999" cx="205.83143507972665" cy="323.1890660592255" r="3" fill="hsl(247, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="J1.DM1
11
+ x+" data-x="-1.7" data-y="-0.1499999999999999" cx="205.83143507972665" cy="342.32346241457856" r="3" fill="hsl(153, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="J1.DM2
12
+ x+" data-x="-1.7" data-y="-0.4500000000000002" cx="205.83143507972665" cy="361.45785876993165" r="3" fill="hsl(154, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="J1.SBU1
13
+ x+" data-x="-1.7" data-y="-0.75" cx="205.83143507972665" cy="380.59225512528474" r="3" fill="hsl(122, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="J1.SBU2
14
+ x+" data-x="-1.7" data-y="-1.0499999999999998" cx="205.83143507972665" cy="399.72665148063777" r="3" fill="hsl(123, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="J1.GND1
15
+ x+" data-x="-1.7" data-y="-1.35" cx="205.83143507972665" cy="418.8610478359909" r="3" fill="hsl(315, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="J1.GND2
16
+ x+" data-x="-1.7" data-y="-1.65" cx="205.83143507972665" cy="437.99544419134395" r="3" fill="hsl(316, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="R1.1
17
+ x-" data-x="-0.4" data-y="1.5" cx="288.74715261959" cy="237.08428246013665" r="3" fill="hsl(226, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="R1.2
18
+ x+" data-x="0.4" data-y="1.5" cx="339.77220956719816" cy="237.08428246013665" r="3" fill="hsl(227, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="D1.1
19
+ x-" data-x="2.5" data-y="1.5" cx="473.71298405466973" cy="237.08428246013665" r="3" fill="hsl(32, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="D1.2
20
+ x+" data-x="3.5" data-y="1.5" cx="537.49430523918" cy="237.08428246013665" r="3" fill="hsl(33, 100%, 50%, 0.8)"/></g><g><circle data-type="point" data-label="anchorPoint
21
+ orientation: y+" data-x="-0.95" data-y="1.5" cx="253.66742596810934" cy="237.08428246013665" r="3" fill="hsl(40, 100%, 50%, 0.9)"/></g><g><circle data-type="point" data-label="anchorPoint
22
+ orientation: x+" data-x="3.7" data-y="1.5" cx="550.2505694760821" cy="237.08428246013665" r="3" fill="hsl(40, 100%, 50%, 0.9)"/></g><g><circle data-type="point" data-label="anchorPoint
23
+ orientation: y+" data-x="1.45" data-y="1.5" cx="406.74259681093395" cy="237.08428246013665" r="3" fill="hsl(40, 100%, 50%, 0.9)"/></g><g id="crosshair" style="display: none"><line id="crosshair-h" y1="0" y2="640" stroke="#666" stroke-width="0.5"/><line id="crosshair-v" x1="0" x2="640" stroke="#666" stroke-width="0.5"/><text id="coordinates" font-family="monospace" font-size="12" fill="#666"></text></g><script><![CDATA[
24
+ document.currentScript.parentElement.addEventListener('mousemove', (e) => {
25
+ const svg = e.currentTarget;
26
+ const rect = svg.getBoundingClientRect();
27
+ const x = e.clientX - rect.left;
28
+ const y = e.clientY - rect.top;
29
+ const crosshair = svg.getElementById('crosshair');
30
+ const h = svg.getElementById('crosshair-h');
31
+ const v = svg.getElementById('crosshair-v');
32
+ const coords = svg.getElementById('coordinates');
33
+
34
+ crosshair.style.display = 'block';
35
+ h.setAttribute('x1', '0');
36
+ h.setAttribute('x2', '640');
37
+ h.setAttribute('y1', y);
38
+ h.setAttribute('y2', y);
39
+ v.setAttribute('x1', x);
40
+ v.setAttribute('x2', x);
41
+ v.setAttribute('y1', '0');
42
+ v.setAttribute('y2', '640');
43
+
44
+ // Calculate real coordinates using inverse transformation
45
+ const matrix = {"a":63.781321184510254,"c":0,"e":314.2596810933941,"b":0,"d":-63.781321184510254,"f":332.75626423690204};
46
+ // Manually invert and apply the affine transform
47
+ // Since we only use translate and scale, we can directly compute:
48
+ // x' = (x - tx) / sx
49
+ // y' = (y - ty) / sy
50
+ const sx = matrix.a;
51
+ const sy = matrix.d;
52
+ const tx = matrix.e;
53
+ const ty = matrix.f;
54
+ const realPoint = {
55
+ x: (x - tx) / sx,
56
+ y: (y - ty) / sy // Flip y back since we used negative scale
57
+ }
58
+
59
+ coords.textContent = `(${realPoint.x.toFixed(2)}, ${realPoint.y.toFixed(2)})`;
60
+ coords.setAttribute('x', (x + 5).toString());
61
+ coords.setAttribute('y', (y - 5).toString());
62
+ });
63
+ document.currentScript.parentElement.addEventListener('mouseleave', () => {
64
+ document.currentScript.parentElement.getElementById('crosshair').style.display = 'none';
65
+ });
66
+ ]]></script></svg>