local module = {} -- PARTS TABLE local parts = {} _G.Storage:WaitForChild("Parts") local function added(object) if object.Parent:IsA("Folder") then local id = object:GetAttribute("PartId") if id then parts[id] = object end end end for _,object in pairs(_G.Storage.Parts:GetDescendants()) do added(object) end _G.Storage.Parts.DescendantAdded:Connect(function(object) if object:GetAttribute("PartId") then warn(string.format("%i:%s, you are late!",object:GetAttribute("PartId"),object:GetFullName())) end added(object) end) function module.get_template_part(part) local id = part:GetAttribute("PartId") if not id then return end local template = parts[id] return template end function module.get_part_by_id(id) return parts[id] end function module.change_material(part,material) if part.Material == Enum.Material.Glass then part.Transparency = part:GetAttribute("OriginalTransparency",part.Transparency) end if material == Enum.Material.Glass then part:SetAttribute("OriginalTransparency",part.Transparency) part.Transparency = 0.5 end part.Material = material end -- PART METHODS function module.apply_material(part,material) if material then part:SetAttribute("Material",material.Name) if part:IsA("Model") then for _,subpart in pairs(part:GetDescendants()) do if subpart:IsA("BasePart") then module.change_material(subpart,material) end end else module.change_material(part,material) end end end function module.apply_edit(part,index,value) part[index].Value = value end function module.get_size(part) local nodes = module.get_nodes_class(part) if nodes == 1 then return part:GetAttribute("Size") elseif nodes == 2 then return Vector3.new(part.Size.Z,0,0) -- Yeah the bloody irony I know right? elseif nodes >= 3 then return Vector3.new(part:FindFirstChildWhichIsA("Part").Size.X,0,0) end end function module.get_thickness(part) return module.get_size(part).X -- WHAT?! end function module.apply_size(part,size) part:SetAttribute("Size",size) local nodes = module.get_nodes_class(part) if nodes == 1 then if part:IsA("Model") then part:ScaleTo(size.X) elseif part:IsA("BasePart") then if size.Y == 0 or size.Z == 0 then return end -- Since this is not for normal parts it's a poly size part.Size = size end elseif nodes == 2 then part.Size = Vector3.new(part.Size.X,size.X,size.X) -- Yeah, this is serious elseif nodes >= 3 then for _,item in pairs(part:GetChildren()) do item.Size = Vector3.new(size.X,item.Size.Y,item.Size.Z) end end end function module.apply_color(part,color) part:SetAttribute("Color",color) if part:IsA("Model") then for _,subpart in pairs(part:GetDescendants()) do if subpart:IsA("BasePart") then subpart.Color = color end end else part.Color = color end end local function Weld(part1,part2,temporary) -- Make a BUILDING weld local weld = Instance.new("WeldConstraint") weld.Name = "BuiltWeldConstraint" weld.Parent = part1 weld.Part0 = part1 weld.Part1 = part2 if temporary then weld.Name = "WeldConstraint" end -- Or not. return weld end function module.weld(part,connection,temporary) local attach = part if part:IsA("Model") then attach = attach.PrimaryPart end local new_weld = Weld(attach,connection,temporary) new_weld.Parent = part return new_weld end function module.form_triangle(pa,pb,p1,p2,p3,s2) local s1 = pa.Size.X s2 = s2 or s1*0.5 s2 = 0 local p12 = p2-p1 local p23 = p3-p2 local p13 = p3-p1 local p12l = p12.Magnitude local p23l = p23.Magnitude local p13l = p13.Magnitude local bottom local bot_len local totop local root local tip local far local t if p12l > p23l and p12l > p13l then -- TRUE: p12 largest FALSE: p12 smaller than p23l or p13l tip = p3 root = p1 far = p2 bottom = p12 bot_len = p12l totop = p13 --[[pa.Color = Color3.new(1,0,0) pb.Color = Color3.new(1,0,0)]] elseif p23l > p13l then -- TRUE: p23 must be largest FALSE: p13 largest tip = p1 root = p2 far = p3 bottom = p23 bot_len = p23l totop = -p12 --[[pa.Color = Color3.new(0,1,0) pb.Color = Color3.new(0,1,0)]] else -- p13 largest tip = p2 root = p1 far = p3 bottom = p13 bot_len = p13l t = pa pa = pb pb = t --[[pa.Color = Color3.new(0,0,1) pb.Color = Color3.new(0,0,1)]] totop = p12 end bottom = bottom.Unit local coeff = totop:Dot(bottom) local mid = root+coeff*bottom local cross = totop:Cross(bottom).Unit local top = (mid-tip) pa.Size = Vector3.new(s1,top.Magnitude,coeff) pb.Size = Vector3.new(s1,top.Magnitude,bot_len - coeff) top = top.Unit pa.CFrame = CFrame.fromMatrix(s2*cross+(root+tip)*0.5,cross,-top,bottom) pb.CFrame = CFrame.fromMatrix(s2*cross+(tip+far)*0.5,-cross,-top,-bottom) end function module.get_tri_positions1(pa,pb) local ca = pa.CFrame local ra = ca.Rotation local cb = pb.CFrame.Position local sa = pa.Size local lb = pb.Size.Z local lh = sa.Y local fb = Vector3.new(0,lh,-lb) return (cb - ra * (fb * 0.5)) -- FOR THIRD RETURN VALUE OF BELOW end function module.get_tri_positions2(pa,pb) local ca = pa.CFrame local ra = ca.Rotation ca = ca.Position local sa = pa.Size local la = sa.Z local lh = sa.Y local fa = Vector3.new(0,lh,la) return (ca + ra * (fa * -0.5)),(ca + ra * (fa * 0.5)) end -- apply data positions of parts local form_triangle = module.form_triangle local number_to_string = {"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"} local string_to_number = {} for num,str in pairs(number_to_string) do string_to_number[str] = num end --; for i = 1,10 do number_to_string[i] = tostring(i) end local mod = CFrame.Angles(0,0,0) function module.apply_nodes(part,nodes) if #nodes < module.get_nodes_class(part) then return true end if #nodes == 1 then part:PivotTo(nodes[1]) elseif #nodes == 2 then part.CFrame = nodes[1]*mod part.Size = Vector3.new(nodes[2],part.Size.Y,part.Size.Z) local first = part:FindFirstChild("1") local second = part:FindFirstChild("2") local p1,p2 = module.get_stick_ends(part) local offset = Vector3.new(nodes[2]*0.5,0,0) if first then first.Position = nodes[1]:PointToWorldSpace(offset) if first:FindFirstChild("WeldConstraint") then first.WeldConstraint.Enabled = true end end if second then second.Position = nodes[1]:PointToWorldSpace(-offset) if second:FindFirstChild("WeldConstraint") then second.WeldConstraint.Enabled = true end end elseif #nodes >= 3 then local template = part["A"] for i=1,(#nodes-2)*2 do local name = number_to_string[i] if not part:FindFirstChild(name) then local piece = template:Clone() piece.Parent = part piece.Name = name end end for i=1,(#nodes-2) do form_triangle(part[number_to_string[i*2-1]],part[number_to_string[i*2]],nodes[1],nodes[i+1],nodes[i+2]) end end end -- get the poly 'type' of a part function module.get_nodes_class(part) return part:GetAttribute("Nodes") or 1 end -- get data positions of parts function module.get_nodes(part) local nodes = part:GetAttribute("Nodes") if not nodes then return {part:GetPivot()} elseif nodes == 2 then local first = part:FindFirstChild("1") local second = part:FindFirstChild("2") if first and second then local offset = second.Position - first.Position return {CFrame.lookAt(first.Position,second.Position,first.CFrame.UpVector)*CFrame.Angles(0,math.pi*0.5,0) + offset * 0.5,offset.Magnitude} else return {part.CFrame,part.Size.X} end elseif nodes >= 3 then local i = -1 local p1 = part["A"] local p2 = part["B"] local t = {module.get_tri_positions2(p1,p2)} while true do i = i + 2 local pn1 = part:FindFirstChild(number_to_string[i]) if not pn1 then return t end local pn2 = part:FindFirstChild(number_to_string[i+1]) if not pn2 then return t end table.insert(t,module.get_tri_positions1(p1,p2)) end return t end end -- shift a poly part along its normal axis while keeping static node positions function module.apply_shift(part,shift) if module.get_nodes_class(part) < 3 then return end local previous = part:GetAttribute("Shift") or 0 if shift == 0 and previous == 0 then part:SetAttribute("Shift",nil) return end part:SetAttribute("Shift",shift) shift = shift - previous for _,item in pairs(part:GetChildren()) do if item:IsA("BasePart") and not item:HasTag("Node") then local neg = (string_to_number[item.Name] % 2 == 0) if neg then neg = -1 else neg = 1 end item.Position = item.Position + item.CFrame.RightVector * item.Size.X * 0.5 * shift * neg end end end -- stupid name, it just tries to shift in the direction based on how we're looking function module.attempt_shift(part,shift,look) local template = part:FindFirstChild("A") if not template then return end module.apply_shift(part,math.sign(template.CFrame.RightVector:Dot(look)) * shift) end -- get the cframe of a single connection function module.eval_connection(connection) local connected = connection.connected local relative = CFrame.identity if connected then relative = connected:GetPivot() end return relative:ToWorldSpace(connection.CFrame) end function module.get_stick_ends(part) local nodes = module.get_nodes(part) if #nodes ~= 2 then error("Part is not a stick",2) end local cframe = nodes[1] local offset = Vector3.new(nodes[2]*0.5,0,0) return cframe:PointToWorldSpace(offset),cframe:PointToWorldSpace(-offset) end function module.get_points(part) local class = module.get_nodes_class(part) if class == 1 then return {} elseif class == 2 then return {module.get_stick_ends(part)} else return module.get_nodes(part) end end -- apply driven connections by attachments in the placement engine function module.eval_connections(part,connections) local class = module.get_nodes_class(part) local cframes = {} for _,connection in ipairs(connections) do table.insert(cframes,module.eval_connection(connection)) end local nodes = {} if class == 1 then nodes = cframes elseif class == 2 then if #cframes == 2 then local to = cframes[2].Position - cframes[1].Position nodes = {CFrame.lookAt(cframes[1].Position,cframes[2].Position,cframes[1].UpVector)*CFrame.Angles(0,math.pi*0.5,0) + (to * 0.5), to.Magnitude} end elseif class == 3 then for _,cframe in ipairs(cframes) do table.insert(nodes,cframe.Position) end end return nodes end function module.apply_internal_welds(part) if part:IsA("Model") then local main = part.PrimaryPart for _,item in pairs(part:GetChildren()) do if item:IsA("BasePart") and item ~= main then Weld(item,main,true) end end end end -- PART STATE METHODS local _state = {} _state.__index = _state function _state.apply_material(this) if this.material then module.apply_material(this.part,this.material) end end function _state.apply_size(this) if this.size then module.apply_size(this.part,this.size) end end function _state.apply_color(this) if this.color then module.apply_color(this.part,this.color) end end function _state.apply_edit(this,index,value) local edit = this.part:FindFirstChild(index) if not edit then error("Could not find edit "..index,2) end module.apply_edit(this.part,index,value) end function _state.apply_edits(this) for index,value in pairs(this.edits) do this:apply_edit(index,value) end end function _state.attempt_shift(this,...) module.attempt_shift(this.part,...) end function _state.apply_internal_welds(this) if module.get_nodes_class(this.part) > 1 then -- Weld together parts inside of poly module.apply_internal_welds(this.part) end end function _state.apply_welds(this) this:apply_internal_welds() for index,connection in pairs(this.welds) do local target = this.part if connection.name then target = this.part[connection.name] end if target:IsA("Model") then target = target.PrimaryPart end if (not this.owner) or (_G.Methods.getOwnedOrNil(this.owner,connection.part)) then module.weld(target,connection.part) end end end function _state.apply_connections(this) this.nodes = module.eval_connections(this.part,this.connections) end function _state.apply_nodes(this) module.apply_nodes(this.part,this.nodes) end function _state.apply_shift(this) if this.look then this:attempt_shift(this.shift or 0,this.look) else module.apply_shift(this.part,this.shift or 0) end end function _state.apply(this,part,options) options = options or {} this.part = part or this.part if this.color then this:apply_color() end if this.size then this:apply_size() end if this.material then this:apply_material() end if this.edits and not options.no_edit then this:apply_edits() end if this.connections then this:apply_connections() end if this.nodes then this:apply_nodes() end if this.welds and not options.no_weld then this:apply_welds() else this:apply_internal_welds() end if this.shift then this:apply_shift() end end function _state.create(this) if not this.id then error("No id",2) end local template = parts[this.id]:Clone() template.Parent = game.Workspace.Objects this:apply(template) return template end function _state.get_nodes_class(this) return module.get_nodes_class(this.part) end function _state.take(this,existing) this.color = existing:GetAttribute("Color") this.material = existing:GetAttribute("Material") this.size = existing:GetAttribute("Size") this.shift = existing:GetAttribute("Shift") this.edits = {} for _,edit in existing:GetChildren() do if edit:HasTag("Edit") then if edit.Value ~= (edit:GetAttribute("Default") or nil) then this.edits[edit.Name] = edit.Value end end end this.nodes = module.get_nodes(existing) return this end function _state.update(this) this:take(this.part) return this end function _state.copy(this,existing) this:take(existing) this.id = existing:GetAttribute("PartId") this.part = parts[this.id]:Clone() this:apply(nil,{no_weld = true}) return this end function module.copy(existing) return module.new():copy(existing) end function module.new(original) return setmetatable(_G.Union({ material = nil, -- Enum.Material SERIAL color = nil, -- Color3.new() SERIAL size = nil, -- Vector3.new() SERIAL shift = nil, -- 1 -> 0 -> -1 SERIAL connections = {}, -- {[n]={cframe=,connection=}} for placement engine etc welds = {}, -- {{name=,touched=},...},... SERIAL edits = {}, -- ? SERIAL owner = nil, -- player part = nil, -- part being worked with (applied) id = nil -- part id SERIAL },original or {}),_state) end -- SERIALISATION --[[local function serialisation_factory(match_string) local serialisation_table = {} for index in match_string:gmatch("[^,]+") do table.insert(serialisation_table,index) end return { serialise = function(this) local t = {} for _,key in pairs(serialisation_table) do table.insert(t,this[key]) end return table.unpack(t) end, deserialise = function(this,...) local t = {...} for index,key in pairs(serialisation_table) do this[key] = t[index] end end, } end local state_serialiser = serialisation_factory("id,connections,color,size,shift,material,edits,welds")]] _state.serialise = function(this) --state_serialiser.serialise local color if this.color then color = {this.color.R,this.color.G,this.color.B} end return this.id,this.connections,color,this.size,this.shift,this.material,this.edits,this.welds,this.look end _state.deserialise = function(this,id,connections,color,size,shift,material,edits,welds,look) --state_serialiser.deserialise if color then this.color = Color3.new(table.unpack(color)) end this.id = id this.connections = connections this.size = size this.shift = shift this.material = material this.edits = edits this.welds = welds this.look = look end return module