@quenty/draw 4.3.0 → 4.3.1-canary.436.cb76259.0

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/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [4.3.1-canary.436.cb76259.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/draw@4.3.0...@quenty/draw@4.3.1-canary.436.cb76259.0) (2024-01-08)
7
+
8
+
9
+ ### Features
10
+
11
+ * Update draw package ([2b5aeab](https://github.com/Quenty/NevermoreEngine/commit/2b5aeab42ae8861aabacf2f796fba43d1e2e8717))
12
+
13
+
14
+
15
+
16
+
6
17
  # [4.3.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/draw@4.2.0...@quenty/draw@4.3.0) (2023-08-23)
7
18
 
8
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/draw",
3
- "version": "4.3.0",
3
+ "version": "4.3.1-canary.436.cb76259.0",
4
4
  "description": "A utility library to debug things in 3D space for Roblox.",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -28,5 +28,9 @@
28
28
  "publishConfig": {
29
29
  "access": "public"
30
30
  },
31
- "gitHead": "2547e660a0333034a5b20ddd6b23e343bc01f7c6"
31
+ "dependencies": {
32
+ "@quenty/loader": "7.1.1-canary.436.cb76259.0",
33
+ "@quenty/maid": "2.6.0"
34
+ },
35
+ "gitHead": "cb7625914c075100546dbd26c7d04eda3c9bd3e4"
32
36
  }
@@ -62,16 +62,15 @@ end
62
62
  @param finish Vector3
63
63
  @param color Color3 -- Optional
64
64
  @param parent Instance? -- Optional
65
- @param meshDiameter number -- Optional
66
65
  @param diameter number -- Optional
67
66
  @return Instance
68
67
  ]=]
69
- function Draw.line(start, finish, color, parent, meshDiameter, diameter)
68
+ function Draw.line(start, finish, color, parent, diameter)
70
69
  start = assert(Draw._toVector3(start), "Bad start")
71
70
  finish = assert(Draw._toVector3(finish), "Bad finish")
72
71
  color = Draw._toColor3(color)
73
72
 
74
- return Draw.ray(Ray.new(start, finish - start), color, parent, meshDiameter, diameter)
73
+ return Draw.ray(Ray.new(start, finish - start), color, parent, diameter)
75
74
  end
76
75
 
77
76
  --[=[
@@ -96,18 +95,38 @@ end
96
95
  --[=[
97
96
  Draws a spherecast
98
97
 
98
+ :::tip
99
+ Unlike WorldRoot:GetPartsInPart(), spherecast does not detect BaseParts
100
+ that initially intersect the shape. So this draw doesn't render that initial sphere.
101
+ :::
102
+
99
103
  @param origin Vector3
100
104
  @param radius number
101
105
  @param direction Vector3
102
106
  @param color Color3
103
107
  @param parent Parent
104
108
  ]=]
105
- function Draw.sphereCast(origin, radius, direction, color, parent)
106
- return Draw.ray(Ray.new(origin, direction), color, parent, 2*radius)
109
+ function Draw.spherecast(origin, radius, direction, color, parent)
110
+ origin = assert(Draw._toVector3(origin), "Bad cframe")
111
+ assert(type(radius) == "number", "Bad radius")
112
+ direction = assert(Draw._toVector3(direction), "Bad direction")
113
+ color = Draw._toColor3(color)
114
+ parent = parent or Draw.getDefaultParent()
115
+
116
+ local folder = Instance.new("Folder")
117
+ folder.Name = "SphereCast"
118
+ folder.Archivable = false
119
+
120
+ Draw.ray(Ray.new(origin, direction), color, folder, 2*radius)
121
+ Draw.sphere(origin + direction, radius, color, folder)
122
+
123
+ folder.Parent = parent
124
+
125
+ return folder
107
126
  end
108
127
 
109
128
  --[=[
110
- Draws a spherecast
129
+ Draws a block cast
111
130
 
112
131
  @param cframe CFrame
113
132
  @param size Vector3
@@ -118,6 +137,7 @@ end
118
137
  function Draw.blockcast(cframe, size, direction, color, parent)
119
138
  cframe = assert(Draw._toCFrame(cframe), "Bad cframe")
120
139
  size = assert(Draw._toVector3(size), "Bad size")
140
+ direction = assert(Draw._toVector3(direction), "Bad direction")
121
141
  color = Draw._toColor3(color)
122
142
  parent = parent or Draw.getDefaultParent()
123
143
 
@@ -135,6 +155,99 @@ function Draw.blockcast(cframe, size, direction, color, parent)
135
155
  return folder
136
156
  end
137
157
 
158
+ function Draw.triangle(a, b, c, color, parent)
159
+ a = assert(Draw._toVector3(a), "Bad a")
160
+ b = assert(Draw._toVector3(b), "Bad b")
161
+ c = assert(Draw._toVector3(c), "Bad c")
162
+ color = Draw._toColor3(color) or Draw._defaultColor
163
+ parent = parent or Draw.getDefaultParent()
164
+
165
+ local edges = {
166
+ {longest = (c - a), other = (b - a), origin = a},
167
+ {longest = (a - b), other = (c - b), origin = b},
168
+ {longest = (b - c), other = (a - c), origin = c}
169
+ };
170
+
171
+ local edge = edges[1]
172
+ for i = 2, #edges do
173
+ if edges[i].longest.magnitude > edge.longest.magnitude then
174
+ edge = edges[i]
175
+ end
176
+ end
177
+
178
+ local theta = math.acos(edge.longest.unit:Dot(edge.other.unit))
179
+ local w1 = math.cos(theta) * edge.other.magnitude
180
+ local w2 = edge.longest.magnitude - w1
181
+ local h = math.sin(theta) * edge.other.magnitude
182
+
183
+ local p1 = edge.origin + edge.other * 0.5;
184
+ local p2 = edge.origin + edge.longest + (edge.other - edge.longest) * 0.5
185
+
186
+ local right = edge.longest:Cross(edge.other).unit
187
+ local up = right:Cross(edge.longest).unit
188
+ local back = edge.longest.unit
189
+
190
+ local cf1 = CFrame.new(
191
+ p1.x, p1.y, p1.z,
192
+ -right.x, up.x, back.x,
193
+ -right.y, up.y, back.y,
194
+ -right.z, up.z, back.z
195
+ );
196
+
197
+ local cf2 = CFrame.new(
198
+ p2.x, p2.y, p2.z,
199
+ right.x, up.x, -back.x,
200
+ right.y, up.y, -back.y,
201
+ right.z, up.z, -back.z
202
+ );
203
+
204
+ -- put it all together by creating the wedges
205
+ local triangle = Instance.new("Folder")
206
+ triangle.Name = "Triangle"
207
+ triangle.Archivable = false
208
+
209
+ local wedge1 = Instance.new("WedgePart")
210
+ wedge1.Material = Enum.Material.SmoothPlastic
211
+ wedge1.Transparency = 0
212
+ wedge1.Anchored = true
213
+ wedge1.CanCollide = false
214
+ wedge1.CanQuery = false
215
+ wedge1.CanTouch = false
216
+ wedge1.Archivable = false
217
+ wedge1.CastShadow = false
218
+ wedge1.Size = Vector3.new(0.05, h, w1)
219
+ wedge1.CFrame = cf1
220
+ wedge1.Color = color
221
+
222
+ local mesh1 = Instance.new("SpecialMesh")
223
+ mesh1.MeshType = Enum.MeshType.Wedge
224
+ mesh1.Scale = Vector3.new(0, 1, 1)
225
+ mesh1.Parent = wedge1
226
+
227
+ local wedge2 = Instance.new("WedgePart")
228
+ wedge2.Material = Enum.Material.SmoothPlastic
229
+ wedge2.Transparency = 0
230
+ wedge2.Anchored = true
231
+ wedge2.CanCollide = false
232
+ wedge2.CanQuery = false
233
+ wedge2.CanTouch = false
234
+ wedge2.Archivable = false
235
+ wedge2.CastShadow = false
236
+ wedge2.Size = Vector3.new(0.05, h, w2)
237
+ wedge2.CFrame = cf2
238
+ wedge2.Color = color
239
+
240
+ local mesh2 = Instance.new("SpecialMesh")
241
+ mesh2.MeshType = Enum.MeshType.Wedge
242
+ mesh2.Scale = Vector3.new(0, 1, 1)
243
+ mesh2.Parent = wedge2
244
+
245
+ wedge1.Parent = triangle
246
+ wedge2.Parent = triangle
247
+
248
+ return triangle
249
+ end
250
+
138
251
  --[=[
139
252
  Draws a raycast for debugging
140
253
 
@@ -166,18 +279,17 @@ end
166
279
  @param color Color3? -- Optional color to draw in
167
280
  @param parent Instance? -- Optional parent
168
281
  @param diameter number? -- Optional diameter
169
- @param meshDiameter number? -- Optional mesh diameter
170
282
  @return BasePart
171
283
  ]=]
172
- function Draw.ray(ray, color, parent, meshDiameter, diameter)
284
+ function Draw.ray(ray, color, parent, diameter)
173
285
  assert(typeof(ray) == "Ray", "Bad typeof(ray) for Ray")
174
286
 
175
287
  color = Draw._toColor3(color) or Draw._defaultColor
176
288
  parent = parent or Draw.getDefaultParent()
177
- meshDiameter = meshDiameter or 0.2
178
289
  diameter = diameter or 0.2
179
290
 
180
291
  local rayCenter = ray.Origin + ray.Direction/2
292
+ local distance = ray.Direction.Magnitude
181
293
 
182
294
  local part = Instance.new("Part")
183
295
  part.Material = Enum.Material.ForceField
@@ -187,41 +299,33 @@ function Draw.ray(ray, color, parent, meshDiameter, diameter)
187
299
  part.CanQuery = false
188
300
  part.CanTouch = false
189
301
  part.CastShadow = false
190
- part.CFrame = CFrame.new(rayCenter, ray.Origin + ray.Direction) * CFrame.Angles(math.pi/2, 0, 0)
302
+ part.CFrame = CFrame.new(rayCenter, ray.Origin + ray.Direction) * CFrame.Angles(0, math.pi/2, 0)
191
303
  part.Color = color
192
304
  part.Name = "DebugRay"
193
305
  part.Shape = Enum.PartType.Cylinder
194
- part.Size = Vector3.new(diameter, ray.Direction.Magnitude, diameter)
306
+ part.Size = Vector3.new(distance, diameter, diameter)
195
307
  part.TopSurface = Enum.SurfaceType.Smooth
196
308
  part.Transparency = 0.5
197
309
 
198
- local rotatedPart = Instance.new("Part")
199
- rotatedPart.Name = "RotatedPart"
200
- rotatedPart.Anchored = true
201
- rotatedPart.Archivable = false
202
- rotatedPart.CanCollide = false
203
- rotatedPart.CanQuery = false
204
- rotatedPart.CanTouch = false
205
- rotatedPart.CastShadow = false
206
- rotatedPart.CFrame = CFrame.new(ray.Origin, ray.Origin + ray.Direction)
207
- rotatedPart.Transparency = 1
208
- rotatedPart.Size = Vector3.new(1, 1, 1)
209
- rotatedPart.Parent = part
210
-
211
- local lineHandleAdornment = Instance.new("LineHandleAdornment")
212
- lineHandleAdornment.Name = "DrawRayLineHandleAdornment"
213
- lineHandleAdornment.Length = ray.Direction.Magnitude
214
- lineHandleAdornment.Thickness = 5*diameter
215
- lineHandleAdornment.ZIndex = 3
216
- lineHandleAdornment.Color3 = color
217
- lineHandleAdornment.AlwaysOnTop = true
218
- lineHandleAdornment.Transparency = 0
219
- lineHandleAdornment.Adornee = rotatedPart
220
- lineHandleAdornment.Parent = rotatedPart
310
+ local cylinderHandleAdornment = Instance.new("CylinderHandleAdornment")
311
+ cylinderHandleAdornment.Name = "CylinderHandleAdornment"
312
+ cylinderHandleAdornment.Height = ray.Direction.Magnitude
313
+ cylinderHandleAdornment.InnerRadius = 0
314
+ cylinderHandleAdornment.Radius = diameter/4
315
+ cylinderHandleAdornment.ZIndex = 3
316
+ cylinderHandleAdornment.Color3 = color
317
+ cylinderHandleAdornment.AlwaysOnTop = true
318
+ cylinderHandleAdornment.Transparency = 0.25
319
+ cylinderHandleAdornment.CFrame = CFrame.Angles(0, math.pi/2, 0)
320
+ cylinderHandleAdornment.Adornee = part
321
+ cylinderHandleAdornment.Parent = part
322
+
323
+ local partSize = part.Size
221
324
 
222
325
  local mesh = Instance.new("SpecialMesh")
326
+ mesh.MeshType = Enum.MeshType.Cylinder
223
327
  mesh.Name = "DrawRayMesh"
224
- mesh.Scale = Vector3.new(0, 1, 0) + Vector3.new(meshDiameter, 0, meshDiameter) / diameter
328
+ mesh.Scale = Vector3.new(distance/partSize.x, diameter/partSize.y, diameter/partSize.z)
225
329
  mesh.Parent = part
226
330
 
227
331
  part.Parent = parent
@@ -244,31 +348,36 @@ end
244
348
  end)
245
349
  ```
246
350
 
247
- @param part Ray part
248
- @param ray Ray
249
- @param color Color3
351
+ @param rayPart Instance -- Ray part
352
+ @param ray Ray -- New ray
353
+ @param color Color3 -- New color
354
+ @param diameter number -- Number
250
355
  ]=]
251
- function Draw.updateRay(part, ray, color)
252
- color = Draw._toColor3(color) or part.Color
356
+ function Draw.updateRay(rayPart, ray, color, diameter)
357
+ assert(typeof(rayPart) == "Instance", "Bad rayPart")
358
+ assert(typeof(ray) == "Ray", "Bad typeof(ray) for Ray")
359
+ color = Draw._toColor3(color) or rayPart.Color
360
+ diameter = diameter or rayPart.Size.x
253
361
 
254
- local diameter = part.Size.x
255
362
  local rayCenter = ray.Origin + ray.Direction/2
363
+ local distance = ray.Direction.Magnitude
256
364
 
257
- part.CFrame = CFrame.new(rayCenter, ray.Origin + ray.Direction) * CFrame.Angles(math.pi/2, 0, 0)
258
- part.Size = Vector3.new(diameter, ray.Direction.Magnitude, diameter)
259
- part.Color = color
260
-
261
- local rotatedPart = part:FindFirstChild("RotatedPart")
262
- if rotatedPart then
263
- rotatedPart.CFrame = CFrame.new(ray.Origin, ray.Origin + ray.Direction)
264
- end
365
+ rayPart.Color = color
366
+ rayPart.Size = Vector3.new(distance, diameter, diameter)
367
+ rayPart.CFrame = CFrame.new(rayCenter, ray.Origin + ray.Direction) * CFrame.Angles(0, math.pi/2, 0)
265
368
 
266
- local lineHandleAdornment = rotatedPart and rotatedPart:FindFirstChild("DrawRayLineHandleAdornment")
369
+ local lineHandleAdornment = rayPart:FindFirstChildWhichIsA("CylinderHandleAdornment")
267
370
  if lineHandleAdornment then
268
- lineHandleAdornment.Length = ray.Direction.Magnitude
269
- lineHandleAdornment.Thickness = 5*diameter
371
+ lineHandleAdornment.Height = ray.Direction.Magnitude
372
+ lineHandleAdornment.Radius = 5*diameter
270
373
  lineHandleAdornment.Color3 = color
271
374
  end
375
+
376
+ local partSize = rayPart.Size
377
+ local mesh = rayPart:FindFirstChildWhichIsA("SpecialMesh")
378
+ if mesh then
379
+ mesh.Scale = Vector3.new(distance/partSize.x, diameter/partSize.y, diameter/partSize.z)
380
+ end
272
381
  end
273
382
 
274
383
  --[=[
@@ -435,6 +544,7 @@ function Draw.point(position, color, parent, diameter)
435
544
 
436
545
  local sphereHandle = Instance.new("SphereHandleAdornment")
437
546
  sphereHandle.Archivable = false
547
+ sphereHandle.Transparency = 0.25
438
548
  sphereHandle.Radius = diameter/4
439
549
  sphereHandle.Color3 = color
440
550
  sphereHandle.AlwaysOnTop = true
@@ -810,6 +920,10 @@ function Draw._toVector3(position)
810
920
  else
811
921
  return nil
812
922
  end
923
+ elseif typeof(position) == "RaycastResult" then
924
+ return position.Position
925
+ elseif typeof(position) == "PathWaypoint" then
926
+ return position.Position
813
927
  else
814
928
  return nil
815
929
  end
@@ -846,6 +960,10 @@ function Draw._toCFrame(cframe)
846
960
  else
847
961
  return nil
848
962
  end
963
+ elseif typeof(cframe) == "RaycastResult" then
964
+ return CFrame.new(cframe.Position, cframe.Normal)
965
+ elseif typeof(cframe) == "PathWaypoint" then
966
+ return CFrame.new(cframe.Position)
849
967
  else
850
968
  return nil
851
969
  end
@@ -0,0 +1,53 @@
1
+ --[[
2
+ @class Draw.story
3
+ ]]
4
+
5
+ local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
6
+
7
+ local UserInputService = game:GetService("UserInputService")
8
+ local Workspace = game:GetService("Workspace")
9
+
10
+ local Draw = require("Draw")
11
+ local Maid = require("Maid")
12
+
13
+ return function(_target)
14
+ local topMaid = Maid.new()
15
+
16
+ topMaid:GiveTask(UserInputService.InputBegan:Connect(function(inputObject, gameProcessed)
17
+ if gameProcessed then
18
+ return
19
+ end
20
+
21
+ if inputObject.UserInputType == Enum.UserInputType.MouseButton1 then
22
+ if topMaid._current then
23
+ if not UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) then
24
+ return
25
+ end
26
+ end
27
+
28
+ local maid = Maid.new()
29
+ local camera = Workspace.CurrentCamera
30
+ local position = inputObject.Position
31
+
32
+ local baseRay = camera:ViewportPointToRay(position.x, position.y, 0)
33
+ local ray = Ray.new(baseRay.Origin, baseRay.Direction.unit * 50)
34
+ local cframe = CFrame.new(ray.Origin, ray.Origin + ray.Direction.unit)
35
+ * CFrame.Angles(0, math.pi/4, math.pi/4)
36
+ local size = Vector3.new(4, 4, 4)
37
+ local direction = ray.Direction
38
+
39
+ maid:Add(Draw.blockcast(cframe, size, direction))
40
+
41
+ local raycastResult = Workspace:Blockcast(cframe, size, direction)
42
+ if raycastResult then
43
+ maid:Add(Draw.point(raycastResult.Position, Color3.new(0.25, 1, 0.25), nil, 0.1))
44
+ end
45
+
46
+ topMaid._current = maid
47
+ end
48
+ end))
49
+
50
+ return function()
51
+ topMaid:DoCleaning()
52
+ end
53
+ end
@@ -0,0 +1,65 @@
1
+ --[[
2
+ @class Draw.story
3
+ ]]
4
+
5
+ local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
6
+
7
+ local UserInputService = game:GetService("UserInputService")
8
+ local Workspace = game:GetService("Workspace")
9
+
10
+ local Draw = require("Draw")
11
+ local Maid = require("Maid")
12
+
13
+ return function(_target)
14
+ local topMaid = Maid.new()
15
+
16
+ local function render(baseRay)
17
+ local maid = Maid.new()
18
+
19
+ local ray = Ray.new(baseRay.Origin, baseRay.Direction.unit * 10000)
20
+
21
+ maid:Add(Draw.ray(ray))
22
+
23
+ local raycastResult = Workspace:Raycast(ray.Origin, ray.Direction)
24
+ if raycastResult then
25
+ maid:Add(Draw.point(raycastResult.Position, Color3.new(0.25, 1, 0.25), nil, 0.1))
26
+ end
27
+
28
+ topMaid._current = maid
29
+ end
30
+
31
+ topMaid:GiveTask(UserInputService.InputBegan:Connect(function(inputObject, gameProcessed)
32
+ if gameProcessed then
33
+ return
34
+ end
35
+
36
+ if inputObject.UserInputType == Enum.UserInputType.MouseButton1 then
37
+ if topMaid._current then
38
+ if not UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) then
39
+ return
40
+ end
41
+ end
42
+
43
+
44
+ local camera = Workspace.CurrentCamera
45
+ local position = inputObject.Position
46
+ local baseRay = camera:ViewportPointToRay(position.x, position.y, 0)
47
+
48
+ -- selene: allow(global_usage)
49
+ shared._lastDrawRayInput = baseRay
50
+
51
+ render(baseRay)
52
+ end
53
+ end))
54
+
55
+ task.spawn(function()
56
+ -- selene: allow(global_usage)
57
+ if shared._lastDrawRayInput then
58
+ render(shared._lastDrawRayInput)
59
+ end
60
+ end)
61
+
62
+ return function()
63
+ topMaid:DoCleaning()
64
+ end
65
+ end
@@ -0,0 +1,65 @@
1
+ --[[
2
+ @class Draw.story
3
+ ]]
4
+
5
+ local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
6
+
7
+ local UserInputService = game:GetService("UserInputService")
8
+ local Workspace = game:GetService("Workspace")
9
+
10
+ local Draw = require("Draw")
11
+ local Maid = require("Maid")
12
+
13
+ return function(_target)
14
+ local topMaid = Maid.new()
15
+
16
+ local function render(baseRay)
17
+ local maid = Maid.new()
18
+
19
+ local ray = Ray.new(baseRay.Origin, baseRay.Direction.unit * 250)
20
+
21
+ maid:Add(Draw.spherecast(ray.Origin, 5, ray.Direction))
22
+
23
+ local raycastResult = Workspace:Raycast(ray.Origin, ray.Direction)
24
+ if raycastResult then
25
+ maid:Add(Draw.point(raycastResult.Position, Color3.new(0.25, 1, 0.25), nil, 0.1))
26
+ end
27
+
28
+ topMaid._current = maid
29
+ end
30
+
31
+ topMaid:GiveTask(UserInputService.InputBegan:Connect(function(inputObject, gameProcessed)
32
+ if gameProcessed then
33
+ return
34
+ end
35
+
36
+ if inputObject.UserInputType == Enum.UserInputType.MouseButton1 then
37
+ if topMaid._current then
38
+ if not UserInputService:IsKeyDown(Enum.KeyCode.LeftControl) then
39
+ return
40
+ end
41
+ end
42
+
43
+
44
+ local camera = Workspace.CurrentCamera
45
+ local position = inputObject.Position
46
+ local baseRay = camera:ViewportPointToRay(position.x, position.y, 0)
47
+
48
+ -- selene: allow(global_usage)
49
+ shared._lastDrawRayInput = baseRay
50
+
51
+ render(baseRay)
52
+ end
53
+ end))
54
+
55
+ task.spawn(function()
56
+ -- selene: allow(global_usage)
57
+ if shared._lastDrawRayInput then
58
+ render(shared._lastDrawRayInput)
59
+ end
60
+ end)
61
+
62
+ return function()
63
+ topMaid:DoCleaning()
64
+ end
65
+ end
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "node_modules",
3
+ "globIgnorePaths": [ "**/.package-lock.json" ],
4
+ "tree": {
5
+ "$path": { "optional": "../node_modules" }
6
+ }
7
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "DrawTest",
3
+ "tree": {
4
+ "$className": "DataModel",
5
+ "ServerScriptService": {
6
+ "draw": {
7
+ "$path": ".."
8
+ }
9
+ }
10
+ }
11
+ }