@tscircuit/schematic-trace-solver 0.0.68 → 0.0.70

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/index.js CHANGED
@@ -1234,12 +1234,43 @@ var TraceOverlapIssueSolver = class extends BaseSolver {
1234
1234
  }) {
1235
1235
  const blockingCollisions = this.getBlockingCollisions({ group, offset });
1236
1236
  if (blockingCollisions.length === 0) return offset;
1237
- return blockingCollisions.flatMap(({ start, isVertical: isVertical4, obstacle }) => [
1238
- (isVertical4 ? obstacle.minX - start.x : obstacle.minY - start.y) - this.SHIFT_DISTANCE,
1239
- (isVertical4 ? obstacle.maxX - start.x : obstacle.maxY - start.y) + this.SHIFT_DISTANCE
1240
- ]).filter(
1241
- (candidateOffset) => this.getBlockingCollisions({ group, offset: candidateOffset }).length === 0
1242
- ).sort((a, b) => Math.abs(a - offset) - Math.abs(b - offset))[0] ?? offset;
1237
+ const crossesThroughObstacle = (candidateOffset) => blockingCollisions.some(({ start, isVertical: isVertical4, obstacle }) => {
1238
+ let orig;
1239
+ let min;
1240
+ let max;
1241
+ if (isVertical4) {
1242
+ orig = start.x;
1243
+ min = obstacle.minX;
1244
+ max = obstacle.maxX;
1245
+ } else {
1246
+ orig = start.y;
1247
+ min = obstacle.minY;
1248
+ max = obstacle.maxY;
1249
+ }
1250
+ const next = orig + candidateOffset;
1251
+ return orig <= min + EPS4 && next >= max - EPS4 || orig >= max - EPS4 && next <= min + EPS4;
1252
+ });
1253
+ const edges = blockingCollisions.map(({ start, isVertical: isVertical4, obstacle }) => {
1254
+ if (isVertical4) {
1255
+ return {
1256
+ coord: start.x,
1257
+ lowEdge: obstacle.minX,
1258
+ highEdge: obstacle.maxX
1259
+ };
1260
+ }
1261
+ return { coord: start.y, lowEdge: obstacle.minY, highEdge: obstacle.maxY };
1262
+ });
1263
+ const bestCandidate = (clearance) => {
1264
+ const candidates = edges.flatMap(({ coord, lowEdge, highEdge }) => [
1265
+ lowEdge - coord - clearance,
1266
+ highEdge - coord + clearance
1267
+ ]);
1268
+ return candidates.filter((candidateOffset) => !crossesThroughObstacle(candidateOffset)).filter(
1269
+ (candidateOffset) => this.getBlockingCollisions({ group, offset: candidateOffset }).length === 0
1270
+ ).sort((a, b) => Math.abs(a - offset) - Math.abs(b - offset))[0];
1271
+ };
1272
+ const EDGE_MARGIN = this.SHIFT_DISTANCE / 10;
1273
+ return bestCandidate(this.SHIFT_DISTANCE) ?? bestCandidate(EDGE_MARGIN) ?? offset;
1243
1274
  }
1244
1275
  getBlockingCollisions({
1245
1276
  group,
@@ -2799,19 +2830,32 @@ var detectTraceLabelOverlap = ({
2799
2830
  };
2800
2831
 
2801
2832
  // lib/solvers/TraceLabelOverlapAvoidanceSolver/violation.ts
2802
- var findTraceViolationZone = (path, labelBounds) => {
2803
- const isPointInside = (p) => p.x > labelBounds.minX && p.x < labelBounds.maxX && p.y > labelBounds.minY && p.y < labelBounds.maxY;
2804
- let firstInsideIndex = -1;
2805
- let lastInsideIndex = -1;
2806
- for (let i = 0; i < path.length; i++) {
2807
- if (isPointInside(path[i])) {
2808
- if (firstInsideIndex === -1) {
2809
- firstInsideIndex = i;
2810
- }
2811
- lastInsideIndex = i;
2833
+ var findTraceViolationZone = ({
2834
+ path,
2835
+ labelBounds,
2836
+ paddedBounds = labelBounds
2837
+ }) => {
2838
+ let firstCollidingSeg = -1;
2839
+ let lastCollidingSeg = -1;
2840
+ for (let i = 0; i < path.length - 1; i++) {
2841
+ if (segmentIntersectsRect2(path[i], path[i + 1], labelBounds)) {
2842
+ if (firstCollidingSeg === -1) firstCollidingSeg = i;
2843
+ lastCollidingSeg = i;
2812
2844
  }
2813
2845
  }
2814
- return { firstInsideIndex, lastInsideIndex };
2846
+ if (firstCollidingSeg === -1) {
2847
+ return { firstInsideIndex: -1, lastInsideIndex: -1 };
2848
+ }
2849
+ const isInsidePadded = (p) => p.x > paddedBounds.minX && p.x < paddedBounds.maxX && p.y > paddedBounds.minY && p.y < paddedBounds.maxY;
2850
+ let entryIndex = firstCollidingSeg;
2851
+ while (entryIndex > 0 && isInsidePadded(path[entryIndex])) {
2852
+ entryIndex--;
2853
+ }
2854
+ let exitIndex = lastCollidingSeg + 1;
2855
+ while (exitIndex < path.length - 1 && isInsidePadded(path[exitIndex])) {
2856
+ exitIndex++;
2857
+ }
2858
+ return { firstInsideIndex: entryIndex + 1, lastInsideIndex: exitIndex - 1 };
2815
2859
  };
2816
2860
 
2817
2861
  // lib/solvers/TraceLabelOverlapAvoidanceSolver/trySnipAndReconnect.ts
@@ -2917,20 +2961,21 @@ var generateFourPointDetourCandidates = ({
2917
2961
  }
2918
2962
  exitPoint = initialTrace.tracePath[exitIndex];
2919
2963
  } else {
2920
- let collidingSegIndex = -1;
2964
+ let firstCollidingSeg = -1;
2965
+ let lastCollidingSeg = -1;
2921
2966
  for (let i = 0; i < initialTrace.tracePath.length - 1; i++) {
2922
2967
  if (segmentIntersectsRect(
2923
2968
  initialTrace.tracePath[i],
2924
2969
  initialTrace.tracePath[i + 1],
2925
- { ...paddedLabelBounds, chipId: "temp-obstacle" }
2970
+ { ...labelBounds, chipId: "temp-obstacle" }
2926
2971
  )) {
2927
- collidingSegIndex = i;
2928
- break;
2972
+ if (firstCollidingSeg === -1) firstCollidingSeg = i;
2973
+ lastCollidingSeg = i;
2929
2974
  }
2930
2975
  }
2931
- if (collidingSegIndex === -1) return [];
2932
- entryIndex = collidingSegIndex;
2933
- exitIndex = collidingSegIndex + 1;
2976
+ if (firstCollidingSeg === -1) return [];
2977
+ entryIndex = firstCollidingSeg;
2978
+ exitIndex = lastCollidingSeg + 1;
2934
2979
  entryPoint = initialTrace.tracePath[entryIndex];
2935
2980
  exitPoint = initialTrace.tracePath[exitIndex];
2936
2981
  }
@@ -3039,10 +3084,18 @@ var generateRerouteCandidates = ({
3039
3084
  paddingBuffer,
3040
3085
  detourCount
3041
3086
  });
3042
- const { firstInsideIndex, lastInsideIndex } = findTraceViolationZone(
3043
- initialTrace.tracePath,
3044
- labelBounds
3045
- );
3087
+ const effectivePadding = paddingBuffer + detourCount * paddingBuffer;
3088
+ const paddedLabelBounds = {
3089
+ minX: labelBounds.minX - effectivePadding,
3090
+ maxX: labelBounds.maxX + effectivePadding,
3091
+ minY: labelBounds.minY - effectivePadding,
3092
+ maxY: labelBounds.maxY + effectivePadding
3093
+ };
3094
+ const { firstInsideIndex, lastInsideIndex } = findTraceViolationZone({
3095
+ path: initialTrace.tracePath,
3096
+ labelBounds,
3097
+ paddedBounds: paddedLabelBounds
3098
+ });
3046
3099
  const snipReconnectCandidates = generateSnipAndReconnectCandidates({
3047
3100
  initialTrace,
3048
3101
  firstInsideIndex,
@@ -3131,7 +3184,11 @@ var SingleOverlapSolver = class extends BaseSolver {
3131
3184
  this._tried++;
3132
3185
  const nextCandidatePath = this.queuedCandidatePaths.shift();
3133
3186
  const simplifiedPath = simplifyPath(nextCandidatePath);
3134
- if (!isPathCollidingWithObstacles(simplifiedPath, this.obstacles) && !doesPathCoincideWithTraces(simplifiedPath, this.tracesToAvoidOverlapping)) {
3187
+ const stillOverlapsLabel = detectTraceLabelOverlap({
3188
+ traces: [{ ...this.initialTrace, tracePath: simplifiedPath }],
3189
+ netLabels: [this.label]
3190
+ }).length > 0;
3191
+ if (!stillOverlapsLabel && !isPathCollidingWithObstacles(simplifiedPath, this.obstacles) && !doesPathCoincideWithTraces(simplifiedPath, this.tracesToAvoidOverlapping)) {
3135
3192
  this.solvedTracePath = simplifiedPath;
3136
3193
  this.solved = true;
3137
3194
  }
@@ -55,10 +55,19 @@ export const generateRerouteCandidates = ({
55
55
  detourCount,
56
56
  })
57
57
 
58
- const { firstInsideIndex, lastInsideIndex } = findTraceViolationZone(
59
- initialTrace.tracePath,
58
+ const effectivePadding = paddingBuffer + detourCount * paddingBuffer
59
+ const paddedLabelBounds = {
60
+ minX: labelBounds.minX - effectivePadding,
61
+ maxX: labelBounds.maxX + effectivePadding,
62
+ minY: labelBounds.minY - effectivePadding,
63
+ maxY: labelBounds.maxY + effectivePadding,
64
+ }
65
+
66
+ const { firstInsideIndex, lastInsideIndex } = findTraceViolationZone({
67
+ path: initialTrace.tracePath,
60
68
  labelBounds,
61
- )
69
+ paddedBounds: paddedLabelBounds,
70
+ })
62
71
 
63
72
  const snipReconnectCandidates = generateSnipAndReconnectCandidates({
64
73
  initialTrace,
@@ -9,6 +9,7 @@ import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/Schemati
9
9
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
10
10
  import { generateRerouteCandidates } from "../../rerouteCollidingTrace"
11
11
  import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
12
+ import { detectTraceLabelOverlap } from "../../detectTraceLabelOverlap"
12
13
 
13
14
  interface SingleOverlapSolverInput {
14
15
  trace: SolvedTracePath
@@ -141,7 +142,17 @@ export class SingleOverlapSolver extends BaseSolver {
141
142
  const nextCandidatePath = this.queuedCandidatePaths.shift()!
142
143
  const simplifiedPath = simplifyPath(nextCandidatePath)
143
144
 
145
+ // A candidate is only valid if it actually clears the label it is meant to
146
+ // avoid. Without this check a "detour" that still grazes the label (e.g. one
147
+ // built around the wrong segment) would be accepted as solved.
148
+ const stillOverlapsLabel =
149
+ detectTraceLabelOverlap({
150
+ traces: [{ ...this.initialTrace, tracePath: simplifiedPath }],
151
+ netLabels: [this.label],
152
+ }).length > 0
153
+
144
154
  if (
155
+ !stillOverlapsLabel &&
145
156
  !isPathCollidingWithObstacles(simplifiedPath, this.obstacles) &&
146
157
  !doesPathCoincideWithTraces(simplifiedPath, this.tracesToAvoidOverlapping)
147
158
  ) {
@@ -71,25 +71,30 @@ export const generateFourPointDetourCandidates = ({
71
71
  }
72
72
  exitPoint = initialTrace.tracePath[exitIndex]
73
73
  } else {
74
- // STRATEGY 1: Single Label - find the first intersecting segment
75
- let collidingSegIndex = -1
74
+ // STRATEGY 1: Single Label - reroute around the contiguous span of segments
75
+ // that actually overlap the label. Detection uses the *unpadded* bounds so
76
+ // a short pin stub that merely pokes into the padding zone is not mistaken
77
+ // for the real crossing (which would leave the offending segment in place
78
+ // and create a useless loop).
79
+ let firstCollidingSeg = -1
80
+ let lastCollidingSeg = -1
76
81
  for (let i = 0; i < initialTrace.tracePath.length - 1; i++) {
77
82
  if (
78
83
  segmentIntersectsRect(
79
84
  initialTrace.tracePath[i],
80
85
  initialTrace.tracePath[i + 1],
81
- { ...paddedLabelBounds, chipId: "temp-obstacle" },
86
+ { ...labelBounds, chipId: "temp-obstacle" },
82
87
  )
83
88
  ) {
84
- collidingSegIndex = i
85
- break
89
+ if (firstCollidingSeg === -1) firstCollidingSeg = i
90
+ lastCollidingSeg = i
86
91
  }
87
92
  }
88
93
 
89
- if (collidingSegIndex === -1) return []
94
+ if (firstCollidingSeg === -1) return []
90
95
 
91
- entryIndex = collidingSegIndex
92
- exitIndex = collidingSegIndex + 1
96
+ entryIndex = firstCollidingSeg
97
+ exitIndex = lastCollidingSeg + 1
93
98
  entryPoint = initialTrace.tracePath[entryIndex]
94
99
  exitPoint = initialTrace.tracePath[exitIndex]
95
100
  }
@@ -1,25 +1,59 @@
1
- import type { Point } from "@tscircuit/math-utils"
1
+ import type { Point, Bounds } from "@tscircuit/math-utils"
2
+ import { segmentIntersectsRect } from "../NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
2
3
 
3
- export const findTraceViolationZone = (
4
- path: Point[],
5
- labelBounds: { minX: number; maxX: number; minY: number; maxY: number },
6
- ) => {
7
- const isPointInside = (p: Point) =>
8
- p.x > labelBounds.minX &&
9
- p.x < labelBounds.maxX &&
10
- p.y > labelBounds.minY &&
11
- p.y < labelBounds.maxY
4
+ /**
5
+ * Locates the portion of a trace that must be rerouted to clear a label.
6
+ *
7
+ * Detection is segment-based against the (unpadded) label bounds so it also
8
+ * catches traces that cross the label edge-to-edge, or run *along* one of its
9
+ * edges, without ever placing a vertex strictly inside it (the previous
10
+ * vertex-only test missed both cases).
11
+ *
12
+ * The returned range describes the vertices to remove: the reroute reconnects
13
+ * `path[firstInsideIndex - 1]` to `path[lastInsideIndex + 1]`. Those two anchor
14
+ * vertices are pushed outward until they sit outside the padded label so the
15
+ * detour has room to pivot without immediately re-entering it.
16
+ */
17
+ export const findTraceViolationZone = ({
18
+ path,
19
+ labelBounds,
20
+ paddedBounds = labelBounds,
21
+ }: {
22
+ path: Point[]
23
+ labelBounds: Bounds
24
+ paddedBounds?: Bounds
25
+ }) => {
26
+ let firstCollidingSeg = -1
27
+ let lastCollidingSeg = -1
28
+ for (let i = 0; i < path.length - 1; i++) {
29
+ if (segmentIntersectsRect(path[i]!, path[i + 1]!, labelBounds)) {
30
+ if (firstCollidingSeg === -1) firstCollidingSeg = i
31
+ lastCollidingSeg = i
32
+ }
33
+ }
12
34
 
13
- let firstInsideIndex = -1
14
- let lastInsideIndex = -1
35
+ if (firstCollidingSeg === -1) {
36
+ return { firstInsideIndex: -1, lastInsideIndex: -1 }
37
+ }
15
38
 
16
- for (let i = 0; i < path.length; i++) {
17
- if (isPointInside(path[i])) {
18
- if (firstInsideIndex === -1) {
19
- firstInsideIndex = i
20
- }
21
- lastInsideIndex = i
22
- }
39
+ const isInsidePadded = (p: Point) =>
40
+ p.x > paddedBounds.minX &&
41
+ p.x < paddedBounds.maxX &&
42
+ p.y > paddedBounds.minY &&
43
+ p.y < paddedBounds.maxY
44
+
45
+ // Anchor before the crossing: walk back to the first vertex clear of the
46
+ // padded label.
47
+ let entryIndex = firstCollidingSeg
48
+ while (entryIndex > 0 && isInsidePadded(path[entryIndex]!)) {
49
+ entryIndex--
50
+ }
51
+
52
+ // Anchor after the crossing.
53
+ let exitIndex = lastCollidingSeg + 1
54
+ while (exitIndex < path.length - 1 && isInsidePadded(path[exitIndex]!)) {
55
+ exitIndex++
23
56
  }
24
- return { firstInsideIndex, lastInsideIndex }
57
+
58
+ return { firstInsideIndex: entryIndex + 1, lastInsideIndex: exitIndex - 1 }
25
59
  }
@@ -180,21 +180,56 @@ export class TraceOverlapIssueSolver extends BaseSolver {
180
180
  const blockingCollisions = this.getBlockingCollisions({ group, offset })
181
181
  if (blockingCollisions.length === 0) return offset
182
182
 
183
- return (
184
- blockingCollisions
185
- .flatMap(({ start, isVertical, obstacle }) => [
186
- (isVertical ? obstacle.minX - start.x : obstacle.minY - start.y) -
187
- this.SHIFT_DISTANCE,
188
- (isVertical ? obstacle.maxX - start.x : obstacle.maxY - start.y) +
189
- this.SHIFT_DISTANCE,
190
- ])
183
+ const crossesThroughObstacle = (candidateOffset: number) =>
184
+ blockingCollisions.some(({ start, isVertical, obstacle }) => {
185
+ let orig: number
186
+ let min: number
187
+ let max: number
188
+ if (isVertical) {
189
+ orig = start.x
190
+ min = obstacle.minX
191
+ max = obstacle.maxX
192
+ } else {
193
+ orig = start.y
194
+ min = obstacle.minY
195
+ max = obstacle.maxY
196
+ }
197
+ const next = orig + candidateOffset
198
+ return (
199
+ (orig <= min + EPS && next >= max - EPS) ||
200
+ (orig >= max - EPS && next <= min + EPS)
201
+ )
202
+ })
203
+
204
+ const edges = blockingCollisions.map(({ start, isVertical, obstacle }) => {
205
+ if (isVertical) {
206
+ return {
207
+ coord: start.x,
208
+ lowEdge: obstacle.minX,
209
+ highEdge: obstacle.maxX,
210
+ }
211
+ }
212
+ return { coord: start.y, lowEdge: obstacle.minY, highEdge: obstacle.maxY }
213
+ })
214
+
215
+ const bestCandidate = (clearance: number) => {
216
+ const candidates = edges.flatMap(({ coord, lowEdge, highEdge }) => [
217
+ lowEdge - coord - clearance,
218
+ highEdge - coord + clearance,
219
+ ])
220
+ return candidates
221
+ .filter((candidateOffset) => !crossesThroughObstacle(candidateOffset))
191
222
  .filter(
192
223
  (candidateOffset) =>
193
224
  this.getBlockingCollisions({ group, offset: candidateOffset })
194
225
  .length === 0,
195
226
  )
196
- .sort((a, b) => Math.abs(a - offset) - Math.abs(b - offset))[0] ??
197
- offset
227
+ .sort((a, b) => Math.abs(a - offset) - Math.abs(b - offset))[0]
228
+ }
229
+
230
+ const EDGE_MARGIN = this.SHIFT_DISTANCE / 10
231
+ return (
232
+ bestCandidate(this.SHIFT_DISTANCE) ?? bestCandidate(EDGE_MARGIN) ?? offset
198
233
  )
199
234
  }
200
235
 
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.68",
4
+ "version": "0.0.70",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",
@@ -0,0 +1,4 @@
1
+ import { PipelineDebugger } from "site/components/PipelineDebugger"
2
+ import inputProblem from "../../tests/assets/example42.json"
3
+
4
+ export default () => <PipelineDebugger inputProblem={inputProblem as any} />
@@ -0,0 +1,139 @@
1
+ {
2
+ "chips": [
3
+ {
4
+ "chipId": "schematic_component_16",
5
+ "center": {
6
+ "x": 0,
7
+ "y": 0
8
+ },
9
+ "width": 4.4,
10
+ "height": 2,
11
+ "pins": [
12
+ {
13
+ "pinId": "U3.1",
14
+ "x": 2.6,
15
+ "y": 0.8499999999999999
16
+ },
17
+ {
18
+ "pinId": "U3.2",
19
+ "x": 2.6,
20
+ "y": 0.6499999999999999
21
+ },
22
+ {
23
+ "pinId": "U3.3",
24
+ "x": 2.6,
25
+ "y": 0.44999999999999996
26
+ },
27
+ {
28
+ "pinId": "U3.4",
29
+ "x": 2.6,
30
+ "y": 0.10000000000000009
31
+ },
32
+ {
33
+ "pinId": "U3.5",
34
+ "x": 2.6,
35
+ "y": -0.09999999999999987
36
+ },
37
+ {
38
+ "pinId": "U3.6",
39
+ "x": 2.6,
40
+ "y": -0.44999999999999984
41
+ },
42
+ {
43
+ "pinId": "U3.7",
44
+ "x": 2.6,
45
+ "y": -0.6499999999999999
46
+ },
47
+ {
48
+ "pinId": "U3.8",
49
+ "x": 2.6,
50
+ "y": -0.8499999999999999
51
+ }
52
+ ]
53
+ },
54
+ {
55
+ "chipId": "schematic_component_17",
56
+ "center": {
57
+ "x": 3.2,
58
+ "y": -1.4
59
+ },
60
+ "width": 1.1,
61
+ "height": 0.7789106999999986,
62
+ "pins": [
63
+ {
64
+ "pinId": "R8.1",
65
+ "x": 2.6500000000000004,
66
+ "y": -1.4
67
+ },
68
+ {
69
+ "pinId": "R8.2",
70
+ "x": 3.75,
71
+ "y": -1.4
72
+ }
73
+ ]
74
+ },
75
+ {
76
+ "chipId": "schematic_component_18",
77
+ "center": {
78
+ "x": 3.2,
79
+ "y": -2.4
80
+ },
81
+ "width": 1.1,
82
+ "height": 0.7789106999999986,
83
+ "pins": [
84
+ {
85
+ "pinId": "R9.1",
86
+ "x": 2.6500000000000004,
87
+ "y": -2.4
88
+ },
89
+ {
90
+ "pinId": "R9.2",
91
+ "x": 3.75,
92
+ "y": -2.4
93
+ }
94
+ ]
95
+ }
96
+ ],
97
+ "directConnections": [],
98
+ "netConnections": [
99
+ {
100
+ "netId": "GND",
101
+ "pinIds": ["U3.8", "R8.2", "R9.2"],
102
+ "netLabelWidth": 0.48
103
+ },
104
+ {
105
+ "netId": "D_MINUS",
106
+ "pinIds": ["U3.3"],
107
+ "netLabelWidth": 0.96
108
+ },
109
+ {
110
+ "netId": "D_PLUS",
111
+ "pinIds": ["U3.2"],
112
+ "netLabelWidth": 0.84
113
+ },
114
+ {
115
+ "netId": "VBUS",
116
+ "pinIds": ["U3.1"],
117
+ "netLabelWidth": 0.6
118
+ },
119
+ {
120
+ "netId": "CC1",
121
+ "pinIds": ["U3.4", "R8.1"],
122
+ "netLabelWidth": 0.48
123
+ },
124
+ {
125
+ "netId": "CC2",
126
+ "pinIds": ["U3.5", "R9.1"],
127
+ "netLabelWidth": 0.48
128
+ }
129
+ ],
130
+ "availableNetLabelOrientations": {
131
+ "D_MINUS": ["x-", "x+"],
132
+ "D_PLUS": ["x-", "x+"],
133
+ "GND": ["y-"],
134
+ "VBUS": ["y+"],
135
+ "CC1": ["x-", "x+"],
136
+ "CC2": ["x-", "x+"]
137
+ },
138
+ "maxMspPairDistance": 2.4
139
+ }