Skip to content
Merged
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
190 changes: 107 additions & 83 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,86 @@ import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';
import globals from 'globals';

// ---------------------------------------------------------------------------
// Feature layering boundaries.
//
// Enforced with the built-in `no-restricted-imports` — eslint-plugin-import is
// not a dependency of this repo, and the built-in rule expresses the same
// zones with no new dependency. Two zones per feature:
// 1. Code OUTSIDE the feature directory may reach it ONLY through its public
// surface `@/src/<dir>` (the index), never a deep path.
// 2. The feature's pure layer may not import stores, components, or the
// upper feature modules, and stays framework-free (no pinia, no vue) —
// dependencies point downward only.
//
// Flat config replaces a rule's options wholesale when a later block matches
// the same file, so all features are generated together: each block carries
// the full pattern set its files need, and no block silently erases another
// feature's boundary.
// ---------------------------------------------------------------------------
// `pure.upperModules` is a hand-maintained list of the feature's non-pure
// modules: a new one has to be added here or the pure layer may import it.
const featureBoundaries = (features) => {
const publicSurface = ({ dir }) => ({
group: [`@/src/${dir}/*`, `@/src/${dir}/*/**`, `!@/src/${dir}/index`],
message: `Import the ${dir} feature only from its public surface \`@/src/${dir}\` (src/${dir}/index.ts). A deep import bypasses the feature boundary.`,
});
const otherSurfaces = (feature) =>
features.filter((other) => other !== feature).map(publicSurface);

return [
{
files: ['src/**/*.{js,ts,vue}'],
ignores: features.map(({ dir }) => `src/${dir}/**`),
rules: {
'no-restricted-imports': [
'error',
{ patterns: features.map(publicSurface) },
],
},
},
...features.map((feature) => ({
files: [`src/${feature.dir}/**/*.{js,ts,vue}`],
rules: {
'no-restricted-imports': [
'error',
{ patterns: otherSurfaces(feature) },
],
},
})),
...features.map((feature) => ({
files: feature.pure.files,
ignores: ['**/__tests__/**'],
rules: {
'no-restricted-imports': [
'error',
{
paths: ['pinia', 'vue'].map((name) => ({
name,
message: `The ${feature.dir} pure layer must stay framework-free — no ${name}.`,
})),
patterns: [
{
group: [
...feature.pure.upperModules.flatMap((mod) => [
`@/src/${feature.dir}/${mod}`,
`./${mod}`,
`../${mod}`,
]),
'@/src/store/**',
'@/src/components/**',
],
message: `The ${feature.dir} pure layer must not import stores, components, or upper feature modules — dependencies point downward only.`,
},
...otherSurfaces(feature),
],
},
],
},
})),
];
};

export default tseslint.config(
{
ignores: [
Expand Down Expand Up @@ -89,90 +169,34 @@ export default tseslint.config(
],
},
},
// ---------------------------------------------------------------------------
// Processing feature layering boundaries.
//
// Enforced with the built-in `no-restricted-imports` — eslint-plugin-import is
// not a dependency of this repo, and the built-in rule expresses the same
// zones with no new dependency. Two rules:
// 1. Code OUTSIDE `src/processing/` may reach the feature ONLY through its
// public surface `@/src/processing` (the index), never a deep path.
// 2. The feature's pure layer (`engine/**`, `types.ts`, `config.ts`) may not
// import stores, components, or the upper feature modules, and stays
// framework-free (no pinia, no vue) — dependencies point downward only.
// ---------------------------------------------------------------------------
{
files: ['src/**/*.{js,ts,vue}'],
ignores: ['src/processing/**'],
rules: {
'no-restricted-imports': [
'error',
{
patterns: [
{
group: [
'@/src/processing/*',
'@/src/processing/*/**',
'!@/src/processing/index',
],
message:
'Import the processing feature only from its public surface `@/src/processing` (src/processing/index.ts). A deep import bypasses the feature boundary.',
},
],
},
],
...featureBoundaries([
{
dir: 'processing',
pure: {
files: [
'src/processing/engine/**/*.{js,ts}',
'src/processing/types.ts',
'src/processing/config.ts',
],
upperModules: [
'store',
'applyResults',
'jobResultReview',
'index',
'components/**',
],
},
},
},
{
files: [
'src/processing/engine/**/*.{js,ts}',
'src/processing/types.ts',
'src/processing/config.ts',
],
ignores: ['**/__tests__/**'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'pinia',
message:
'The processing pure layer (engine/types/config) must stay framework-free — no pinia.',
},
{
name: 'vue',
message:
'The processing pure layer (engine/types/config) must stay framework-free — no vue.',
},
],
patterns: [
{
group: [
'@/src/processing/store',
'@/src/processing/applyResults',
'@/src/processing/jobResultReview',
'@/src/processing/index',
'@/src/processing/components/**',
'@/src/store/**',
'@/src/components/**',
'./store',
'./applyResults',
'./jobResultReview',
'./index',
'../store',
'../applyResults',
'../jobResultReview',
'../index',
'../components/**',
],
message:
'The processing pure layer (engine/types/config) must not import stores, components, or upper feature modules — dependencies point downward only.',
},
],
},
],
{
dir: 'referenceLines',
pure: {
files: [
'src/referenceLines/geometry.ts',
'src/referenceLines/crossings.ts',
],
upperModules: ['store', 'index', 'useReferenceLines', 'components/**'],
},
},
},
]),
eslintConfigPrettier
);
14 changes: 14 additions & 0 deletions src/components/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
hide-details
></v-switch>

<v-switch
:label="`Reference Lines (${referenceLinesEnabled ? 'On' : 'Off'})`"
v-model="referenceLinesEnabled"
color="secondary"
density="compact"
hide-details
data-testid="reference-lines-switch"
></v-switch>

<v-switch
v-if="errorReportingConfigured"
:label="`Error Reporting (${reportingEnabled ? 'On' : 'Off'})`"
Expand All @@ -52,6 +61,7 @@ import { useLocalStorage } from '@vueuse/core';

import { useKeyboardShortcutsStore } from '@/src/store/keyboard-shortcuts';
import { useViewCameraStore } from '@/src/store/view-configs/camera';
import { useReferenceLinesStore } from '@/src/referenceLines';
import DicomWebSettings from './dicom-web/DicomWebSettings.vue';
import ServerSettings from './ServerSettings.vue';
import { DarkTheme, LightTheme, ThemeStorageKey } from '../constants';
Expand All @@ -78,6 +88,9 @@ export default defineComponent({
});

const { disableCameraAutoReset } = storeToRefs(useViewCameraStore());
const { enabled: referenceLinesEnabled } = storeToRefs(
useReferenceLinesStore()
);

const keyboardStore = useKeyboardShortcutsStore();
const openKeyboardShortcuts = () => {
Expand All @@ -90,6 +103,7 @@ export default defineComponent({
errorReportingConfigured,
openKeyboardShortcuts,
disableCameraAutoReset,
referenceLinesEnabled,
};
},
components: {
Expand Down
15 changes: 13 additions & 2 deletions src/components/SliceViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@
></vtk-layer-slice-representation>
</template>
<crop-tool :view-id="viewId" :image-id="currentImageID" />
<crosshairs-tool
<reference-lines
v-if="referenceLinesVisible"
:view-id="viewId"
:image-id="currentImageID"
/>
<CrosshairsWidget2D
v-if="currentTool === Tools.Crosshairs"
:view-id="viewId"
:image-id="currentImageID"
:view-direction="viewDirection"
Expand Down Expand Up @@ -170,7 +176,8 @@ import { useSegmentGroupStore } from '@/src/store/segmentGroups';
import VtkLayerSliceRepresentation from '@/src/components/vtk/VtkLayerSliceRepresentation.vue';
import { useViewAnimationListener } from '@/src/composables/useViewAnimationListener';
import CropTool from '@/src/components/tools/crop/CropTool.vue';
import CrosshairsTool from '@/src/components/tools/crosshairs/CrosshairsTool.vue';
import { ReferenceLines, useReferenceLinesStore } from '@/src/referenceLines';
import CrosshairsWidget2D from '@/src/components/tools/crosshairs/CrosshairsWidget2D.vue';
import PaintTool from '@/src/components/tools/paint/PaintTool.vue';
import PolygonTool from '@/src/components/tools/polygon/PolygonTool.vue';
import RulerTool from '@/src/components/tools/ruler/RulerTool.vue';
Expand Down Expand Up @@ -240,6 +247,10 @@ useViewAnimationListener(vtkView, viewId, '2D');
// active tool
const { currentTool } = storeToRefs(useToolStore());

const { visible: referenceLinesVisible } = storeToRefs(
useReferenceLinesStore()
);

const { slice: currentSlice, range: sliceRange } = useSliceConfig(
viewId,
currentImageID
Expand Down
114 changes: 0 additions & 114 deletions src/components/tools/crosshairs/CrosshairSVG2D.vue

This file was deleted.

Loading
Loading