Compare commits

...

20 Commits

Author SHA1 Message Date
Ziad-I 9aba274070 make has_started check for both pid and port
this solves the issue when a user has not killed a previous server so the port of that server wasn't removed from the pid file, which would make has_started return true even if server hasn't started yet.
2024-08-15 01:24:53 +03:00
Ziad-I 3b7d44a1c6 fix gantt display when there are no dependencies 2024-08-15 01:24:52 +03:00
Ziad-I bf722ecfe3 fix setting color-scheme css property 2024-08-15 01:24:51 +03:00
Ziad-I 61cff3df43 edit names of pages and paths for them 2024-08-15 01:24:50 +03:00
Ziad-I 8b4b1da8a4 docstrings and formating 2024-08-15 01:24:48 +03:00
Ziad-I 69997bf9d8 add open different web UI pages on starting it up 2024-08-15 01:24:47 +03:00
Ziad-I 53907accd9 make webui default for work schedules and simplfy condition 2024-08-15 01:24:45 +03:00
Ziad-I a4f881517b fix dependency display bug
dependency was not being displayed correctly due to calling draw on Gantt before appending it to the DOM, I think?
2024-08-15 01:24:44 +03:00
Ziad-I 8acaf0ff26 edit condition if user tries to export to web with no server
previously it was just checking if there was a server running but not if blender was connected to a server, this was being handled in the operator itself which isn't the best.
2024-08-15 01:24:43 +03:00
Ziad-I 105bfb5647 make web default format when exporting 2024-08-15 01:24:42 +03:00
Ziad-I 866e3b0f66 handle edge case not covered by last commit 2024-08-15 01:24:40 +03:00
Ziad-I 79d6801315 fix error when exporting an empty csv 2024-08-15 01:24:39 +03:00
Ziad-I a2539b659f split addGanttElement function into smaller functions
for code organization and readability
2024-08-15 01:24:38 +03:00
Ziad-I 3e154b6344 fix printing when there is multiple gantt charts 2024-08-15 01:24:37 +03:00
Ziad-I c1fa735f9c edit or commented left over console.log 2024-08-15 01:24:35 +03:00
Ziad-I 9e5f73701c fix connected list appearing in printing 2024-08-15 01:24:34 +03:00
Ziad-I 7fa8ebfbd1 add different bg color for work schedule table rows 2024-08-15 01:24:33 +03:00
Ziad-I d47e5c1167 move outdated gantt warning into connected list 2024-08-15 01:24:32 +03:00
Ziad-I d142fa5e62 move outdated data warning into connected list for csv page 2024-08-15 01:24:30 +03:00
Ziad-I 27ef2b5d49 change used text color for warnings 2024-08-15 01:24:29 +03:00
15 changed files with 418 additions and 291 deletions
+16 -13
View File
@@ -122,27 +122,21 @@ class BlenderNamespace(socketio.AsyncNamespace):
await sio.emit("theme_data", data, namespace="/web")
# Attach namespaces
sio.register_namespace(WebNamespace("/web"))
sio.register_namespace(BlenderNamespace("/blender"))
# Define a route to render the index.html template
async def index(request):
async def schedules(request):
with open("templates/index.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
async def gantt(request):
async def sequencing(request):
with open("templates/gantt.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
return web.Response(text=html_content, content_type="text/html")
async def drawings(request):
async def documentation(request):
with open("templates/drawings.html", "r") as f:
template = f.read()
html_content = pystache.render(template, {"port": sio_port, "version": bonsai_version})
@@ -164,12 +158,21 @@ async def on_startup(app):
json.dump(pids, f, indent=4)
app.router.add_get("/", index)
app.router.add_get("/drawings", drawings)
app.router.add_get("/gantt", gantt)
# Add on startup function
app.on_startup.append(on_startup)
# Attach namespaces
sio.register_namespace(WebNamespace("/web"))
sio.register_namespace(BlenderNamespace("/blender"))
# Regsier routes
app.router.add_get("/", schedules)
app.router.add_get("/documentation", documentation)
app.router.add_get("/sequencing", sequencing)
# Add static files
app.router.add_static("/jsgantt/", path="../gantt", name="jsgantt")
app.router.add_static("/static/", path="./static", name="static")
app.on_startup.append(on_startup)
def main():
@@ -119,11 +119,9 @@ nav ul li a.active {
color: var(--blender-selected-object, var(--nav-link-hover-color));
}
.warning {
.warning,
.warning-icon {
color: var(--blender-info-warning, var(--warning-color));
margin-bottom: var(--margin-small);
padding: var(--padding-tiny);
display: none;
}
.table-description {
@@ -144,6 +142,14 @@ nav ul li a.active {
border-bottom: 1px solid var(--blender-tab-outline, var(--border-color));
}
.table-description tr:nth-child(even) {
background-color: var(--blender-even-row);
}
.table-description tr:nth-child(odd) {
background-color: var(--blender-odd-row);
}
#toggle-theme {
border: none;
background: none;
@@ -342,12 +348,6 @@ button:hover {
border-color: var(--border-color);
}
/* .gTaskInfo {
background-color: #2e2e2e;
color: #c7c7c7;
border-color: #c7c7c7;;
} */
/* Dark mode scrollbar */
:root.dark .frame::-webkit-scrollbar-thumb,
:root.dark .frame::-webkit-scrollbar-track {
@@ -465,6 +465,11 @@ button:hover {
border-color: var(--blender-tab-outline);
}
:root.blender .gtaskcellcurrent {
background-color: var(--blender-odd-row);
border-color: var(--blender-tab-outline);
}
:root.blender .gtaskheading,
:root.blender .gname,
:root.blender .gtaskname,
@@ -105,11 +105,9 @@ nav ul li a.active {
color: var(--blender-selected-object, var(--nav-link-hover-color));
}
.warning {
.warning,
.warning-icon {
color: var(--blender-info-warning, var(--warning-color));
margin-bottom: var(--margin-small);
padding: var(--padding-tiny);
display: none;
}
#toggle-theme {
@@ -5,10 +5,6 @@ let socket;
// Document ready function
$(document).ready(function () {
var systemTheme = window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
$(":root").css("color-scheme", systemTheme);
var defaultTheme = "blender";
var theme = localStorage.getItem("theme") || defaultTheme;
setTheme(theme);
@@ -46,7 +42,7 @@ function handleWebConnect() {
// Function to handle 'blender_connect' event
function handleBlenderConnect(blenderId) {
console.log("blender_connect: ", blenderId);
console.log("blender connected: ", blenderId);
if (!connectedClients.hasOwnProperty(blenderId)) {
connectedClients[blenderId] = {
shown: false,
@@ -77,7 +73,7 @@ function handleDefaultData(data) {
// Function to handle 'blender_disconnect' event
function handleBlenderDisconnect(blenderId) {
console.log("blender_disconnect: ", blenderId);
console.log("blender disconnected: ", blenderId);
if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId];
removeSelectOption(blenderId);
@@ -90,7 +86,7 @@ function handleBlenderDisconnect(blenderId) {
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
console.log(data);
// console.log(data);
data.forEach(function (id) {
connectedClients[id] = {
shown: false,
@@ -101,7 +97,7 @@ function handleConnectedClients(data) {
}
function handleThemeData(themeData) {
console.log(themeData);
// console.log(themeData);
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
@@ -145,7 +141,7 @@ function handleDrawingsData(data) {
const hasDrawings = drawings.length > 0 || sheets.length > 0;
console.log(connectedClients);
// console.log(connectedClients);
if (connectedClients.hasOwnProperty(blenderId)) {
if (!connectedClients[blenderId].shown) {
@@ -203,11 +199,10 @@ function addSelectOption(blenderId, filename) {
function updateSelectOption(blenderId, filename) {
$("#blender-" + blenderId).text(filename);
console.log(blenderId, filename);
// console.log(blenderId, filename);
console.log($("#dropdown-menu").val());
if ($("#dropdown-menu").val() === filename) {
console.log("here");
displayDrawingsNames(blenderId, filename);
}
}
@@ -253,7 +248,7 @@ function displayDrawingsNames(blenderId, ifcFile) {
const msg = {
path: path,
};
console.log(msg);
// console.log(msg);
socket.emit("get_svg", msg);
$("li").removeClass("selected-svg");
@@ -277,6 +272,7 @@ function displayDrawingsNames(blenderId, ifcFile) {
function setTheme(theme) {
$("html").removeClass("light dark blender").addClass(theme);
$(":root").css("color-scheme", theme);
if (theme === "light") {
$("#toggle-theme").html('<i class="fas fa-sun"></i>');
} else if (theme === "dark") {
@@ -1,5 +1,5 @@
// keeps track of blenders connected in form of
// shown:bool, workSchedule: {}, ganttTasks: {},
// shown:bool, workSchedule: {}, ganttTasks: {}, warning: bool
const connectedClients = {};
let socket;
@@ -19,10 +19,6 @@ const pageSizes = [
// Document ready function
$(document).ready(function () {
var systemTheme = window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
$(":root").css("color-scheme", systemTheme);
var defaultTheme = "blender";
var theme = localStorage.getItem("theme") || defaultTheme;
setTheme(theme);
@@ -47,7 +43,7 @@ function connectSocket() {
// Function to handle 'blender_connect' event
function handleBlenderConnect(blenderId) {
console.log("blender_connect: ", blenderId);
console.log("blender connected: ", blenderId);
if (!connectedClients.hasOwnProperty(blenderId)) {
connectedClients[blenderId] = {
shown: false,
@@ -63,7 +59,7 @@ function handleBlenderConnect(blenderId) {
// Function to handle 'blender_disconnect' event
function handleBlenderDisconnect(blenderId) {
console.log("blender_disconnect: ", blenderId);
console.log("blender disconnected: ", blenderId);
if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId];
removeGanttElement(blenderId);
@@ -76,7 +72,7 @@ function handleBlenderDisconnect(blenderId) {
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
console.log(data);
// console.log(data);
data.forEach(function (id) {
connectedClients[id] = {
shown: false,
@@ -87,7 +83,7 @@ function handleConnectedClients(data) {
}
function handleThemeData(themeData) {
console.log(themeData);
// console.log(themeData);
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
@@ -135,12 +131,14 @@ function handleGanttData(data) {
ifc_file: filename,
ganttTasks: ganttTasks,
workSchedule: ganttWorkSched,
warning: false,
};
addGanttElement(blenderId, ganttTasks, ganttWorkSched, filename);
} else {
updateGanttElement(blenderId, ganttTasks, ganttWorkSched, filename);
connectedClients[blenderId].workSchedule = ganttWorkSched;
connectedClients[blenderId].ganttTasks = ganttTasks;
connectedClients[blenderId].warning = false;
}
} else {
connectedClients[blenderId] = {
@@ -148,6 +146,7 @@ function handleGanttData(data) {
ifc_file: filename,
ganttTasks: ganttTasks,
workSchedule: ganttWorkSched,
warning: false,
};
addGanttElement(blenderId, ganttTasks, ganttWorkSched, filename);
}
@@ -161,7 +160,163 @@ function handleDefaultData(data) {
}
// Function to add a new gantt with data and filename
function addGanttElement(blenderId, tasks, workSched, filename) {
function createWorkSchedDiv(blenderId, workSched) {
const workSchedDiv = $("<div></div>").attr("id", "workSched" + blenderId);
const scheduleTable = $("<table></table>")
.addClass("no-print table-description")
.attr("id", "workSchedTable-" + blenderId)
.hide();
$.each(workSched, (key, value) => {
value = value ? value : "null";
$("<tr></tr>")
.append($("<td></td>").text(key))
.append($("<td></td>").text(value))
.appendTo(scheduleTable);
});
const toggleButton = $("<button></button>")
.text("Show Schedule Info")
.addClass("btn no-print")
.on("click", function () {
scheduleTable.toggle();
const buttonText = scheduleTable.is(":visible")
? "Hide Schedule Info"
: "Show Schedule Info";
toggleButton.text(buttonText);
});
workSchedDiv.append(toggleButton);
workSchedDiv.append(scheduleTable);
return workSchedDiv;
}
function createGanttInfoDiv(blenderId, workSched) {
const ganttInfoDiv = $("<div></div>")
.addClass("gantt-info")
.attr("id", "gantt-info-" + blenderId);
const scheduleName = $("<span></span>").text("Schedule: " + workSched.Name);
const createdOn = $("<span></span>")
.text("Created: " + new Date(workSched.CreationDate).toLocaleDateString())
.css("float", "right");
ganttInfoDiv.append(scheduleName);
ganttInfoDiv.append(createdOn);
return ganttInfoDiv;
}
function createGantt(blenderId) {
const ganttDiv = $("<div></div>")
.addClass("gantt-chart")
.attr("id", "gantt-" + blenderId);
let g = new JSGantt.GanttChart(ganttDiv[0], "week");
g.setOptions({
vCaptionType: "Caption", // Set to Show Caption : None,Caption,Resource,Duration,Complete,
vQuarterColWidth: 36,
vDateTaskDisplayFormat: "day dd month yyyy", // Shown in tool tip box
vDayMajorDateDisplayFormat: "mon yyyy - Week ww", // Set format to dates in the "Major" header of the "Day" view
vWeekMinorDateDisplayFormat: "dd mon", // Set format to display dates in the "Minor" header of the "Week" view
vLang: "en",
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
vFormatArr: ["Day", "Week", "Month", "Quarter"], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
vShowRes: true, // Disable the resource column.
vShowComp: false, // Disable the completion column.
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
vAdditionalHeaders: {
ifcduration: { title: "Duration" },
resourceUsage: { title: "Resource Usage" },
},
vUseToolTip: true, // Disable tooltips.
vTooltipTemplate: generateTooltip,
vTotalHeight: 900,
vEventsChange: {
taskname: editValue, // if you need to use the this scope, do: editValue.bind(this)
res: editValue,
dur: editValue,
comp: editValue,
start: editValue,
end: editValue,
planstart: editValue,
planend: editValue,
cost: editValue,
additional_category: editValue,
},
});
JSGantt.addJSONTask(g, tasks);
g.setEditable(true);
connectedClients[blenderId]["gantt"] = g;
return ganttDiv;
}
function createPrint(blenderId) {
const printButton = $("<button>", {
id: "print-btn-" + blenderId,
html: "Print",
class: "btn no-print",
});
const printOptions = $("<select>", {
id: "print-options-" + blenderId,
class: "no-print",
});
$.each(pageSizes, function (index, size) {
printOptions.append(
$("<option>", {
value: size.value,
text: size.text,
})
);
});
printButton.on("click", function () {
$(".gantt-chart").removeClass("no-print");
$(".gantt-chart")
.not("#gantt-" + blenderId)
.addClass("no-print");
$(".gantt-info")
.not("#gantt-info-" + blenderId)
.addClass("no-print");
const values = printOptions.val().split(",");
const g = connectedClients[blenderId]["gantt"];
g.setEditable(false);
g.setTotalHeight("");
g.Draw();
addEventListener("afterprint", () => g.setEditable(true));
let css = `@media print {
@page {
size: ${values[0]}mm ${values[1]}mm;
}
body, p, span, h1, h2, h3, h4, h5, h6, div, a, li, td, th, * {
color: black !important;
}
}`;
g.printChart(values[0], values[1], css);
g.setTotalHeight(900);
g.Draw();
});
return { printButton, printOptions };
}
const ganttContainer = $("<div></div>")
.addClass("gantt-container")
.attr("id", "container-" + blenderId);
@@ -170,171 +325,27 @@ function addGanttElement(blenderId, tasks, workSched, filename) {
.attr("id", "title-" + blenderId)
.text(filename)
.addClass("no-print")
.css("margin-bottom", "10px");
.css("margin-bottom", "0.875rem");
const warning = $("<div></div>")
.attr("id", "warning-" + blenderId)
.html(
"&#9888; Warning: This Gantt Chart may contain outdated data due to recent changes in Blender."
)
.addClass("warning no-print");
const workSchedDiv = $("<div></div>").attr("id", "workSched" + blenderId);
const scheduleTable = $("<table></table>")
.addClass("no-print table-description")
.attr("id", "workSchedTable-" + blenderId)
.hide();
$.each(workSched, (key, value) => {
value = value ? value : "null";
$("<tr></tr>")
.append($("<td></td>").text(key))
.append($("<td></td>").text(value))
.appendTo(scheduleTable);
});
const toggleButton = $("<button></button>")
.text("Show Schedule Info")
.addClass("btn no-print")
.on("click", function () {
scheduleTable.toggle();
const buttonText = scheduleTable.is(":visible")
? "Hide Schedule Info"
: "Show Schedule Info";
toggleButton.text(buttonText);
});
var ganttInfoDiv = $("<div></div>")
.addClass("gantt-info")
.attr("id", "gantt-info-" + blenderId);
const scheduleName = $("<span></span>").text("Schedule: " + workSched.Name);
const createdOn = $("<span></span>")
.text("Created: " + new Date(workSched.CreationDate).toLocaleDateString())
.css("float", "right");
const ganttDiv = $("<div></div>")
.addClass("gantt-chart")
.attr("id", "gantt-" + blenderId);
ganttInfoDiv.append(scheduleName);
ganttInfoDiv.append(createdOn);
workSchedDiv.append(toggleButton);
workSchedDiv.append(scheduleTable);
const workSchedDiv = createWorkSchedDiv(blenderId, workSched);
const ganttInfoDiv = createGanttInfoDiv(blenderId, workSched);
const ganttDiv = createGantt(blenderId, tasks);
const { printButton, printOptions } = createPrint(blenderId);
ganttContainer.append(ganttTitle);
ganttContainer.append(warning);
ganttContainer.append(workSchedDiv);
ganttContainer.append(ganttInfoDiv);
ganttContainer.append(ganttDiv);
$("#container").append(ganttContainer);
let g = new JSGantt.GanttChart($("#gantt-" + blenderId)[0], "week");
g.setOptions({
vCaptionType: "Caption", // Set to Show Caption : None,Caption,Resource,Duration,Complete,
vQuarterColWidth: 36,
vDateTaskDisplayFormat: "day dd month yyyy", // Shown in tool tip box
vDayMajorDateDisplayFormat: "mon yyyy - Week ww", // Set format to dates in the "Major" header of the "Day" view
vWeekMinorDateDisplayFormat: "dd mon", // Set format to display dates in the "Minor" header of the "Week" view
vLang: "en",
vShowTaskInfoLink: 1, // Show link in tool tip (0/1)
vShowEndWeekDate: 0, // Show/Hide the date for the last day of the week in header for daily
vUseSingleCell: 10000, // Set the threshold cell per table row (Helps performance for large data.
vFormatArr: ["Day", "Week", "Month", "Quarter"], // Even with setUseSingleCell using Hour format on such a large chart can cause issues in some browsers,
vShowRes: true, // Disable the resource column.
vShowComp: false, // Disable the completion column.
vShowDur: false, // Disable the duration column, because jsgantt doesn't calculate durations the way we want.
vAdditionalHeaders: {
ifcduration: { title: "Duration" },
resourceUsage: { title: "Resource Usage" },
},
vUseToolTip: true, // Disable tooltips.
vTooltipTemplate: generateTooltip,
vTotalHeight: 900,
vEventsChange: {
taskname: editValue, // if you need to use the this scope, do: editValue.bind(this)
res: editValue,
dur: editValue,
comp: editValue,
start: editValue,
end: editValue,
planstart: editValue,
planend: editValue,
cost: editValue,
additional_category: editValue,
},
});
JSGantt.addJSONTask(g, tasks);
g.setEditable(true);
g.Draw();
connectedClients[blenderId]["gantt"] = g;
let printButton = $("<button>", {
id: "print-btn-" + blenderId,
html: "Print",
class: "btn no-print",
});
let printOptions = $("<select>", {
id: "print-options-" + blenderId,
class: "no-print",
});
$.each(pageSizes, function (index, size) {
printOptions.append(
$("<option>", {
value: size.value,
text: size.text,
})
);
});
printButton.on("click", function () {
// make it only the corresponding gantt chart is printed
$(".gantt-chart").removeClass("no-print");
$(".gantt-chart")
.not("#gantt-" + blenderId)
.addClass("your-css-class");
var values = $("#print-options-" + blenderId)
.val()
.split(",");
g.setEditable(false);
g.setTotalHeight("");
g.Draw();
addEventListener("afterprint", (event) => {
g.setEditable(true);
});
let css =
"@media print {\n" +
" @page {\n" +
" size: " +
values[0] +
"mm " +
values[1] +
"mm;\n" +
" }\n" +
" /* Make all text black */\n" +
" body, p, span, h1, h2, h3, h4, h5, h6, div, a, li, td, th, * {\n" +
" color: black !important;\n" +
" }\n" +
"}";
g.printChart(values[0], values[1], css);
g.setTotalHeight(900);
g.Draw();
});
ganttContainer.append(printOptions);
ganttContainer.append(printButton);
$("#container").append(ganttContainer);
// have to call draw after appening the containers to the DOM
// otherwise the dependencies will not be displayed properly
const g = connectedClients[blenderId].gantt;
g.Draw();
}
// Function to update gantt and filename
@@ -358,7 +369,6 @@ function updateGanttElement(blenderId, tasks, workSched, filename) {
g.Draw();
$("#title-" + blenderId).text(filename);
$("#warning-" + blenderId).css("display", "none");
}
// Function to remove gantt element
@@ -367,7 +377,7 @@ function removeGanttElement(blenderId) {
}
function showWarning(blenderId, isDirty) {
$("#warning-" + blenderId).css("display", "block");
connectedClients[blenderId].warning = true;
}
// Utility function to create a tooltip for the gantt chars
@@ -390,12 +400,12 @@ function generateTooltip(task) {
// Event handlers for editing gantt table data
function editValue(list, task, event, cell, column) {
console.log("editValue function called with the following parameters:");
console.log("list:", list);
console.log("task:", task);
console.log("event:", event);
console.log("cell:", cell);
console.log("column:", column);
// console.log("editValue function called with the following parameters:");
// console.log("list:", list);
// console.log("task:", task);
// console.log("event:", event);
// console.log("cell:", cell);
// console.log("column:", column);
const ganttId = task.getGantt()["vDiv"].id;
const index = ganttId.indexOf("-") + 1;
@@ -418,11 +428,13 @@ function editValue(list, task, event, cell, column) {
value: event.target.value,
},
};
console.log("web operator: " + msg);
socket.emit("web_operator", msg);
}
function setTheme(theme) {
$("html").removeClass("light dark blender").addClass(theme);
$(":root").css("color-scheme", theme);
if (theme === "light") {
$("#toggle-theme").html('<i class="fas fa-sun"></i>');
} else if (theme === "dark") {
@@ -462,12 +474,29 @@ function toggleClientList() {
const clientDiv = $("<div>")
.addClass("client")
.addClass(`client-${id}`)
.text(client.ifc_file || "Blender " + counter);
if (client.warning) {
warningIcon = $("<i>")
.addClass("fas fa-triangle-exclamation warning")
.css("margin-left", "0.5rem");
clientDiv.append(warningIcon);
}
clientDiv.append(dropdownIcon);
const clientDetailsDiv = $("<div>").addClass("client-details");
if (client.warning) {
warningIcon = $("<i>").addClass("fa-solid fa-triangle-exclamation");
const clientWarning = $("<div>")
.addClass("client-detail warning")
.text(" Might have outdated data due to recent changes in Blender.");
clientWarning.prepend(warningIcon);
clientDetailsDiv.append(clientWarning);
}
if (id) {
const clientId = $("<div>")
.addClass("client-detail")
@@ -519,5 +548,6 @@ function toggleClientList() {
clientList.append(clientDiv);
});
clientList.addClass("show");
}
@@ -1,14 +1,10 @@
// keeps track of blenders connected in form of
// {shown:bool, filename:string, headers:array}
// {shown:bool, filename:string, headers:array, warning: false}
const connectedClients = {};
let socket;
// Document ready function
$(document).ready(function () {
var systemTheme = window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
$(":root").css("color-scheme", systemTheme);
var defaultTheme = "blender";
var theme = localStorage.getItem("theme") || defaultTheme;
setTheme(theme);
@@ -33,10 +29,11 @@ function connectSocket() {
// Function to handle 'blender_connect' event
function handleBlenderConnect(blenderId) {
console.log("blender_connect: ", blenderId);
console.log("blender connected: ", blenderId);
if (!connectedClients.hasOwnProperty(blenderId)) {
connectedClients[blenderId] = { shown: false, ifc_file: "" };
}
$("#blender-count").text(function (i, text) {
return parseInt(text, 10) + 1;
});
@@ -44,7 +41,7 @@ function handleBlenderConnect(blenderId) {
// Function to handle 'blender_disconnect' event
function handleBlenderDisconnect(blenderId) {
console.log("blender_disconnect: ", blenderId);
console.log("blender disconnected: ", blenderId);
if (connectedClients.hasOwnProperty(blenderId)) {
delete connectedClients[blenderId];
removeTableElement(blenderId);
@@ -57,14 +54,14 @@ function handleBlenderDisconnect(blenderId) {
function handleConnectedClients(data) {
$("#blender-count").text(data.length);
console.log(data);
// console.log(data);
data.forEach(function (id) {
connectedClients[id] = { shown: false, ifc_file: "" };
});
}
function handleThemeData(themeData) {
console.log(themeData);
// console.log(themeData);
function arrayToRgbString(arr) {
const [r, g, b, a] = arr.map((num) => Math.round(num * 255));
@@ -108,16 +105,19 @@ function handleCsvData(data) {
shown: true,
ifc_file: filename,
headers: [],
warning: false,
};
addTableElement(blenderId, csvData, filename);
} else {
updateTableElement(blenderId, csvData, filename);
connectedClients[blenderId].warning = false;
}
} else {
connectedClients[blenderId] = {
shown: true,
ifc_file: filename,
headers: [],
warning: false,
};
addTableElement(blenderId, csvData, filename);
}
@@ -135,9 +135,15 @@ function addTableElement(blenderId, csvData, filename) {
// store headers of the csv data
const firstLine = csvData.indexOf("\n");
const csvHeaders = csvData.substring(0, firstLine).split(",");
connectedClients[blenderId].headers = csvHeaders;
const hasRows = csvData.substring(firstLine + 1).trim().length > 0;
console.log(connectedClients[blenderId]);
if (hasRows) {
connectedClients[blenderId].headers = csvHeaders;
} else {
connectedClients[blenderId].headers = "";
}
// console.log(connectedClients[blenderId]);
const tableContainer = $("<div></div>")
.addClass("table-container")
.attr("id", "container-" + blenderId);
@@ -147,19 +153,11 @@ function addTableElement(blenderId, csvData, filename) {
.text(filename)
.css("margin-bottom", "10px");
const warning = $("<div></div>")
.attr("id", "warning-" + blenderId)
.html(
"&#9888; Warning: This table may contain outdated data due to recent changes in Blender."
)
.addClass("warning");
const tableDiv = $("<div></div>")
.addClass("csv-table")
.attr("id", "table-" + blenderId);
tableContainer.append(tableTitle);
tableContainer.append(warning);
tableContainer.append(tableDiv);
$("#container").append(tableContainer);
@@ -192,8 +190,8 @@ function addTableElement(blenderId, csvData, filename) {
// Toggle current column visibility
column.visible = !column.visible;
console.log(column.title, column.visible);
// Change menu item checkbox
// console.log(column.title, column.visible);
if (column.visible) {
table.showColumn(column.title);
checkbox.textContent = "\u2611"; // ☑
@@ -209,6 +207,7 @@ function addTableElement(blenderId, csvData, filename) {
}
var table = new Tabulator("#table-" + blenderId, {
placeholder: "No data to display",
height: "400px",
resizableColumnGuide: true,
index: "GlobalId",
@@ -258,6 +257,8 @@ function updateTableElement(blenderId, csvData, filename) {
// else, replace the whole data and column definations
const firstLine = csvData.indexOf("\n");
const newHeaders = csvData.substring(0, firstLine).split(",");
const hasRows = csvData.substring(firstLine + 1).trim().length > 0;
const sameHeaders = compareHeaders(
connectedClients[blenderId].headers,
newHeaders
@@ -269,10 +270,14 @@ function updateTableElement(blenderId, csvData, filename) {
table.replaceData(csvData, { importFormat: "csv" });
} else {
table.setData(csvData);
connectedClients[blenderId].headers = newHeaders;
if (hasRows) {
connectedClients[blenderId].headers = newHeaders;
} else {
connectedClients[blenderId].headers = "";
}
}
$("#title-" + blenderId).text(filename);
$("#warning-" + blenderId).css("display", "none");
}
}
@@ -282,12 +287,16 @@ function removeTableElement(blenderId) {
}
function showWarning(blenderId, isDirty) {
$("#warning-" + blenderId).css("display", "block");
connectedClients[blenderId].warning = true;
}
// Utility function to compare two csv header
function compareHeaders(headers1, headers2) {
if (headers1.length !== headers2.length) {
if (
headers1.length !== headers2.length ||
headers1.length == 0 ||
headers2.length == 0
) {
return false;
}
for (let i = 0; i < headers1.length; i++) {
@@ -300,6 +309,7 @@ function compareHeaders(headers1, headers2) {
function setTheme(theme) {
$("html").removeClass("light dark blender").addClass(theme);
$(":root").css("color-scheme", theme);
var stylesheet = $("#tabulator-stylesheet");
if (theme === "light") {
stylesheet.attr(
@@ -349,12 +359,29 @@ function toggleClientList() {
const clientDiv = $("<div>")
.addClass("client")
.addClass(`client-${id}`)
.text(client.ifc_file || "Blender " + counter);
if (client.warning) {
warningIcon = $("<i>")
.addClass("fas fa-triangle-exclamation warning")
.css("margin-left", "0.5rem");
clientDiv.append(warningIcon);
}
clientDiv.append(dropdownIcon);
const clientDetailsDiv = $("<div>").addClass("client-details");
if (client.warning) {
warningIcon = $("<i>").addClass("fa-solid fa-triangle-exclamation");
const clientWarning = $("<div>")
.addClass("client-detail warning")
.text(" Might have outdated data due to recent changes in Blender.");
clientWarning.prepend(warningIcon);
clientDetailsDiv.append(clientWarning);
}
if (id) {
const clientId = $("<div>")
.addClass("client-detail")
@@ -397,5 +424,6 @@ function toggleClientList() {
clientList.append(clientDiv);
});
clientList.addClass("show");
}
@@ -54,12 +54,12 @@
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li>
<li>
<a href="/gantt"
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
>
</li>
<li>
<a href="drawings" class="active"
<a href="/documentation" class="active"
><i class="fa-solid fa-images"></i> Documentation</a
>
</li>
@@ -36,12 +36,12 @@
<a href="/"><i class="fa-solid fa-table"></i> Schedules</a>
</li>
<li>
<a href="/gantt" class="active"
<a href="/sequencing" class="active"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
>
</li>
<li>
<a href="drawings"
<a href="/documentation"
><i class="fa-solid fa-images"></i> Documentation</a
>
</li>
@@ -60,12 +60,16 @@
</button>
</nav>
<div id="connected-list-div">
<button id="show-connected-button" onclick="toggleClientList()">
<button
id="show-connected-button"
class="no-print"
onclick="toggleClientList()"
>
Connected Blenders:
<span id="blender-count">0</span>
<i class="fas fa-chevron-down"></i>
</button>
<div id="client-list"></div>
<div id="client-list" class="no-print"></div>
</div>
<div id="container"></div>
<footer>
@@ -45,12 +45,12 @@
>
</li>
<li>
<a href="/gantt"
<a href="/sequencing"
><i class="fa-solid fa-chart-gantt"></i> Construction Sequencing</a
>
</li>
<li>
<a href="drawings"
<a href="/documentation"
><i class="fa-solid fa-images"></i> Documentation</a
>
</li>
+1 -1
View File
@@ -254,7 +254,7 @@ class ExportIfcCsv(bpy.types.Operator):
schedule_creator = scheduler.Scheduler()
schedule_creator.schedule(self.filepath, tool.Drawing.get_path_with_ext(self.filepath, "svg"))
if props.format == "web":
if not context.scene.WebProperties.is_running:
if not context.scene.WebProperties.is_connected:
bpy.ops.bim.connect_websocket_server()
tool.Web.send_webui_data(data=ifc_csv.dataframe.to_csv(index=False), data_key="csv_data", event="csv_data")
+1 -1
View File
@@ -91,7 +91,7 @@ class CsvProperties(PropertyGroup):
("web", "web", ""),
],
name="Output format",
default="csv",
default="web",
)
csv_custom_delimiter: StringProperty(default="", name="Custom Delimiter")
should_show_settings: BoolProperty(default=False, name="Show Settings")
+5 -3
View File
@@ -26,6 +26,7 @@ class ConnectToWebsocketServer(bpy.types.Operator):
bl_idname = "bim.connect_websocket_server"
bl_label = "Connect/Start websocket server"
bl_description = "Start/Connect to a Websocket server"
page: bpy.props.StringProperty(default="")
def execute(self, context):
port = context.scene.WebProperties.webserver_port
@@ -33,7 +34,7 @@ class ConnectToWebsocketServer(bpy.types.Operator):
context.scene.WebProperties.webserver_port = core.generate_port_number(tool.Web)
port = context.scene.WebProperties.webserver_port
core.connect_websocket_server(tool.Web, port)
core.connect_websocket_server(tool.Web, port, self.page)
return {"FINISHED"}
@@ -60,9 +61,10 @@ class killWebsocketServer(bpy.types.Operator):
class OpenWebBrowser(bpy.types.Operator):
bl_idname = "bim.open_web_browser"
bl_label = "Open Web Browser"
bl_description = "Open the web UI base URL in your Web Browser"
bl_description = "Open the web UI page URL in your Web Browser"
page: bpy.props.StringProperty(default="")
def execute(self, context):
port = context.scene.WebProperties.webserver_port
core.open_web_browser(tool.Web, port)
core.open_web_browser(tool.Web, port, self.page)
return {"FINISHED"}
+4 -4
View File
@@ -2,14 +2,14 @@ def generate_port_number(web):
return web.generate_port_number()
def connect_websocket_server(web, port):
def connect_websocket_server(web, port, page):
# check if port already has a server listening to it
if web.is_port_available(port):
web.start_websocket_server(port)
if web.has_started(port):
web.connect_websocket_server(port)
web.send_theme_data()
web.open_web_browser(port)
web.open_web_browser(port, page)
return
web.connect_websocket_server(port)
@@ -23,5 +23,5 @@ def kill_websocket_server(web):
web.kill_websocket_server()
def open_web_browser(web, port):
web.open_web_browser(port)
def open_web_browser(web, port, page):
web.open_web_browser(port, page)
+4 -14
View File
@@ -1541,20 +1541,10 @@ class Sequence(bonsai.core.tool.Sequence):
def generate_gantt_browser_chart(
cls, task_json: list[dict[str, Any]], work_schedule: ifcopenshell.entity_instance
) -> None:
if bpy.context.scene.WebProperties.is_connected:
gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)}
tool.Web.send_webui_data(data=gantt_data, data_key="gantt_data", event="gantt_data")
return
with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"), "w") as f:
with open(os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.mustache"), "r") as t:
task_b64 = base64.b64encode(bytes(json.dumps(task_json), "utf-8")).decode("utf-8")
f.write(
pystache.render(
t.read(), {"json_data": task_b64, "data": json.dumps(work_schedule.get_info(recursive=True))}
)
)
webbrowser.open("file://" + os.path.join(bpy.context.scene.BIMProperties.data_dir, "gantt", "index.html"))
if not bpy.context.scene.WebProperties.is_connected:
bpy.ops.bim.connect_websocket_server(page="sequencing")
gantt_data = {"tasks": task_json, "work_schedule": work_schedule.get_info(recursive=True)}
tool.Web.send_webui_data(data=gantt_data, data_key="gantt_data", event="gantt_data")
@classmethod
def is_filter_by_active_schedule(cls) -> bool:
+94 -23
View File
@@ -61,7 +61,7 @@ class Web(bonsai.core.tool.Web):
It then retrieves the port number, and returns it.
Returns:
- int: The port number that was generated.
int: The port number that was generated.
"""
print("Generating port number")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -78,10 +78,10 @@ class Web(bonsai.core.tool.Web):
If the connection is refused, the port is available for use; otherwise, it is in use.
Args:
- port (int): The port number to check.
port (int): The port number to check.
Returns:
- bool: True if the port is available, False if it is in use.
bool: True if the port is available, False if it is in use.
"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# connect_ex returns errno.SUCCESS (0) if the connection succeeds
@@ -94,11 +94,10 @@ class Web(bonsai.core.tool.Web):
Starts a WebSocket server on the specified port.
This method sets up the environment, locates paths, and starts
the WebSocket server process. It also handles the creation and updating of a PID file to keep track
of running server instances.
the WebSocket server process.
Args:
- port (int): The port number on which to start the WebSocket server.
port (int): The port number on which to start the WebSocket server.
"""
import addon_utils
@@ -143,7 +142,7 @@ class Web(bonsai.core.tool.Web):
reconnection attempts, starts an asyncio thread, connects to the WebSocket server, and sets the connection status.
Args:
- port (int): The port number to connect to the WebSocket server.
port (int): The port number to connect to the WebSocket server.
"""
global ws_thread, sio
@@ -223,15 +222,16 @@ class Web(bonsai.core.tool.Web):
"""
Check if the WebSocket server has started on the specified port.
This function continuously checks for the existence of the WebSocket server's process ID (PID) in the running_pid JSON file.
this method continuously checks for the existence of the WebSocket server's process ID (PID) in the running_pid JSON file.
It waits for a maximum of 5 seconds before returning False.
Args:
- port (int): The port number on which the WebSocket server is expected to be running.
port (int): The port number on which the WebSocket server is expected to be running.
Returns:
- bool: True if the WebSocket server has started on the specified port within the maximum time limit, False otherwise.
bool: True if the WebSocket server has started on the specified port within the maximum time limit, False otherwise.
"""
pid = ws_process.pid
max_time = 5
start = time.time()
while True:
@@ -242,7 +242,7 @@ class Web(bonsai.core.tool.Web):
try:
with open(pid_file, "r") as f:
data = json.load(f)
if port in data.values():
if data.get(str(pid)) == port:
return True
except:
pass
@@ -261,11 +261,11 @@ class Web(bonsai.core.tool.Web):
Sends data to the Web UI via Websocket connection.
Args:
- data (Optional[Any]): The data to send. If None, just sends data from WebData.
- data_key (str): The key under which to store the data in the payload. Defaults to "data".
- event (str): The WebSocket event to emit. Defaults to "data".
- namespace (str): The namespace for the WebSocket event. Defaults to "/blender".
- use_web_data (bool): Whether to use data from WebData. Defaults to True.
data (Optional[Any]): The data to send. If None, just sends data from WebData.
data_key (str): The key under which to store the data in the payload. Defaults to "data".
event (str): The WebSocket event to emit. Defaults to "data".
namespace (str): The namespace for the WebSocket event. Defaults to "/blender".
use_web_data (bool): Whether to use data from WebData. Defaults to True.
"""
global ws_thread
@@ -284,6 +284,14 @@ class Web(bonsai.core.tool.Web):
@classmethod
def check_operator_queue(cls) -> None | float:
"""
this method checks the operator queue and processes the operators based on the source page.
If the WebProperties.is_connected is False, it clears the queue and returns None to unregister the timer.
If the queue is not empty, it processes each operator by calling the corresponding handling function.
Returns:
(Optional[float]): Returns None if the WebProperties.is_connected is False, otherwise returns 1.0 to continue the timer.
"""
if not bpy.context.scene.WebProperties.is_connected:
with web_operator_queue.mutex:
web_operator_queue.queue.clear()
@@ -303,6 +311,12 @@ class Web(bonsai.core.tool.Web):
@classmethod
def handle_csv_operator(cls, operator_data: dict) -> None:
"""
this method handles the Schedules page operators.
Args:
operator_data (dict): A dictionary containing the operator data.
"""
if operator_data["type"] == "selection":
bpy.ops.object.select_all(action="DESELECT")
guid = operator_data["globalId"]
@@ -313,6 +327,12 @@ class Web(bonsai.core.tool.Web):
@classmethod
def handle_gantt_operator(cls, operator_data: dict) -> None:
"""
this method handles the Sequencing page operators.
Args:
operator_data (dict): A dictionary containing the operator data.
"""
ifc_file = tool.Ifc.get()
if operator_data["type"] == "editTask":
task_id = int(operator_data["taskId"])
@@ -345,6 +365,12 @@ class Web(bonsai.core.tool.Web):
@classmethod
def handle_drawings_operator(cls, operator_data: dict) -> None:
"""
this method handles the Documentation page operators.
Args:
operator_data (dict): A dictionary containing the operator data.
"""
if operator_data["type"] == "getDrawings":
drawings_data = []
sheets_data = []
@@ -372,11 +398,23 @@ class Web(bonsai.core.tool.Web):
)
@classmethod
def open_web_browser(cls, port: int) -> None:
webbrowser.open(f"http://127.0.0.1:{port}/")
def open_web_browser(cls, port: int, page: str = "") -> None:
"""
Opens a web browser and navigates to the specified URL.
Args:
port (int): The port number to be used in the URL.
page (str): The page name to be appended to the URL. Default is an empty string which points to the index page.
"""
webbrowser.open(f"http://127.0.0.1:{port}/{page}")
@classmethod
def send_theme_data(cls) -> None:
"""
this method retrieves the theme colors from Blender preferences,
calculates the mixed colors, and sends the theme data to the Web UI.
"""
def get_color(theme, attribute) -> tuple[Any, ...]:
return tuple(getattr(theme, attribute)[:])
@@ -409,7 +447,7 @@ class Web(bonsai.core.tool.Web):
"top_bar_header_text": get_color(top_bar, "header_text"),
"active_object": get_color(view_3d, "object_active"),
"selected_object": get_color(view_3d, "object_selected"),
"info_warning": get_color(info, "info_warning"),
"info_warning": get_color(info, "info_warning_text"),
"odd_row": get_color(outliner.space, "back"),
"even_row": mix_colors(get_color(outliner.space, "back"), get_color(outliner, "row_alternate")),
"active_highlight": get_color(outliner, "active"),
@@ -422,19 +460,52 @@ class Web(bonsai.core.tool.Web):
@classmethod
async def sio_connect(cls, url: str) -> None:
"""
Asynchronously establishes a WebSocket connection and sets up event listeners.
This method connects to the specified URL using WebSocket transport and registers
an event listener for the `web_operator` event within the `/blender` namespace.
Args:
url (str): The URL of the WebSocket server to connect to.
"""
await sio.connect(url, transports=["websocket"], namespaces="/blender")
sio.on("web_operator", cls.sio_listen_web_operator, namespace="/blender")
@classmethod
async def sio_disconnect(cls) -> None:
"""
Asynchronously disconnects the WebSocket connection.
This method terminates the active WebSocket connection established by the `sio_connect` method.
"""
await sio.disconnect()
@classmethod
async def sio_send(cls, data: Any, event: str = "data", namespace: str = "/blender") -> None:
"""
Asynchronously sends data to the WebSocket server.
This method emits an event with the provided data to the WebSocket server within the specified namespace.
Args:
data (Any): The data to send to the WebSocket server.
event (Optional[str]): The WebSocket event to emit. Defaults to "data".
namespace (Optional[str]): The namespace for the WebSocket event. Defaults to "/blender".
"""
await sio.emit(event, data, namespace=namespace)
@classmethod
async def sio_listen_web_operator(cls, data: dict) -> None:
"""
Asynchronously handles incoming `web_operator` events from the WebSocket server.
This method receives data from the WebSocket server and attempts to place it into the
`web_operator_queue`. If the queue is full, the data is discarded.
Args:
data (dict): The data received from the `web_operator` event.
"""
try:
web_operator_queue.put_nowait(data)
except queue.Full:
@@ -458,9 +529,9 @@ class AsyncioThread(threading.Thread):
in a separate thread from the main thread.
Args:
- *args: Variable length argument list. These arguments are passed to the superclass constructor.
- loop: An existing asyncio event loop. If None, a new event loop is created.
- **kwargs: Arbitrary keyword arguments. These arguments are passed to the superclass constructor.
*args: Variable length argument list. These arguments are passed to the superclass constructor.
loop: An existing asyncio event loop. If None, a new event loop is created.
**kwargs: Arbitrary keyword arguments. These arguments are passed to the superclass constructor.
"""
super().__init__(*args, **kwargs)
self.loop = loop or asyncio.new_event_loop()
@@ -478,7 +549,7 @@ class AsyncioThread(threading.Thread):
Run a coroutine in the asyncio event loop from a separate thread.
Args:
- coro: The coroutine to be run.
coro: The coroutine to be run.
Returns:
The result of the coroutine.