LibreCAD 3 - Plugin creation

From LibreCAD wiki
Revision as of 17:05, 26 July 2017 by Feragon (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

This is a quick guide on how to create a LibreCAD 3 plugin with the example of the rectangle plugin.

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)