rbxstudio-mcp 1.14.3 → 2.1.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/dist/index.js +102 -400
- package/dist/index.js.map +1 -1
- package/dist/tools/index.d.ts +37 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +93 -0
- package/dist/tools/index.js.map +1 -1
- package/package.json +1 -1
- package/studio-plugin/plugin.luau +512 -2
|
@@ -111,7 +111,7 @@ local screenGui = plugin:CreateDockWidgetPluginGuiAsync(
|
|
|
111
111
|
"MCPServerInterface",
|
|
112
112
|
DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, false, false, 400, 500, 350, 450)
|
|
113
113
|
)
|
|
114
|
-
screenGui.Title = "MCP Server
|
|
114
|
+
screenGui.Title = "MCP Server v2.0.0"
|
|
115
115
|
|
|
116
116
|
local mainFrame = Instance.new("Frame")
|
|
117
117
|
mainFrame.Size = UDim2.new(1, 0, 1, 0)
|
|
@@ -164,7 +164,7 @@ local versionLabel = Instance.new("TextLabel")
|
|
|
164
164
|
versionLabel.Size = UDim2.new(1, 0, 0, 16)
|
|
165
165
|
versionLabel.Position = UDim2.new(0, 0, 0, 32)
|
|
166
166
|
versionLabel.BackgroundTransparency = 1
|
|
167
|
-
versionLabel.Text = "AI Integration •
|
|
167
|
+
versionLabel.Text = "AI Integration • v2.0.0"
|
|
168
168
|
versionLabel.TextColor3 = Color3.fromRGB(191, 219, 254)
|
|
169
169
|
versionLabel.TextScaled = false
|
|
170
170
|
versionLabel.TextSize = 12
|
|
@@ -4015,6 +4015,223 @@ handlers.redo = function(requestData)
|
|
|
4015
4015
|
end
|
|
4016
4016
|
end
|
|
4017
4017
|
|
|
4018
|
+
-- ============================================
|
|
4019
|
+
-- EXECUTE LUA HANDLER (Run arbitrary Lua code)
|
|
4020
|
+
-- ============================================
|
|
4021
|
+
|
|
4022
|
+
handlers.executeLua = function(requestData)
|
|
4023
|
+
local code = requestData.code
|
|
4024
|
+
|
|
4025
|
+
if not code or type(code) ~= "string" then
|
|
4026
|
+
return {
|
|
4027
|
+
success = false,
|
|
4028
|
+
error = "code parameter is required and must be a string"
|
|
4029
|
+
}
|
|
4030
|
+
end
|
|
4031
|
+
|
|
4032
|
+
-- Create a sandboxed environment with access to game services and Roblox types
|
|
4033
|
+
local env = setmetatable({
|
|
4034
|
+
-- Core globals
|
|
4035
|
+
game = game,
|
|
4036
|
+
workspace = workspace,
|
|
4037
|
+
script = nil, -- No script context
|
|
4038
|
+
|
|
4039
|
+
-- Services (commonly used)
|
|
4040
|
+
Players = game:GetService("Players"),
|
|
4041
|
+
Workspace = workspace,
|
|
4042
|
+
ReplicatedStorage = game:GetService("ReplicatedStorage"),
|
|
4043
|
+
ServerStorage = game:GetService("ServerStorage"),
|
|
4044
|
+
ServerScriptService = game:GetService("ServerScriptService"),
|
|
4045
|
+
StarterGui = game:GetService("StarterGui"),
|
|
4046
|
+
StarterPack = game:GetService("StarterPack"),
|
|
4047
|
+
StarterPlayer = game:GetService("StarterPlayer"),
|
|
4048
|
+
Lighting = game:GetService("Lighting"),
|
|
4049
|
+
SoundService = game:GetService("SoundService"),
|
|
4050
|
+
TweenService = game:GetService("TweenService"),
|
|
4051
|
+
RunService = game:GetService("RunService"),
|
|
4052
|
+
UserInputService = game:GetService("UserInputService"),
|
|
4053
|
+
HttpService = game:GetService("HttpService"),
|
|
4054
|
+
CollectionService = game:GetService("CollectionService"),
|
|
4055
|
+
PhysicsService = game:GetService("PhysicsService"),
|
|
4056
|
+
PathfindingService = game:GetService("PathfindingService"),
|
|
4057
|
+
TextService = game:GetService("TextService"),
|
|
4058
|
+
MarketplaceService = game:GetService("MarketplaceService"),
|
|
4059
|
+
TeleportService = game:GetService("TeleportService"),
|
|
4060
|
+
GroupService = game:GetService("GroupService"),
|
|
4061
|
+
BadgeService = game:GetService("BadgeService"),
|
|
4062
|
+
DataStoreService = game:GetService("DataStoreService"),
|
|
4063
|
+
MemoryStoreService = game:GetService("MemoryStoreService"),
|
|
4064
|
+
MessagingService = game:GetService("MessagingService"),
|
|
4065
|
+
PolicyService = game:GetService("PolicyService"),
|
|
4066
|
+
LocalizationService = game:GetService("LocalizationService"),
|
|
4067
|
+
Selection = game:GetService("Selection"),
|
|
4068
|
+
ChangeHistoryService = game:GetService("ChangeHistoryService"),
|
|
4069
|
+
ScriptEditorService = game:GetService("ScriptEditorService"),
|
|
4070
|
+
|
|
4071
|
+
-- Roblox constructors
|
|
4072
|
+
Instance = Instance,
|
|
4073
|
+
Vector3 = Vector3,
|
|
4074
|
+
Vector2 = Vector2,
|
|
4075
|
+
CFrame = CFrame,
|
|
4076
|
+
Color3 = Color3,
|
|
4077
|
+
BrickColor = BrickColor,
|
|
4078
|
+
UDim = UDim,
|
|
4079
|
+
UDim2 = UDim2,
|
|
4080
|
+
Rect = Rect,
|
|
4081
|
+
Ray = Ray,
|
|
4082
|
+
Region3 = Region3,
|
|
4083
|
+
NumberSequence = NumberSequence,
|
|
4084
|
+
NumberSequenceKeypoint = NumberSequenceKeypoint,
|
|
4085
|
+
ColorSequence = ColorSequence,
|
|
4086
|
+
ColorSequenceKeypoint = ColorSequenceKeypoint,
|
|
4087
|
+
NumberRange = NumberRange,
|
|
4088
|
+
TweenInfo = TweenInfo,
|
|
4089
|
+
Font = Font,
|
|
4090
|
+
|
|
4091
|
+
-- Enums
|
|
4092
|
+
Enum = Enum,
|
|
4093
|
+
|
|
4094
|
+
-- Lua globals
|
|
4095
|
+
print = print,
|
|
4096
|
+
warn = warn,
|
|
4097
|
+
error = error,
|
|
4098
|
+
assert = assert,
|
|
4099
|
+
type = type,
|
|
4100
|
+
typeof = typeof,
|
|
4101
|
+
tostring = tostring,
|
|
4102
|
+
tonumber = tonumber,
|
|
4103
|
+
pairs = pairs,
|
|
4104
|
+
ipairs = ipairs,
|
|
4105
|
+
next = next,
|
|
4106
|
+
select = select,
|
|
4107
|
+
unpack = unpack,
|
|
4108
|
+
pcall = pcall,
|
|
4109
|
+
xpcall = xpcall,
|
|
4110
|
+
rawget = rawget,
|
|
4111
|
+
rawset = rawset,
|
|
4112
|
+
rawequal = rawequal,
|
|
4113
|
+
setmetatable = setmetatable,
|
|
4114
|
+
getmetatable = getmetatable,
|
|
4115
|
+
newproxy = newproxy,
|
|
4116
|
+
|
|
4117
|
+
-- Tables
|
|
4118
|
+
table = table,
|
|
4119
|
+
string = string,
|
|
4120
|
+
math = math,
|
|
4121
|
+
os = { time = os.time, date = os.date, difftime = os.difftime, clock = os.clock },
|
|
4122
|
+
coroutine = coroutine,
|
|
4123
|
+
bit32 = bit32,
|
|
4124
|
+
utf8 = utf8,
|
|
4125
|
+
buffer = buffer,
|
|
4126
|
+
|
|
4127
|
+
-- Task library
|
|
4128
|
+
task = task,
|
|
4129
|
+
wait = task.wait,
|
|
4130
|
+
delay = task.delay,
|
|
4131
|
+
spawn = task.spawn,
|
|
4132
|
+
defer = task.defer,
|
|
4133
|
+
|
|
4134
|
+
-- Instance helper from plugin
|
|
4135
|
+
getInstanceByPath = getInstanceByPath,
|
|
4136
|
+
|
|
4137
|
+
-- Allow loadstring for dynamic code (we're already in a plugin context)
|
|
4138
|
+
loadstring = loadstring,
|
|
4139
|
+
}, { __index = _G })
|
|
4140
|
+
|
|
4141
|
+
local success, compileResult = pcall(function()
|
|
4142
|
+
return loadstring(code)
|
|
4143
|
+
end)
|
|
4144
|
+
|
|
4145
|
+
if not success then
|
|
4146
|
+
return {
|
|
4147
|
+
success = false,
|
|
4148
|
+
error = "Failed to compile code: " .. tostring(compileResult)
|
|
4149
|
+
}
|
|
4150
|
+
end
|
|
4151
|
+
|
|
4152
|
+
local fn = compileResult
|
|
4153
|
+
if not fn then
|
|
4154
|
+
return {
|
|
4155
|
+
success = false,
|
|
4156
|
+
error = "Failed to compile code (loadstring returned nil)"
|
|
4157
|
+
}
|
|
4158
|
+
end
|
|
4159
|
+
|
|
4160
|
+
-- Set the environment
|
|
4161
|
+
setfenv(fn, env)
|
|
4162
|
+
|
|
4163
|
+
-- Execute the code
|
|
4164
|
+
local execSuccess, execResult = pcall(fn)
|
|
4165
|
+
|
|
4166
|
+
if not execSuccess then
|
|
4167
|
+
return {
|
|
4168
|
+
success = false,
|
|
4169
|
+
error = "Runtime error: " .. tostring(execResult)
|
|
4170
|
+
}
|
|
4171
|
+
end
|
|
4172
|
+
|
|
4173
|
+
-- Serialize the result
|
|
4174
|
+
local function serializeResult(value, depth)
|
|
4175
|
+
depth = depth or 0
|
|
4176
|
+
if depth > 5 then return "<max depth>" end
|
|
4177
|
+
|
|
4178
|
+
local t = typeof(value)
|
|
4179
|
+
|
|
4180
|
+
if value == nil then
|
|
4181
|
+
return nil
|
|
4182
|
+
elseif t == "string" then
|
|
4183
|
+
return value
|
|
4184
|
+
elseif t == "number" or t == "boolean" then
|
|
4185
|
+
return value
|
|
4186
|
+
elseif t == "Vector3" then
|
|
4187
|
+
return { type = "Vector3", X = value.X, Y = value.Y, Z = value.Z }
|
|
4188
|
+
elseif t == "Vector2" then
|
|
4189
|
+
return { type = "Vector2", X = value.X, Y = value.Y }
|
|
4190
|
+
elseif t == "CFrame" then
|
|
4191
|
+
local components = { value:GetComponents() }
|
|
4192
|
+
return { type = "CFrame", components = components }
|
|
4193
|
+
elseif t == "Color3" then
|
|
4194
|
+
return { type = "Color3", R = value.R, G = value.G, B = value.B }
|
|
4195
|
+
elseif t == "BrickColor" then
|
|
4196
|
+
return { type = "BrickColor", Name = value.Name }
|
|
4197
|
+
elseif t == "UDim2" then
|
|
4198
|
+
return { type = "UDim2", X = { Scale = value.X.Scale, Offset = value.X.Offset }, Y = { Scale = value.Y.Scale, Offset = value.Y.Offset } }
|
|
4199
|
+
elseif t == "UDim" then
|
|
4200
|
+
return { type = "UDim", Scale = value.Scale, Offset = value.Offset }
|
|
4201
|
+
elseif t == "Instance" then
|
|
4202
|
+
return { type = "Instance", ClassName = value.ClassName, Name = value.Name, Path = value:GetFullName() }
|
|
4203
|
+
elseif t == "EnumItem" then
|
|
4204
|
+
return { type = "EnumItem", Name = tostring(value) }
|
|
4205
|
+
elseif t == "table" then
|
|
4206
|
+
local result = {}
|
|
4207
|
+
local count = 0
|
|
4208
|
+
for k, v in pairs(value) do
|
|
4209
|
+
count = count + 1
|
|
4210
|
+
if count > 100 then
|
|
4211
|
+
result["__truncated__"] = true
|
|
4212
|
+
break
|
|
4213
|
+
end
|
|
4214
|
+
local keyStr = type(k) == "string" and k or tostring(k)
|
|
4215
|
+
result[keyStr] = serializeResult(v, depth + 1)
|
|
4216
|
+
end
|
|
4217
|
+
return result
|
|
4218
|
+
elseif t == "function" then
|
|
4219
|
+
return "<function>"
|
|
4220
|
+
elseif t == "thread" then
|
|
4221
|
+
return "<thread>"
|
|
4222
|
+
else
|
|
4223
|
+
return "<" .. t .. ">"
|
|
4224
|
+
end
|
|
4225
|
+
end
|
|
4226
|
+
|
|
4227
|
+
return {
|
|
4228
|
+
success = true,
|
|
4229
|
+
result = serializeResult(execResult),
|
|
4230
|
+
resultType = typeof(execResult),
|
|
4231
|
+
message = "Code executed successfully"
|
|
4232
|
+
}
|
|
4233
|
+
end
|
|
4234
|
+
|
|
4018
4235
|
-- ============================================
|
|
4019
4236
|
-- INSERT ASSET HANDLER (Creator Store)
|
|
4020
4237
|
-- ============================================
|
|
@@ -4382,6 +4599,295 @@ handlers.captureScreenshot = function(requestData)
|
|
|
4382
4599
|
return resultData
|
|
4383
4600
|
end
|
|
4384
4601
|
|
|
4602
|
+
-- ============================================
|
|
4603
|
+
-- VIEWPORTFRAME RENDERING SYSTEM
|
|
4604
|
+
-- ============================================
|
|
4605
|
+
|
|
4606
|
+
-- Camera angle presets
|
|
4607
|
+
local CameraPresets = {
|
|
4608
|
+
-- Standard orthographic views
|
|
4609
|
+
front = CFrame.new(0, 0, 10) * CFrame.Angles(0, 0, 0),
|
|
4610
|
+
back = CFrame.new(0, 0, -10) * CFrame.Angles(0, math.pi, 0),
|
|
4611
|
+
left = CFrame.new(-10, 0, 0) * CFrame.Angles(0, math.pi/2, 0),
|
|
4612
|
+
right = CFrame.new(10, 0, 0) * CFrame.Angles(0, -math.pi/2, 0),
|
|
4613
|
+
top = CFrame.new(0, 10, 0) * CFrame.Angles(-math.pi/2, 0, 0),
|
|
4614
|
+
bottom = CFrame.new(0, -10, 0) * CFrame.Angles(math.pi/2, 0, 0),
|
|
4615
|
+
|
|
4616
|
+
-- Isometric views (popular for thumbnails)
|
|
4617
|
+
iso = CFrame.new(7, 7, 7) * CFrame.Angles(-math.pi/6, math.pi/4, 0),
|
|
4618
|
+
iso_front = CFrame.new(5, 5, 10) * CFrame.Angles(-math.pi/6, 0, 0),
|
|
4619
|
+
iso_back = CFrame.new(-5, 5, -10) * CFrame.Angles(-math.pi/6, math.pi, 0),
|
|
4620
|
+
|
|
4621
|
+
-- Dramatic angles
|
|
4622
|
+
low_angle = CFrame.new(0, -5, 10) * CFrame.Angles(math.pi/6, 0, 0),
|
|
4623
|
+
high_angle = CFrame.new(0, 15, 8) * CFrame.Angles(-math.pi/3, 0, 0),
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4626
|
+
-- Apply lighting preset to WorldModel
|
|
4627
|
+
local function applyLightingPreset(worldModel, preset)
|
|
4628
|
+
-- Remove existing lighting
|
|
4629
|
+
for _, child in ipairs(worldModel:GetChildren()) do
|
|
4630
|
+
if child:IsA("Light") or child:IsA("Sky") or child:IsA("Atmosphere") then
|
|
4631
|
+
child:Destroy()
|
|
4632
|
+
end
|
|
4633
|
+
end
|
|
4634
|
+
|
|
4635
|
+
if preset == "bright" or preset == "showcase" then
|
|
4636
|
+
-- Three-point lighting setup
|
|
4637
|
+
local keyLight = Instance.new("PointLight")
|
|
4638
|
+
keyLight.Brightness = 2
|
|
4639
|
+
keyLight.Range = 60
|
|
4640
|
+
keyLight.Color = Color3.fromRGB(255, 255, 255)
|
|
4641
|
+
keyLight.Parent = worldModel
|
|
4642
|
+
|
|
4643
|
+
local fillLight = Instance.new("PointLight")
|
|
4644
|
+
fillLight.Brightness = 1
|
|
4645
|
+
fillLight.Range = 40
|
|
4646
|
+
fillLight.Color = Color3.fromRGB(200, 220, 255)
|
|
4647
|
+
fillLight.Parent = worldModel
|
|
4648
|
+
|
|
4649
|
+
local rimLight = Instance.new("PointLight")
|
|
4650
|
+
rimLight.Brightness = 1.5
|
|
4651
|
+
rimLight.Range = 50
|
|
4652
|
+
rimLight.Color = Color3.fromRGB(255, 240, 200)
|
|
4653
|
+
rimLight.Parent = worldModel
|
|
4654
|
+
|
|
4655
|
+
elseif preset == "studio" or preset == "flat" then
|
|
4656
|
+
-- Flat, even lighting like in Studio
|
|
4657
|
+
local ambientLight = Instance.new("PointLight")
|
|
4658
|
+
ambientLight.Brightness = 1
|
|
4659
|
+
ambientLight.Range = 100
|
|
4660
|
+
ambientLight.Color = Color3.fromRGB(255, 255, 255)
|
|
4661
|
+
ambientLight.Parent = worldModel
|
|
4662
|
+
|
|
4663
|
+
elseif preset == "dark" or preset == "dramatic" then
|
|
4664
|
+
-- Single directional light
|
|
4665
|
+
local light = Instance.new("PointLight")
|
|
4666
|
+
light.Brightness = 1.5
|
|
4667
|
+
light.Range = 40
|
|
4668
|
+
light.Color = Color3.fromRGB(255, 200, 150)
|
|
4669
|
+
light.Parent = worldModel
|
|
4670
|
+
end
|
|
4671
|
+
-- default: no lights added (ambient only)
|
|
4672
|
+
end
|
|
4673
|
+
|
|
4674
|
+
-- Calculate bounding box of a model
|
|
4675
|
+
local function getModelBoundingBox(model)
|
|
4676
|
+
local parts = {}
|
|
4677
|
+
|
|
4678
|
+
-- Collect all BaseParts
|
|
4679
|
+
for _, child in ipairs(model:GetDescendants()) do
|
|
4680
|
+
if child:IsA("BasePart") then
|
|
4681
|
+
table.insert(parts, child)
|
|
4682
|
+
end
|
|
4683
|
+
end
|
|
4684
|
+
|
|
4685
|
+
if #parts == 0 then
|
|
4686
|
+
-- No parts found, return default
|
|
4687
|
+
return Vector3.new(1, 1, 1), Vector3.new(0, 0, 0)
|
|
4688
|
+
end
|
|
4689
|
+
|
|
4690
|
+
-- Calculate bounding box
|
|
4691
|
+
local minPos = parts[1].Position - parts[1].Size/2
|
|
4692
|
+
local maxPos = parts[1].Position + parts[1].Size/2
|
|
4693
|
+
|
|
4694
|
+
for _, part in ipairs(parts) do
|
|
4695
|
+
local partMin = part.Position - part.Size/2
|
|
4696
|
+
local partMax = part.Position + part.Size/2
|
|
4697
|
+
|
|
4698
|
+
minPos = Vector3.new(
|
|
4699
|
+
math.min(minPos.X, partMin.X),
|
|
4700
|
+
math.min(minPos.Y, partMin.Y),
|
|
4701
|
+
math.min(minPos.Z, partMin.Z)
|
|
4702
|
+
)
|
|
4703
|
+
maxPos = Vector3.new(
|
|
4704
|
+
math.max(maxPos.X, partMax.X),
|
|
4705
|
+
math.max(maxPos.Y, partMax.Y),
|
|
4706
|
+
math.max(maxPos.Z, partMax.Z)
|
|
4707
|
+
)
|
|
4708
|
+
end
|
|
4709
|
+
|
|
4710
|
+
local size = maxPos - minPos
|
|
4711
|
+
local center = (minPos + maxPos) / 2
|
|
4712
|
+
|
|
4713
|
+
return size, center
|
|
4714
|
+
end
|
|
4715
|
+
|
|
4716
|
+
-- Main handler: Render Object View
|
|
4717
|
+
handlers.renderObjectView = function(requestData)
|
|
4718
|
+
local success, result = pcall(function()
|
|
4719
|
+
local instancePath = requestData.instancePath
|
|
4720
|
+
if not instancePath then
|
|
4721
|
+
return {
|
|
4722
|
+
success = false,
|
|
4723
|
+
error = "instancePath is required"
|
|
4724
|
+
}
|
|
4725
|
+
end
|
|
4726
|
+
|
|
4727
|
+
-- Parse resolution
|
|
4728
|
+
local resolution = requestData.resolution or {width = 512, height = 512}
|
|
4729
|
+
local width = resolution.width or 512
|
|
4730
|
+
local height = resolution.height or 512
|
|
4731
|
+
|
|
4732
|
+
-- Clamp resolution for performance
|
|
4733
|
+
width = math.clamp(width, 64, 2048)
|
|
4734
|
+
height = math.clamp(height, 64, 2048)
|
|
4735
|
+
|
|
4736
|
+
local anglePreset = requestData.angle or "iso"
|
|
4737
|
+
local lighting = requestData.lighting or "bright"
|
|
4738
|
+
local background = requestData.background or "transparent"
|
|
4739
|
+
local autoDistance = requestData.autoDistance ~= false
|
|
4740
|
+
|
|
4741
|
+
-- Get target instance
|
|
4742
|
+
local targetInstance = getInstanceByPath(instancePath)
|
|
4743
|
+
if not targetInstance then
|
|
4744
|
+
return {
|
|
4745
|
+
success = false,
|
|
4746
|
+
error = "Instance not found: " .. instancePath
|
|
4747
|
+
}
|
|
4748
|
+
end
|
|
4749
|
+
|
|
4750
|
+
-- Create ViewportFrame (no parent = no GUI overhead)
|
|
4751
|
+
local viewportFrame = Instance.new("ViewportFrame")
|
|
4752
|
+
viewportFrame.Size = UDim2.fromOffset(width, height)
|
|
4753
|
+
viewportFrame.BackgroundTransparency = background == "transparent" and 1 or 0
|
|
4754
|
+
viewportFrame.BackgroundColor3 = background == "grid" and Color3.fromRGB(128, 128, 128) or Color3.fromRGB(255, 255, 255)
|
|
4755
|
+
|
|
4756
|
+
-- Create WorldModel for viewport
|
|
4757
|
+
local worldModel = Instance.new("WorldModel")
|
|
4758
|
+
worldModel.Parent = viewportFrame
|
|
4759
|
+
|
|
4760
|
+
local camera = Instance.new("Camera")
|
|
4761
|
+
camera.Parent = viewportFrame
|
|
4762
|
+
viewportFrame.CurrentCamera = camera
|
|
4763
|
+
|
|
4764
|
+
-- Clone target into WorldModel
|
|
4765
|
+
local clonedInstance = targetInstance:Clone()
|
|
4766
|
+
clonedInstance.Parent = worldModel
|
|
4767
|
+
|
|
4768
|
+
-- Calculate bounding box and center
|
|
4769
|
+
local boundingSize, boundingCenter = getModelBoundingBox(clonedInstance)
|
|
4770
|
+
local maxDimension = math.max(boundingSize.X, boundingSize.Y, boundingSize.Z)
|
|
4771
|
+
|
|
4772
|
+
-- Position camera
|
|
4773
|
+
local cameraOffset
|
|
4774
|
+
if type(anglePreset) == "string" and CameraPresets[anglePreset] then
|
|
4775
|
+
cameraOffset = CameraPresets[anglePreset]
|
|
4776
|
+
elseif type(anglePreset) == "table" then
|
|
4777
|
+
-- Custom angle {pitch, yaw, roll, distance}
|
|
4778
|
+
local pitch = math.rad(anglePreset.pitch or 0)
|
|
4779
|
+
local yaw = math.rad(anglePreset.yaw or 0)
|
|
4780
|
+
local roll = math.rad(anglePreset.roll or 0)
|
|
4781
|
+
local distance = anglePreset.distance or 10
|
|
4782
|
+
cameraOffset = CFrame.new(0, 0, distance) * CFrame.Angles(pitch, yaw, roll)
|
|
4783
|
+
else
|
|
4784
|
+
cameraOffset = CameraPresets.iso
|
|
4785
|
+
end
|
|
4786
|
+
|
|
4787
|
+
-- Auto-calculate distance to fit object
|
|
4788
|
+
local cameraDistance = 10
|
|
4789
|
+
if autoDistance then
|
|
4790
|
+
local fov = 70
|
|
4791
|
+
cameraDistance = maxDimension / (2 * math.tan(math.rad(fov / 2))) * 1.3
|
|
4792
|
+
cameraDistance = math.max(cameraDistance, 1) -- Minimum distance
|
|
4793
|
+
elseif type(anglePreset) == "table" and anglePreset.distance then
|
|
4794
|
+
cameraDistance = anglePreset.distance
|
|
4795
|
+
end
|
|
4796
|
+
|
|
4797
|
+
-- Apply distance to camera offset
|
|
4798
|
+
local offsetDirection = cameraOffset.Position.Unit
|
|
4799
|
+
local finalCameraPos = boundingCenter + (offsetDirection * cameraDistance)
|
|
4800
|
+
|
|
4801
|
+
camera.CFrame = CFrame.new(finalCameraPos, boundingCenter) * cameraOffset.Rotation
|
|
4802
|
+
camera.FieldOfView = 70
|
|
4803
|
+
|
|
4804
|
+
-- Apply lighting
|
|
4805
|
+
applyLightingPreset(worldModel, lighting)
|
|
4806
|
+
|
|
4807
|
+
-- Add grid background if requested
|
|
4808
|
+
if background == "grid" then
|
|
4809
|
+
local gridPart = Instance.new("Part")
|
|
4810
|
+
gridPart.Size = Vector3.new(maxDimension * 5, 0.1, maxDimension * 5)
|
|
4811
|
+
gridPart.Position = boundingCenter - Vector3.new(0, boundingSize.Y/2 + 0.1, 0)
|
|
4812
|
+
gridPart.Anchored = true
|
|
4813
|
+
gridPart.Material = Enum.Material.SmoothPlastic
|
|
4814
|
+
gridPart.Color = Color3.fromRGB(200, 200, 200)
|
|
4815
|
+
gridPart.Parent = worldModel
|
|
4816
|
+
end
|
|
4817
|
+
|
|
4818
|
+
-- Wait for viewport to render
|
|
4819
|
+
task.wait(0.1)
|
|
4820
|
+
RunService.Heartbeat:Wait()
|
|
4821
|
+
|
|
4822
|
+
-- Capture to EditableImage using AssetService
|
|
4823
|
+
local AssetService = game:GetService("AssetService")
|
|
4824
|
+
local editableImage = AssetService:CreateEditableImage({
|
|
4825
|
+
Size = Vector2.new(width, height)
|
|
4826
|
+
})
|
|
4827
|
+
|
|
4828
|
+
-- Read pixels from viewport
|
|
4829
|
+
local pixelSuccess, pixelBuffer = pcall(function()
|
|
4830
|
+
return viewportFrame:ReadPixelsBuffer(Vector2.new(0, 0), Vector2.new(width, height))
|
|
4831
|
+
end)
|
|
4832
|
+
|
|
4833
|
+
if not pixelSuccess or not pixelBuffer then
|
|
4834
|
+
viewportFrame:Destroy()
|
|
4835
|
+
editableImage:Destroy()
|
|
4836
|
+
return {
|
|
4837
|
+
success = false,
|
|
4838
|
+
error = "Failed to capture viewport pixels: " .. tostring(pixelBuffer)
|
|
4839
|
+
}
|
|
4840
|
+
end
|
|
4841
|
+
|
|
4842
|
+
-- Write pixels to EditableImage
|
|
4843
|
+
editableImage:WritePixelsBuffer(Vector2.new(0, 0), Vector2.new(width, height), pixelBuffer)
|
|
4844
|
+
|
|
4845
|
+
-- Convert to base64 RGBA
|
|
4846
|
+
local rgbaBuffer = editableImage:ReadPixelsBuffer(Vector2.new(0, 0), editableImage.Size)
|
|
4847
|
+
local rgbaString = buffer.tostring(rgbaBuffer)
|
|
4848
|
+
local base64Data = base64encode(rgbaString)
|
|
4849
|
+
|
|
4850
|
+
-- Cleanup
|
|
4851
|
+
viewportFrame:Destroy()
|
|
4852
|
+
editableImage:Destroy()
|
|
4853
|
+
|
|
4854
|
+
return {
|
|
4855
|
+
success = true,
|
|
4856
|
+
base64 = base64Data,
|
|
4857
|
+
width = width,
|
|
4858
|
+
height = height,
|
|
4859
|
+
viewInfo = {
|
|
4860
|
+
objectName = targetInstance.Name,
|
|
4861
|
+
objectClass = targetInstance.ClassName,
|
|
4862
|
+
boundingBox = {
|
|
4863
|
+
size = {x = boundingSize.X, y = boundingSize.Y, z = boundingSize.Z},
|
|
4864
|
+
center = {x = boundingCenter.X, y = boundingCenter.Y, z = boundingCenter.Z}
|
|
4865
|
+
},
|
|
4866
|
+
camera = {
|
|
4867
|
+
distance = cameraDistance,
|
|
4868
|
+
position = {x = finalCameraPos.X, y = finalCameraPos.Y, z = finalCameraPos.Z}
|
|
4869
|
+
},
|
|
4870
|
+
settings = {
|
|
4871
|
+
angle = anglePreset,
|
|
4872
|
+
lighting = lighting,
|
|
4873
|
+
background = background,
|
|
4874
|
+
resolution = {width = width, height = height}
|
|
4875
|
+
}
|
|
4876
|
+
},
|
|
4877
|
+
message = "Rendered " .. targetInstance.Name .. " at " .. width .. "x" .. height
|
|
4878
|
+
}
|
|
4879
|
+
end)
|
|
4880
|
+
|
|
4881
|
+
if success then
|
|
4882
|
+
return result
|
|
4883
|
+
else
|
|
4884
|
+
return {
|
|
4885
|
+
success = false,
|
|
4886
|
+
error = "Failed to render object: " .. tostring(result)
|
|
4887
|
+
}
|
|
4888
|
+
end
|
|
4889
|
+
end
|
|
4890
|
+
|
|
4385
4891
|
-- Now that all handlers are defined, populate the endpoint mapping table
|
|
4386
4892
|
endpointHandlers = {
|
|
4387
4893
|
["/api/file-tree"] = handlers.getFileTree,
|
|
@@ -4431,11 +4937,15 @@ endpointHandlers = {
|
|
|
4431
4937
|
["/api/insert-asset"] = handlers.insertAsset,
|
|
4432
4938
|
["/api/undo"] = handlers.undo,
|
|
4433
4939
|
["/api/redo"] = handlers.redo,
|
|
4940
|
+
-- Execute Lua
|
|
4941
|
+
["/api/execute-lua"] = handlers.executeLua,
|
|
4434
4942
|
-- Playtest control
|
|
4435
4943
|
["/api/play-solo"] = handlers.playSolo,
|
|
4436
4944
|
["/api/stop-play"] = handlers.stopPlay,
|
|
4437
4945
|
-- Screenshot
|
|
4438
4946
|
["/api/capture-screenshot"] = handlers.captureScreenshot,
|
|
4947
|
+
-- ViewportFrame rendering
|
|
4948
|
+
["/api/render-object-view"] = handlers.renderObjectView,
|
|
4439
4949
|
}
|
|
4440
4950
|
|
|
4441
4951
|
local function updateUIState()
|