We have moved to https://dokuwiki.librecad.org/
Lots of content was already moved to the new wiki, but there is still work to do. If you want to contribute, please register a new account at https://dokuwiki.librecad.org/
This wiki will be kept for a while to keep search engine results valid. Moved sites may be deleted here in future.
LibreCAD 3 - Plugin creation
This is a quick guide on how to create a LibreCAD 3 plugin with the example of the rectangle plugin.
Contents
Plugin executions mode
Plugin in GUI mode
In this mode, the LC_interface variable is set to "gui". You can :
- Modify GUI (create toolbar, add buttons, ...)
- Manage multiple documents
- Manage interactions with custom entities
- Create functions that can be used outside the plugin
Quick script (GUI)
In this mode, the LC_interface is not set (equals to nil).
You can:
- Manage the active document (given by the document variable)
- Manage interactions with custom entity
Plugin in command line mode
In this mode, the LC_interface is set to "cli".
You can:
- Manage one document (given by the document variable)
- Create function that can be used in the main script
Script functions
The custom entities are based on blocks, so they need three components:
- Block
- Entities composing the block
- Insert
Here are the three functions to create the components:
function generate_lines(p1, p2, layer, block, metaInfo)
local lines = {}
local lb = LineBuilder()
lb:setLayer(layer)
if(block ~= nil) then
lb:setBlock(block)
end
if(metaInfo ~= nil) then
lb:setMetaInfo(metaInfo)
end
lb:setStart(p1)
lb:setEnd(Coordinate(p2:x(), p1:y(), p1:z()))
lines[1] = lb:build()
lb:setStart(p2)
lb:newID()
lines[2] = lb:build()
lb:setEnd(Coordinate(p1:x(), p2:y(), p1:z()))
lb:newID()
lines[3] = lb:build()
lb:setStart(p1)
lb:newID()
lines[4] = lb:build()
return lines;
end
function createStorage(pos, width, height)
return CustomEntityStorage("LC Plugin", "Rectangle", pos, {
width = width,
height = height
})
end
function createInsert(position, layer, document, storage, metaInfo)
local ceb = CustomEntityBuilder()
if(metaInfo ~= nil) then
ceb:setMetaInfo(metaInfo)
end
ceb:setLayer(layer)
ceb:setCoordinate(position)
ceb:setDocument(document)
ceb:setDisplayBlock(storage)
ceb:setSnapFunction(snapPoints)
ceb:setNearestPointFunction(nearestPointOnPath)
ceb:setDragPointsFunction(dragPoints)
ceb:setNewDragPointFunction(newDragPoints)
ceb:setDragPointsClickedFunction(dragPointClicked)
ceb:setDragPointsReleasedFunction(dragPointReleased)
return ceb:build()
end
The three functions are "enough" to create the custom entity in command line or quick script mode. Some functions (nearestPointOnPath, ...) are missing, they will be given later when we'll treat user interaction. If you don't want them, you can replace the custom entity with an Insert, but you'll loose all the features of custom entities.
GUI
Create the button
if(LC_interface == "gui") then
local tab = toolbar:tabByName("Quick Access")
local group = tab:addGroup("Rectangle")
local RectangleButton = create_button("Rectangle")
tab:addWidget(group, RectangleButton, 0, 0, 1, 1)
luaInterface:luaConnect(RectangleButton, "pressed()", function()
new_operation()
luaInterface:setOperation(active_widget().id, Rectangle(active_widget().id))
end)
end
Here, if we are in GUI mode, we add the rectangle button to the toolbar, and when it's pressed we call the new_operation() function which terminate the previous operation if needed, and we create the Rectangle operation in the current window.
Operation
The operation must inherit Operation class, and re-implement some functions.
local Rectangle = {}
Rectangle.__index = Rectangle
setmetatable(Rectangle, {
__index = Operations,
__call = function (cls, ...)
return cls.new(...)
end,
})
Creation function:
function Rectangle.new(id)
local self = setmetatable({}, Rectangle)
-- Initialization the operation
Operations._init(self, id)
self.p1 = nil
self.p2 = nil
self.entities = {}
-- Register for events
luaInterface:registerEvent("point", self) -- Correspond to a click in the document or a coordinate entered in the command line
luaInterface:registerEvent("mouseMove", self) -- Correspond to a cursor move in the document
-- Send a message in the command line
message("Enter corner 1")
return self
end
The function which receive the events:
function Rectangle:onEvent(eventName, ...)
-- Ignore the event if it was sent from another window
if(Operations.forMe(self) == false) then
return
end
if(eventName == "point") then
self:newData(...)
elseif(eventName == "mouseMove") then
self:tempRectangle(...)
end
end
function Rectangle:newData(data)
if(self.p1 == nil) then
self.p1 = data
message("Enter corner 2")
elseif(self.p2 == nil) then
self.p2 = data
self:storeRectangle()
end
end
The function which display the preview of the rectangle, we need to use TempEntities because it's much faster than adding an entity in the document.
function Rectangle:tempRectangle(point)
-- Don't display the rectangle if we don't have at least two points
if(self.p1 == nil) then
return
end
-- Remove the old preview
for k, v in pairs(self.entities) do
active_widget():tempEntities():removeEntity(v)
end
-- Generate the new preview
self.entities = generate_lines(self.p1, point, active_layer(), nil, active_metaInfo())
-- And display it
for k, v in pairs(self.entities) do
active_widget():tempEntities():addEntity(v)
end
end
The function which end the operation, it'll be called if the user ask for a new operation when the rectangle is being created.
function Rectangle:close()
-- Useful to prevent ending the operation twice
if(self.finished == false) then
self.finished = true
-- Remove the temp entities
for k, v in pairs(self.entities) do
active_widget():tempEntities():removeEntity(v)
end
-- Don't forget to remove the registered events
luaInterface:deleteEvent("point", self)
luaInterface:deleteEvent("mouseMove", self)
-- This is used to remove the "finish" button
luaInterface:triggerEvent("operationFinished", nil)
end
end
User interaction
First, we need to define the nearestPointOnPath as it's used for optimization for the next functions. Here we call the function on each line and return the nearest point.
function nearestPointOnPath(insert, coord)
local point
local distance
local d = insert:document()
local block = insert:displayBlock()
local entities = d:entitiesByBlock(block):asVector()
for k, v in pairs(entities) do
local tmp = v:nearestPointOnPath(coord)
local tmpDistance = coord:distanceTo(tmp)
if(point == nil or tmpDistance < distance) then
point = tmp
distance = tmpDistance
end
end
if(point == nil) then
return Coordinate(0, 0, 0)
end
return point
end
Snap points
For this function, we return the 4 corners of the rectangle. We are getting the base of the rectangle, the the width and height we stored in the parameters.
function snapPoints(insert, a, b, c, d)
local points = {}
local block = insert:displayBlock()
local w = tonumber(block:param("width"))
local h = tonumber(block:param("height"))
local base = insert:position()
table.insert(points, EntityCoordinate(base, 0))
table.insert(points, EntityCoordinate(Coordinate(base:x() + w, base:y(), base:z()), 1))
table.insert(points, EntityCoordinate(Coordinate(base:x() + w, base:y() + h, base:z()), 2))
table.insert(points, EntityCoordinate(Coordinate(base:x(), base:y() + h, base:z()), 3))
return points
end
Drag points
The drag points are the boxes around some points that allows to modify the entity quickly.
The function which gives the drag points is the same as the snap points:
function dragPoints(insert)
local points = {}
local block = insert:displayBlock()
local w = tonumber(block:param("width"))
local h = tonumber(block:param("height"))
local base = insert:position()
points[0] = Coordinate(base:x(), base:y(), base:z())
points[1] = Coordinate(base:x() + w, base:y(), base:z())
points[2] = Coordinate(base:x() + w, base:y() + h, base:z())
points[3] = Coordinate(base:x(), base:y() + h, base:z())
return points
end
Now the modification: you need to declare those variables in the file
local drag_storage = {}
local drag_selected_point = {}
local drag_width = {}
local drag_height = {}
local drag_base = {}
The first function is called when one of this point is clicked. The entity is removed from the document and added to TempEntity.
function dragPointClicked(insert, builder, point)
drag_storage[insert:id()] = {}
drag_selected_point[insert:id()] = point
local d = insert:document()
--Remove old entitites
local removeEntitiesBuilder = EntityBuilder(d)
drag_storage[insert:id()] = d:entitiesByBlock(insert:displayBlock()):asVector()
for k, v in pairs(drag_storage[insert:id()]) do
removeEntitiesBuilder:appendEntity(v)
active_widget():tempEntities():addEntity(v)
end
removeEntitiesBuilder:appendEntity(insert)
removeEntitiesBuilder:appendOperation(Push())
removeEntitiesBuilder:appendOperation(Remove())
builder:append(removeEntitiesBuilder)
--Remove block
builder:append(RemoveBlock(d, insert:displayBlock()))
end
The second function is called when the point is released, it inserts the new entity in the document
function dragPointReleased(insert, builder)
local d = insert:document()
local base = drag_base[insert:id()]
local w = drag_width[insert:id()]
local h = drag_height[insert:id()]
for k, v in pairs(drag_storage[insert:id()]) do
active_widget():tempEntities():removeEntity(v)
end
local block = createStorage(base, w, h)
builder:append(AddBlock(d, block))
local eb = EntityBuilder(d)
local lines = generate_lines(base, Coordinate(base:x() + w, base:y() + h, base:z()), insert:layer(), block, insert:metaInfo())
for k, v in pairs(lines) do
eb:appendEntity(v)
end
eb:appendEntity(createInsert(base, insert:layer(), d, block, insert:metaInfo()))
builder:append(eb)
drag_storage[insert:id()] = nil
drag_selected_point[insert:id()] = nil
drag_width[insert:id()] = nil
drag_height[insert:id()] = nil
drag_base[insert:id()] = nil
end
The last function is called when the cursor has moved (after a point is clicked). The new base, width and height are calculated and the lines of TempEntities updated.
function newDragPoints(insert, position)
for k, v in pairs(drag_storage[insert:id()]) do
active_widget():tempEntities():removeEntity(v)
end
local block = insert:displayBlock()
local w = tonumber(block:param("width"))
local h = tonumber(block:param("height"))
local base = insert:position()
local pointID = drag_selected_point[insert:id()]
if(pointID == 0) then
w = (base:x() + w) - position:x()
h = (base:y() + h) - position:y()
base = position
elseif(pointID == 1) then
w = position:x() - base:x()
h = (base:y() + h) - position:y()
base = Coordinate(base:x(), position:y(), base:z())
elseif(pointID == 2) then
w = position:x() - base:x()
h = position:y() - base:y()
else
w = (base:x() + w) - position:x()
h = position:y() - base:y()
base = Coordinate(position:x(), base:y(), base:z())
end
drag_width[insert:id()] = w
drag_height[insert:id()] = h
drag_base[insert:id()] = base
drag_storage[insert:id()] = generate_lines(base, Coordinate(base:x() + w, base:y() + h, base:z()), insert:layer(), nil, insert:metaInfo())
for k, v in pairs(drag_storage[insert:id()]) do
active_widget():tempEntities():addEntity(v)
end
return insert
end
File load/save
When the document is saved in a file, all the properties of custom entity (Lua functions) are lost. The solution is to declare a function which get the insert as a parameter, and recreate the custom entity.
local function onNewWaitingCustomEntity(insert)
-- We assume that all the custom entities given are rectangles
local ceb = CustomEntityBuilder()
ceb:copy(insert)
ceb:setSnapFunction(snapPoints)
ceb:setNearestPointFunction(nearestPointOnPath)
ceb:setDragPointsFunction(dragPoints)
ceb:setNewDragPointFunction(newDragPoints)
ceb:setDragPointsClickedFunction(dragPointClicked)
ceb:setDragPointsReleasedFunction(dragPointReleased)
local ce = ceb:build()
local b = EntityBuilder(insert:document())
b:appendEntity(ce)
b:execute()
end
LuaCustomEntityManager.getInstance():registerPlugin("LC Plugin", onNewWaitingCustomEntity)