Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions DoodleBUGS/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions DoodleBUGS/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"cytoscape-grid-guide": "^2.3.3",
"cytoscape-klay": "^3.1.4",
"cytoscape-no-overlap": "^1.0.1",
"cytoscape-panzoom": "^2.5.3",
"cytoscape-snap-to-grid": "^2.0.0",
"cytoscape-svg": "^0.4.0",
"pinia": "^3.0.2",
Expand Down
1 change: 1 addition & 0 deletions DoodleBUGS/src/assets/styles/global.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css');
@import url('https://unpkg.com/cytoscape-panzoom/cytoscape.js-panzoom.css');

:root {
--color-background: #f8f9fa;
Expand Down
193 changes: 181 additions & 12 deletions DoodleBUGS/src/components/canvas/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const props = defineProps<{
gridSize: number;
currentMode: string;
validationErrors: Map<string, ValidationError[]>;
zoomControlsPosition?: string;
}>();

const emit = defineEmits<{
Expand All @@ -29,6 +30,53 @@ const { enableGridSnapping, disableGridSnapping, setGridSize } = useGridSnapping

const validNodeTypes: NodeType[] = ['stochastic', 'deterministic', 'constant', 'observed', 'plate'];

// Zoom state
const currentZoom = ref(1);
const minZoom = 0.1;
const maxZoom =2;

// Panzoom control functions
const zoomIn = () => {
if (cy) {
const newZoom = Math.min(cy.zoom() * 1.2, maxZoom);
cy.zoom({
level: newZoom,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
currentZoom.value = newZoom;
}
};

const zoomOut = () => {
if (cy) {
const newZoom = Math.max(cy.zoom() / 1.2, minZoom);
cy.zoom({
level: newZoom,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
currentZoom.value = newZoom;
}
};

const resetView = () => {
if (cy) {
cy.fit();
currentZoom.value = 1;
}
};

const setZoomLevel = (event: Event) => {
if (cy) {
const target = event.target as HTMLInputElement;
const zoomLevel = Number(target.value);
cy.zoom({
level: zoomLevel,
renderedPosition: { x: cy.width() / 2, y: cy.height() / 2 }
});
currentZoom.value = zoomLevel;
}
};

interface CompoundDropPayload {
node: NodeSingular;
newParent: NodeSingular | null;
Expand Down Expand Up @@ -208,23 +256,72 @@ watch(() => props.gridSize, (newValue: number) => {
watch([() => props.elements, () => props.validationErrors], ([newElements, newErrors]) => {
syncGraphWithProps(newElements, newErrors);
}, { deep: true });

// Sync zoom level with graph
watch(() => cy, (newCy) => {
if (newCy) {
// Set initial zoom level
currentZoom.value = newCy.zoom();

// Listen for zoom events from other sources (mouse wheel, etc.)
newCy.on('zoom', () => {
currentZoom.value = newCy.zoom();
});
}
}, { immediate: true });
</script>

<template>
<div
ref="cyContainer"
class="cytoscape-container"
:class="{
'grid-background': isGridEnabled && gridSize > 0,
'mode-add-node': currentMode === 'add-node',
'mode-add-edge': currentMode === 'add-edge',
'mode-select': currentMode === 'select'
}"
:style="{ '--grid-size': `${gridSize}px` }"
></div>
<div class="canvas-wrapper">
<div
ref="cyContainer"
class="cytoscape-container"
:class="{
'grid-background': isGridEnabled && gridSize > 0,
'mode-add-node': currentMode === 'add-node',
'mode-add-edge': currentMode === 'add-edge',
'mode-select': currentMode === 'select'
}"
:style="{ '--grid-size': `${gridSize}px` }"
></div>

<!-- Custom Panzoom Controls Container -->
<div
v-if="zoomControlsPosition !== 'hidden'"
class="panzoom-controls"
:class="`panzoom-position-${zoomControlsPosition || 'default'}`"
>
<div class="panzoom-button" @click="zoomIn" title="Zoom In">
<i class="fas fa-plus"></i>
</div>
<div class="panzoom-slider-container">
<input
type="range"
:min="minZoom"
:max="maxZoom"
:value="currentZoom"
step="0.1"
class="panzoom-slider"
@input="setZoomLevel"
/>
</div>
<div class="panzoom-button" @click="zoomOut" title="Zoom Out">
<i class="fas fa-minus"></i>
</div>
<div class="panzoom-button" @click="resetView" title="Reset View">
<i class="fas fa-expand"></i>
</div>
</div>
</div>
</template>

<style scoped>
.canvas-wrapper {
position: relative;
flex-grow: 1;
display: flex;
}

.cytoscape-container {
flex-grow: 1;
background-color: var(--color-background-soft);
Expand Down Expand Up @@ -258,4 +355,76 @@ watch([() => props.elements, () => props.validationErrors], ([newElements, newEr
border: 2px dashed #FF0000 !important;
background-color: rgba(255, 0, 0, 0.1) !important;
}
</style>

/* Custom Panzoom Controls Container */
.panzoom-controls {
position: absolute;
z-index: 10;
display: flex;
flex-direction: column;
gap: 4px;
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
padding: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(4px);
}

/* Position classes */
.panzoom-position-default,
.panzoom-position-bottom-left {
left: 12px;
bottom: 12px;
}

.panzoom-position-top-left {
left: 12px;
top: 12px;
}

.panzoom-position-top-right {
right: 12px;
top: 12px;
}

.panzoom-position-bottom-right {
right: 12px;
bottom: 12px;
}

.panzoom-button {
width: 32px;
height: 32px;
background: #ffffff;
border: 1px solid #d0d7de;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
color: #222;
font-size: 12px;
}

.panzoom-button:hover {
background: #f6f8fa;
border-color: #c2c8d0;
transform: scale(1.05);
}

.panzoom-button:active {
transform: scale(0.95);
}

.panzoom-slider-container {

align-items: center;
}
/* Custom Panzoom Slider vertically */
.panzoom-slider {
margin-left: 8px;
writing-mode: vertical-lr;
direction: rtl;
}
</style>
2 changes: 2 additions & 0 deletions DoodleBUGS/src/components/canvas/GraphEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const props = defineProps<{
currentNodeType: NodeType;
elements: GraphElement[];
validationErrors: Map<string, ValidationError[]>;
zoomControlsPosition?: string;
}>();

const emit = defineEmits<{
Expand Down Expand Up @@ -246,6 +247,7 @@ watch(() => props.currentMode, (newMode) => {
:grid-size="gridSize"
:current-mode="props.currentMode"
:validation-errors="props.validationErrors"
:zoom-controls-position="props.zoomControlsPosition"
@canvas-tap="handleCanvasTap"
@node-moved="handleNodeMoved"
@node-dropped="handleNodeDropped"
Expand Down
5 changes: 4 additions & 1 deletion DoodleBUGS/src/components/layouts/MainLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const currentMode = ref<string>('select');
const currentNodeType = ref<NodeType>('stochastic');
const isGridEnabled = ref(true);
const gridSize = ref(20);
const zoomControlsPosition = ref<string>('default');

const isResizingLeft = ref(false);
const isResizingRight = ref(false);
Expand Down Expand Up @@ -377,7 +378,8 @@ const isModelValid = computed(() => validationErrors.value.size === 0);
@new-graph="showNewGraphModal = true" @save-current-graph="saveCurrentGraph"
@open-about-modal="showAboutModal = true" @export-json="handleExportJson" @open-export-modal="openExportModal"
@apply-layout="handleGraphLayout" @load-example="handleLoadExample" @validate-model="validateGraph"
:is-model-valid="isModelValid" @show-validation-issues="showValidationModal = true" />
:is-model-valid="isModelValid" @show-validation-issues="showValidationModal = true"
@update:zoom-controls-position="zoomControlsPosition = $event" />

<div class="content-area">
<aside class="left-sidebar" :style="leftSidebarStyle">
Expand Down Expand Up @@ -413,6 +415,7 @@ const isModelValid = computed(() => validationErrors.value.size === 0);
<main class="graph-editor-wrapper">
<GraphEditor :is-grid-enabled="isGridEnabled" :grid-size="gridSize" :current-mode="currentMode"
:elements="elements" :current-node-type="currentNodeType" :validation-errors="validationErrors"
:zoom-controls-position="zoomControlsPosition"
@update:current-mode="currentMode = $event" @update:current-node-type="currentNodeType = $event"
@element-selected="handleElementSelected" />
</main>
Expand Down
Loading