MABobrov commited on
Commit
10fa2a9
·
1 Parent(s): 7fb79e4

Deploy full studio app to HF

Browse files
.dockerignore CHANGED
@@ -2,6 +2,12 @@ web-new/node_modules
2
  web-new/.vite
3
  web-new/dist
4
  mnemo-studio-tool
 
 
 
 
 
 
5
  __pycache__
6
  *.pyc
7
  .pytest_cache
 
2
  web-new/.vite
3
  web-new/dist
4
  mnemo-studio-tool
5
+ !mnemo-studio-tool/
6
+ mnemo-studio-tool/*
7
+ !mnemo-studio-tool/web/
8
+ mnemo-studio-tool/web/*
9
+ !mnemo-studio-tool/web/Widget.json
10
+ !mnemo-studio-tool/web/Statics.xml
11
  __pycache__
12
  *.pyc
13
  .pytest_cache
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ lib/*.xlsm filter=lfs diff=lfs merge=lfs -text
Dockerfile CHANGED
@@ -1,17 +1,27 @@
 
 
 
 
 
 
 
 
1
  FROM python:3.10-slim
2
 
3
  ENV PYTHONDONTWRITEBYTECODE=1 \
4
  PYTHONUNBUFFERED=1 \
5
  PORT=7860 \
6
- FRONTEND_MODE=core \
 
7
  MNEMO_DETECTOR_MODE=colab-pipeline \
 
8
  MNEMO_SENSOR_OCR_ENGINE=easyocr \
9
  MNEMO_PADDLE_DEVICE=cpu \
10
  MNEMO_EASYOCR_DEVICE=cpu
11
 
12
  RUN apt-get update && apt-get install -y --no-install-recommends \
13
  gcc g++ build-essential \
14
- libglib2.0-0 libgl1 \
15
  && rm -rf /var/lib/apt/lists/*
16
 
17
  WORKDIR /app
@@ -21,7 +31,8 @@ COPY requirements.txt .
21
  RUN pip install --upgrade pip && pip install -r requirements.txt
22
 
23
  COPY . .
 
24
 
25
  EXPOSE 7860
26
 
27
- CMD ["python", "app.py", "--frontend-mode", "core"]
 
1
+ FROM node:20-bookworm-slim AS web-builder
2
+
3
+ WORKDIR /src/web-new
4
+ COPY web-new/package*.json ./
5
+ RUN npm ci
6
+ COPY web-new/ ./
7
+ RUN npm run build
8
+
9
  FROM python:3.10-slim
10
 
11
  ENV PYTHONDONTWRITEBYTECODE=1 \
12
  PYTHONUNBUFFERED=1 \
13
  PORT=7860 \
14
+ FRONTEND_MODE=studio \
15
+ WEB_ROOT=/app/web \
16
  MNEMO_DETECTOR_MODE=colab-pipeline \
17
+ MNEMO_ANALYZE_USE_EXCEL=1 \
18
  MNEMO_SENSOR_OCR_ENGINE=easyocr \
19
  MNEMO_PADDLE_DEVICE=cpu \
20
  MNEMO_EASYOCR_DEVICE=cpu
21
 
22
  RUN apt-get update && apt-get install -y --no-install-recommends \
23
  gcc g++ build-essential \
24
+ libglib2.0-0 libgl1 libgomp1 \
25
  && rm -rf /var/lib/apt/lists/*
26
 
27
  WORKDIR /app
 
31
  RUN pip install --upgrade pip && pip install -r requirements.txt
32
 
33
  COPY . .
34
+ COPY --from=web-builder /src/web /app/web
35
 
36
  EXPOSE 7860
37
 
38
+ CMD ["python", "app.py", "--frontend-mode", "studio"]
lib/приложение 6 Альбом и матрица мнемосхем Общий внедрение и проектирование 1.9 (1).xlsm ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:904e6238ea08384af76b91883e86d554768e5a070b933a9b6b07f3d0e58761c5
3
+ size 94933543
mnemo-studio-tool/web/Statics.xml ADDED
@@ -0,0 +1,2407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <shapes name="Rusal"><shape h="32.0" w="40.0" aspect="variable" strokewidth="inherit" name="adjustable_valve_horizontal" displayName="adjustable_valve_horizontal"><foreground><path><move x="20.0" y="19.5" /><line x="20.0" y="9.06" /></path><stroke /><path><move x="0.0" y="8.0" /><line x="20.0" y="20.0" /><line x="0.0" y="32.0" /><close /><move x="40.0" y="8.0" /><line x="20.0" y="20.0" /><line x="40.0" y="32.0" /><close /></path><fillstroke /><stroke /><path><move x="7.940000000000001" y="9.059999999999999" /><curve x1="7.940000000000001" y1="0.05999999999999872" x2="20.0" y2="0.05999999999999961" x3="20.0" y3="0.05999999999999961" /><curve x1="32.06" y1="0.0600000000000005" x2="32.06" y2="9.06" x3="32.06" y3="9.06" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="40.0" w="32.0" aspect="variable" strokewidth="inherit" name="adjustable_valve_vertical" displayName="adjustable_valve_vertical"><foreground><path><move x="12.0" y="20.0" /><line x="23.0" y="20.0" /></path><stroke /><path><move x="24.0" y="0.0" /><line x="12.0" y="20.0" /><line x="-1.7763568394002505e-15" y="0.0" /><close /><move x="24.0" y="40.0" /><line x="12.0" y="20.0" /><line x="1.7763568394002505e-15" y="40.0" /><close /></path><fillstroke /><stroke /><path><move x="23.0" y="8.0" /><curve x1="32.0" y1="8.0" x2="32.0" y2="20.06" x3="32.0" y3="20.06" /><curve x1="32.0" y1="32.12" x2="23.0" y2="32.12" x3="23.0" y3="32.12" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="62.0" w="86.0" aspect="variable" strokewidth="inherit" name="aerial_dryer" displayName="aerial_dryer"><foreground><rect x="0" y="16" w="78" h="46" /><fillstroke /><stroke /><rect x="4" y="0" w="17" h="16" /><fillstroke /><stroke /><rect x="78" y="28.5" w="8" h="21" /><fillstroke /><stroke /></foreground></shape><shape h="159.0" w="32.0" aspect="variable" strokewidth="inherit" name="autoclave" displayName="autoclave"><foreground><ellipse x="0.0" y="0.0" w="32.0" h="29.0" /><fillstroke /><stroke /><ellipse x="0.0" y="130.0" w="32.0" h="29.0" /><fillstroke /><stroke /><rect x="0" y="19" w="32" h="121" /><fillstroke /><stroke /><rect x="0" y="14" w="32" h="5" /><fillstroke /><stroke /><rect x="0" y="140" w="32" h="5" /><fillstroke /><stroke /></foreground></shape><shape h="43.0" w="56.0" aspect="variable" strokewidth="inherit" name="batcher" displayName="batcher"><foreground><rect x="0.06" y="0" w="40.65" h="8.85" /><fillstroke /><stroke /><path><move x="40.72" y="8.850000000000003" /><line x="33.26" y="35.41" /><line x="7.520000000000001" y="35.41" /><line x="0.060000000000002274" y="8.85" /><close /></path><fillstroke /><stroke /><path><move x="55.18" y="38.33" /><line x="55.18" y="42.6" /><line x="3.7900000000000027" y="42.60000000000001" /><line x="3.7900000000000027" y="33.330000000000005" /><line x="50.18" y="33.33" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="42.0" w="63.0" aspect="variable" strokewidth="inherit" name="block" displayName="block"><foreground><path><move x="63.5" y="-3.552713678800501e-15" /><line x="32.0" y="42.0" /><line x="0.5" y="3.552713678800501e-15" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="112.0" w="58.0" aspect="variable" strokewidth="inherit" name="boiler" displayName="boiler">
2
+ <foreground>
3
+ <roundrect arcsize="10" x="0" y="0" w="50.67" h="111" />
4
+ <fillstroke />
5
+ <stroke />
6
+ <rect x="50.67" y="19.59" w="6.33" h="11.43" />
7
+ <fillstroke />
8
+ <stroke />
9
+ <path>
10
+ <move x="5.55" y="8.16" />
11
+ <curve x1="0.0" y1="8.16" x2="0.0" x3="0.0" y2="8.16" y3="8.16" />
12
+ </path>
13
+ <fillstroke />
14
+ <stroke />
15
+ <path>
16
+ <move x="45.11" y="8.17" />
17
+ <curve x1="50.67" y1="8.17" x2="50.67" x3="50.67" y2="8.17" y3="8.17" />
18
+ </path>
19
+ <fillstroke />
20
+ <stroke />
21
+ <path>
22
+ <move x="5.55" y="100.39" />
23
+ <curve x1="10" y1="110.39" x2="13.44" x3="13.44" y2="100.39" y3="100.39" />
24
+ </path>
25
+ <fillstroke />
26
+ <stroke />
27
+ <path>
28
+ <move x="21.37" y="100.39" />
29
+ <curve x1="41.16" y1="110.39" x2="45.11" x3="45.11" y2="100.39" y3="100.39" />
30
+ </path>
31
+ <path>
32
+ <move x="21.37" y="100.39" />
33
+ <curve x1="25.33" y1="110.39" x2="29.29" x3="29.29" y2="100.39" y3="100.39" />
34
+ </path>
35
+ <fillstroke />
36
+ <stroke />
37
+ <path>
38
+ <move x="37.211" y="100.39" />
39
+ <curve x1="41.16" y1="110.39" x2="45.11" x3="45.11" y2="100.39" y3="100.39" />
40
+ </path>
41
+ <fillstroke />
42
+ <stroke />
43
+ <path>
44
+ <move x="29.29" y="16.21" />
45
+ <curve x1="33.25" y1="6.21" x2="37.211" x3="37.211" y2="16.21" y3="16.21" />
46
+ </path>
47
+ <fillstroke />
48
+ <stroke />
49
+ <path>
50
+ <move x="13.46" y="16.21" />
51
+ <curve x1="17.42" y1="6.21" x2="21.37" x3="21.37" y2="16.21" y3="16.21" />
52
+ </path>
53
+ <fillstroke />
54
+ <stroke />
55
+ <path>
56
+ <move x="21.38" y="100.39" />
57
+ <curve x1="21.38" y1="16.32" x2="21.38" x3="21.38" y2="16.32" y3="16.32" />
58
+ </path>
59
+ <fillstroke />
60
+ <stroke />
61
+ <path>
62
+ <move x="29.29" y="100.39" />
63
+ <curve x1="29.29" y1="16.32" x2="29.29" x3="29.29" y2="16.32" y3="16.32" />
64
+ </path>
65
+ <fillstroke />
66
+ <stroke />
67
+ <path>
68
+ <move x="37.21" y="100.39" />
69
+ <curve x1="37.21" y1="16.32" x2="37.21" x3="37.21" y2="16.32" y3="16.32" />
70
+ </path>
71
+ <fillstroke />
72
+ <stroke />
73
+ <path>
74
+ <move x="13.46" y="100.39" />
75
+ <curve x1="13.46" y1="16.32" x2="13.46" x3="13.46" y2="16.32" y3="16.32" />
76
+ </path>
77
+ <fillstroke />
78
+ <stroke />
79
+ <path>
80
+ <move x="0.9" y="4.08" />
81
+ <line x="49.9" y="4.08" />
82
+ </path>
83
+ <fillstroke />
84
+ <stroke />
85
+ <path>
86
+ <move x="5.53" y="100.58" />
87
+ <curve x1="5.52" y1="8.05" x2="5.52" x3="5.52" y2="8.05" y3="8.05" />
88
+ </path>
89
+ <fillstroke />
90
+ <stroke />
91
+ <path>
92
+ <move x="45.11" y="8.05" />
93
+ <curve x1="45.11" y1="100.28" x2="45.11" x3="45.11" y2="100.28" y3="100.28" />
94
+ </path>
95
+ <fillstroke />
96
+ <stroke />
97
+ <path>
98
+ <move x="21.36" y="100.28" />
99
+ <curve x1="21.36" y1="16.21" x2="21.36" x3="21.36" y2="16.21" y3="16.21" />
100
+ </path>
101
+ <fillstroke />
102
+ <stroke />
103
+ <path>
104
+ <move x="29.27" y="100.28" />
105
+ <curve x1="29.27" y1="16.21" x2="29.27" x3="29.27" y2="16.21" y3="16.21" />
106
+ </path>
107
+ <fillstroke />
108
+ <stroke />
109
+ <path>
110
+ <move x="37.19" y="100.28" />
111
+ <curve x1="37.19" y1="16.21" x2="37.19" x3="37.19" y2="16.21" y3="16.21" />
112
+ </path>
113
+ <fillstroke />
114
+ <stroke />
115
+ <path>
116
+ <move x="13.44" y="100.28" />
117
+ <curve x1="13.44" y1="16.21" x2="13.44" x3="13.44" y2="16.21" y3="16.21" />
118
+ </path>
119
+ <fillstroke />
120
+ <stroke />
121
+ </foreground>
122
+ </shape><shape h="85.0" w="50.0" aspect="variable" strokewidth="inherit" name="bunker" displayName="bunker"><foreground><rect x="0" y="0" w="50" h="72" /><fillstroke /><stroke /><path><move x="37.0" y="85.0" /><line x="13.0" y="85.0" /><line x="0.0" y="74.6" /><line x="0.0" y="72.0" /><line x="50.0" y="72.0" /><line x="50.0" y="74.6" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="122.0" w="88.0" aspect="variable" strokewidth="inherit" name="bypass_switch" displayName="bypass_switch">
123
+ <foreground>
124
+ <ellipse x="54.00000000000001" y="59.309999999999995" w="24.8" h="24.8" />
125
+ <fillstroke />
126
+ <stroke />
127
+ <path>
128
+ <move x="66.4" y="59.31" />
129
+ <line x="66.43" y="37.29" />
130
+ <line x="29.0" y="37.31" />
131
+ </path>
132
+ <stroke />
133
+ <ellipse x="44.0" y="74.30999999999999" w="24.8" h="24.8" />
134
+ <fillstroke />
135
+ <stroke />
136
+ <ellipse x="63.00000000000001" y="74.30999999999999" w="24.8" h="24.8" />
137
+ <fillstroke />
138
+ <stroke />
139
+ <path>
140
+ <move x="18.11" y="0.0" />
141
+ <line x="18.14" y="118.29" />
142
+ </path>
143
+ <fillstroke />
144
+ <stroke />
145
+ <path>
146
+ <move x="18.04" y="99.31" />
147
+ <line x="0.0" y="99.29" />
148
+ <line x="0.0" y="120.29" />
149
+ </path>
150
+ <stroke />
151
+ <rect x="7" y="27.81" w="22" h="19" />
152
+ <fillstroke />
153
+ <stroke />
154
+ <ellipse x="44.0" y="74.30999999999999" w="24.8" h="24.8" />
155
+ <stroke />
156
+ <ellipse x="54.00000000000001" y="59.309999999999995" w="24.8" h="24.8" />
157
+ <stroke />
158
+ </foreground>
159
+ </shape><shape h="55.0" w="98.0" aspect="variable" strokewidth="inherit" name="capacitor" displayName="capacitor">
160
+ <foreground>
161
+ <rect x="31" y="35" w="36" h="20" />
162
+ <fillstroke />
163
+ <stroke />
164
+ <ellipse x="19.0" y="0.0" w="60.0" h="45.0" />
165
+ <fillstroke />
166
+ <stroke />
167
+ <path>
168
+ <move x="0.0" y="32.11" />
169
+ <line x="37.0" y="32.0" />
170
+ <line x="31.0" y="23.0" />
171
+ <line x="37.0" y="14.0" />
172
+ <line x="0.0" y="14.0" />
173
+ </path>
174
+ <stroke />
175
+ <path>
176
+ <move x="96.24" y="32.0" />
177
+ <line x="60.0" y="32.0" />
178
+ <line x="68.0" y="23.0" />
179
+ <line x="60.0" y="14.0" />
180
+ <line x="96.24" y="14.12" />
181
+ </path>
182
+ <stroke />
183
+ </foreground>
184
+ </shape><shape h="146.0" w="64.0" aspect="variable" strokewidth="inherit" name="carbonator" displayName="carbonator"><foreground><rect x="0" y="0" w="64" h="73" /><fillstroke /><stroke /><path><move x="64.0" y="73.0" /><line x="41.49999999999999" y="146.0" /><line x="22.499999999999993" y="146.0" /><line x="3.552713678800501e-15" y="73.0" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="33.0" w="96.0" aspect="variable" strokewidth="inherit" name="centrifuge" displayName="centrifuge"><foreground><rect x="83" y="24" w="12" h="8" /><fillstroke /><stroke /><roundrect arcsize="1.82" x="24" y="0" w="71" h="26" /><fillstroke /><stroke /><rect x="6" y="10" w="18" h="6" /><fillstroke /><stroke /><rect x="0" y="16" w="24" h="16" /><fillstroke /><stroke /></foreground></shape><shape h="29.0" w="29.0" aspect="variable" strokewidth="inherit" name="chamber_pump" displayName="chamber_pump"><foreground><rect x="0" y="7.17" w="28" h="17" /><fillstroke /><stroke /><path><move x="0.0" y="7.17" /><line x="3.61" y="2.04" /><line x="24.27" y="2.04" /><line x="27.88" y="7.17" /><close /></path><fillstroke /><stroke /><path><move x="27.89" y="24.17" /><line x="23.28" y="28.769999999999996" /><line x="4.73" y="28.769999999999996" /><line x="0.120000000000001" y="24.169999999999995" /><close /></path><fillstroke /><stroke /><rect x="8.94" y="-0.23" w="9.53" h="2.27" /><fillstroke /><stroke /></foreground></shape><shape h="158.0" w="80.0" aspect="fixed" strokewidth="inherit" name="classifier" displayName="classifier">
185
+ <foreground>
186
+ <path>
187
+ <move x="64.35284264360266" y="27.040235488817537" />
188
+ <line x="69.30170248858624" y="37.65309967501671" />
189
+ <line x="49.36293117377994" y="46.9507014333121" />
190
+ <line x="44.414071328796354" y="36.33783724711293" />
191
+ <close />
192
+ </path>
193
+ <fillstroke />
194
+ <stroke />
195
+ <rect x="27" y="104" w="25" h="53" />
196
+ <fillstroke />
197
+ <stroke />
198
+ <roundrect arcsize="35" x="3" y="42" w="74" h="7" />
199
+ <fillstroke />
200
+ <stroke />
201
+ <roundrect arcsize="35" x="0" y="47" w="79" h="14" />
202
+ <fillstroke />
203
+ <stroke />
204
+ <path>
205
+ <move x="79.0" y="57.0" />
206
+ <line x="60.25" y="104.0" />
207
+ <line x="18.749999999999996" y="104.0" />
208
+ <line x="0.0" y="57.0" />
209
+ <close />
210
+ </path>
211
+ <fillstroke />
212
+ <stroke />
213
+ <path>
214
+ <move x="58.000000000000014" y="42.0" />
215
+ <line x="54.000000000000014" y="31.0" />
216
+ <line x="26.000000000000018" y="31.0" />
217
+ <line x="22.000000000000018" y="42.0" />
218
+ <close />
219
+ </path>
220
+ <fillstroke />
221
+ <stroke />
222
+ <rect x="29" y="18" w="22" h="13" />
223
+ <fillstroke />
224
+ <stroke />
225
+ <rect x="35" y="0" w="10" h="18" />
226
+ <fillstroke />
227
+ <stroke />
228
+
229
+ <path>
230
+ <move x="33.0" y="142.0" />
231
+ <line x="19.067445532594228" y="156.02379471869827" />
232
+ <line x="12.569134213489859" y="149.5254833995939" />
233
+ <line x="20" y="142.0" />
234
+ <line x="20" y="104.0" />
235
+ <line x="59.0" y="104.0" />
236
+ <line x="59.0" y="116.0" />
237
+ <close />
238
+ </path>
239
+ <fillstroke />
240
+ <stroke />
241
+
242
+ </foreground>
243
+ </shape><shape h="61.0" w="231.0" aspect="variable" strokewidth="inherit" name="CoKneader_Continuous" displayName="CoKneader_Continuous"><foreground><rect x="73.23" y="13.22" w="107.26" h="33.56" /><fillstroke /><stroke /><rect x="180.49" y="18.81" w="17.53" h="22.37" /><fillstroke /><stroke /><rect x="198.03" y="26.44" w="31.97" h="7.12" /><fillstroke /><stroke /><rect x="212.47" y="20.91" w="11.35" h="36.04" /><fillstroke /><stroke /><rect x="141.3" y="36.61" w="10.31" h="20.34" /><fillstroke /><stroke /><rect x="89.73" y="36.61" w="10.31" h="20.34" /><fillstroke /><stroke /><path><move x="62.98999999999999" y="0.16000000000000725" /><line x="73.16" y="8.910000000000007" /><line x="73.16" y="50.71000000000001" /><line x="62.989999999999995" y="59.46000000000001" /><close /></path><fillstroke /><stroke /><path><move x="25.459999999999994" y="59.65" /><line x="15.8" y="50.9" /><line x="15.8" y="9.099999999999998" /><line x="25.46" y="0.34999999999999787" /><close /></path><fillstroke /><stroke /><rect x="25.78" y="0" w="37.13" h="60" /><fillstroke /><stroke /><rect x="9.28" y="26.25" w="6.19" h="7.12" /><fillstroke /><stroke /><rect x="0" y="15.25" w="9.28" h="28.47" /><fillstroke /><stroke /><rect x="3.09" y="15.25" w="3.09" h="28.47" /><fillstroke /><stroke /></foreground></shape><shape h="47.0" w="66.0" aspect="variable" strokewidth="inherit" name="compressor" displayName="compressor"><foreground><path><move x="65.5" y="46.5" /><line x="0.5" y="39.37" /><line x="0.5" y="7.619999999999996" /><line x="65.5" y="0.5000000000000036" /><close /></path><fillstroke /><stroke /><path><move x="65.5" y="0.5" /><line x="65.5" y="46.49999999999999" /><line x="62.15" y="46.49999999999999" /><line x="62.15" y="0.5" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="25.0" w="266.0" aspect="variable" strokewidth="inherit" name="conveyor" displayName="conveyor"><foreground><rect x="12.22" y="0" w="238.84" h="25" /><fillstroke /><stroke /><ellipse x="0.0" y="0.0" w="25.0" h="25.0" /><fillstroke /><stroke /><ellipse x="240.77" y="0.0" w="25.0" h="25.0" /><fillstroke /><stroke /></foreground></shape><shape h="25.0" w="703.0" aspect="variable" strokewidth="inherit" name="conveyor_large" displayName="conveyor_large"><foreground><rect x="12" y="0" w="678" h="25" /><fillstroke /><stroke /><ellipse x="0.0" y="0.0" w="25.0" h="25.0" /><fillstroke /><stroke /><ellipse x="677.0" y="0.0" w="25.0" h="25.0" /><fillstroke /><stroke /></foreground></shape><shape h="25.0" w="121.0" aspect="variable" strokewidth="inherit" name="conveyor_small" displayName="conveyor_small"><foreground><rect x="13" y="0" w="95" h="25" /><fillstroke /><stroke /><ellipse x="0.0" y="0.0" w="25.0" h="25.0" /><fillstroke /><stroke /><ellipse x="95.0" y="0.0" w="25.0" h="25.0" /><fillstroke /><stroke /></foreground></shape><shape h="97.0" w="88.0" aspect="variable" strokewidth="inherit" name="cooling_tower" displayName="cooling_tower"><foreground><path><move x="0.0" y="80.48" /><line x="17.6" y="0.0" /><line x="70.4" y="0.0" /><line x="88.0" y="80.48" /><close /></path><fillstroke /><stroke /><rect x="0" y="80.48" w="88" h="15.52" /><fillstroke /><stroke /><path><move x="74.4" y="17.06" /><line x="14.4" y="16.98" /></path><fillstroke /><stroke /><path><move x="43.74" y="80.64" /><line x="43.74" y="0.16" /></path><fillstroke /><stroke /><path><move x="69.04" y="80.48" /><line x="56.55" y="0.32" /></path><fillstroke /><stroke /><path><move x="19.98" y="80.6" /><line x="32.39" y="0.0" /></path><fillstroke /><stroke /><path><move x="77.4" y="32.0" /><line x="10.78" y="32.16" /></path><fillstroke /><stroke /><path><move x="81.4" y="47.73" /><line x="6.82" y="47.68" /></path><fillstroke /><stroke /><path><move x="84.4" y="63.24" /><line x="3.4" y="63.26" /></path><fillstroke /><stroke /></foreground></shape><shape h="73.0" w="73.0" aspect="variable" strokewidth="inherit" name="cooling_tower_section" displayName="cooling_tower_section"><foreground><rect x="0" y="0" w="72" h="72" /><fillstroke /><stroke /><ellipse x="3.1999999999999957" y="3.1999999999999957" w="65.60000000000001" h="65.60000000000001" /><fillstroke /><stroke /><path><move x="21.199999999999996" y="35.2" /><line x="36.0" y="4.0" /><line x="50.8" y="35.2" /><close /></path><fillstroke /><stroke /><path><move x="50.8" y="36.4" /><line x="36.0" y="67.6" /><line x="21.200000000000003" y="36.4" /><close /></path><fillstroke /><stroke /><path><move x="35.2" y="50.800000000000004" /><line x="4.0" y="36.0" /><line x="35.2" y="21.200000000000003" /><close /></path><fillstroke /><stroke /><path><move x="36.8" y="21.2" /><line x="68.0" y="36.0" /><line x="36.8" y="50.8" /><close /></path><fillstroke /><stroke /><ellipse x="22.4" y="22.4" w="27.200000000000003" h="27.200000000000003" /><fillstroke /><stroke /></foreground></shape><shape h="164.0" w="70.0" aspect="variable" strokewidth="inherit" name="correction_pool" displayName="correction_pool">
244
+ <foreground>
245
+
246
+ <path>
247
+ <move x="69.5" y="17.0" />
248
+ <line x="69.5" y="113.0" />
249
+ <line x="69.5" y="128.0" />
250
+ <line x="34.75" y="164.0" />
251
+ <line x="0.0" y="128.0" />
252
+ <line x="0.0" y="17.0" />
253
+ <curve x1="0" y1="0" x2="17" x3="17" y2="0" y3="0" />
254
+ <move x="17.0" y="0.0" />
255
+ <line x="52.5" y="0.0" />
256
+ <curve x1="69.5" y1="0" x2="69.5" x3="69.5" y2="17" y3="17" />
257
+ </path>
258
+ <fillstroke />
259
+
260
+ </foreground>
261
+ </shape><shape h="131.0" w="90.0" aspect="variable" strokewidth="inherit" name="countercurrent_evaporator" displayName="countercurrent_evaporator">
262
+ <foreground>
263
+ <roundrect arcsize="8" x="21.51" y="0" w="51" h="121" />
264
+ <fillstroke />
265
+ <stroke />
266
+ <path>
267
+ <move x="67.00999999999999" y="121.24000000000001" />
268
+ <line x="47.01" y="130.99" />
269
+ <line x="27.009999999999998" y="121.24000000000001" />
270
+ <close />
271
+ </path>
272
+ <fillstroke />
273
+ <stroke />
274
+ <path>
275
+ <move x="21.5" y="42.0" />
276
+ <line x="8.0" y="42.0" />
277
+ <line x="8.0" y="120.0" />
278
+ <line x="0.0" y="120.0" />
279
+ <line x="0.0" y="35.0" />
280
+ <line x="21.5" y="35.0" />
281
+ <close />
282
+ </path>
283
+ <fillstroke />
284
+ <stroke />
285
+ <path>
286
+ <move x="72.5" y="50.0" />
287
+ <line x="90.0" y="50.0" />
288
+ <line x="90.0" y="121.0" />
289
+ <line x="81.0" y="121.0" />
290
+ <line x="81.0" y="56.0" />
291
+ <line x="72.5" y="56.0" />
292
+ <close />
293
+ </path>
294
+ <fillstroke />
295
+ <stroke />
296
+ <path>
297
+ <move x="22.0" y="4.0" />
298
+ <line x="72.7" y="4.0" />
299
+ </path>
300
+ <stroke />
301
+ <path>
302
+ <move x="22.0" y="10.0" />
303
+ <line x="72.7" y="10.0" />
304
+ </path>
305
+ <stroke />
306
+ </foreground>
307
+ </shape><shape h="57.0" w="56.0" aspect="variable" strokewidth="inherit" name="crusher_cone" displayName="crusher_cone"><foreground><rect x="7.72" y="0" w="40.55" h="32.09" /><fillstroke /><stroke /><rect x="0" y="44.67" w="56" h="11.33" /><fillstroke /><stroke /><rect x="0" y="32.09" w="56" h="12.58" /><fillstroke /><stroke /><rect x="5.79" y="32.09" w="4.51" h="12.58" /><fillstroke /><stroke /><rect x="15.61" y="32.09" w="4.51" h="12.58" /><fillstroke /><stroke /><rect x="25.75" y="32.09" w="4.51" h="12.58" /><fillstroke /><stroke /><rect x="36.05" y="32.09" w="4.51" h="12.58" /><fillstroke /><stroke /><rect x="45.7" y="32.09" w="4.51" h="12.58" /><fillstroke /><stroke /><path><move x="20.919999999999998" y="32.019999999999996" /><line x="28.0" y="10.940000000000001" /><line x="35.08" y="32.019999999999996" /><close /></path><fillstroke /><stroke /><path><move x="22.0" y="0.0" /><line x="15.5" y="32.89" /></path><fillstroke /><stroke /><path><move x="33.0" y="0.0" /><line x="40.5" y="32.89" /></path><fillstroke /><stroke /></foreground></shape><shape h="94.0" w="114.0" aspect="variable" strokewidth="inherit" name="crusher_jaw" displayName="crusher_jaw"><foreground><rect x="0" y="0" w="113" h="93" /><fillstroke /><stroke /><ellipse x="8.0" y="39.0" w="50.0" h="41.0" /><fillstroke /><stroke /><path><move x="84.75" y="93.0" /><line x="97.0" y="11.0" /><line x="113.0" y="11.0" /><line x="113.0" y="93.0" /><line x="84.57" y="93.07" /><close /></path><stroke /><path><move x="56.2" y="92.24" /><line x="61.0" y="9.0" /><line x="77.0" y="8.0" /><line x="83.75" y="89.83" /><close /></path><stroke /></foreground></shape><shape h="83.0" w="48.0" aspect="variable" strokewidth="inherit" name="cyclone" displayName="cyclone"><foreground><path><move x="44.0" y="41.0" /><line x="33.0" y="79.0" /><line x="10.999999999999998" y="79.0" /><line x="3.552713678800501e-15" y="41.0" /><close /></path><fillstroke /><stroke /><rect x="0" y="32" w="44" h="10.5" /><fillstroke /><stroke /><rect x="14.5" y="0" w="15" h="10" /><fillstroke /><stroke /><path><move x="44.0" y="32.0" /><line x="44.0" y="42.0" /><line x="33.0" y="79.0" /><line x="11.0" y="79.0" /><line x="0.0" y="42.0" /><line x="0.0" y="32.0" /></path><fillstroke /><stroke /><rect x="0" y="10" w="44" h="22" /><fillstroke /><stroke /></foreground></shape><shape h="73.0" w="222.0" aspect="variable" strokewidth="inherit" name="deaerator" displayName="deaerator"><foreground><roundrect arcsize="12" x="0" y="10" w="222" h="53" /><fillstroke /><stroke /><rect x="21" y="0" w="11" h="10" /><fillstroke /><stroke /><rect x="107" y="0" w="11" h="10" /><fillstroke /><stroke /><rect x="190" y="0" w="11" h="10" /><fillstroke /><stroke /><rect x="156" y="63" w="11" h="10" /><fillstroke /><stroke /><rect x="61" y="63" w="11" h="10" /><fillstroke /><stroke /></foreground></shape><shape h="119.0" w="37.0" aspect="variable" strokewidth="inherit" name="decomposer" displayName="decomposer"><foreground><rect x="0" y="0" w="36" h="96" /><fillstroke /><stroke /><path><move x="0.0" y="96.0" /><line x="36.0" y="96.0" /><line x="36.0" y="105.0" /><line x="18.0" y="119.0" /><line x="0.0" y="105.0" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="155.0" w="67.0" aspect="variable" strokewidth="inherit" name="direct-flow_evaporator" displayName="direct-flow_evaporator">
308
+ <foreground>
309
+
310
+
311
+ <path>
312
+ <move x="9.0" y="60.0" />
313
+ <curve x1="12.0" y1="60.0" x2="6.0" y2="101.5" x3="12.0" y3="143.0" />
314
+ <curve x1="9.0" y1="143.0" x2="3.0" y2="101.5" x3="9.0" y3="60.0" />
315
+ </path>
316
+ <fillstroke />
317
+ <stroke />
318
+
319
+
320
+ <path>
321
+ <move x="56.99999999999999" y="143.0" />
322
+ <curve x1="53.99999999999999" y1="143.0" x2="60.0" y2="101.5" x3="54.00000000000001" y3="60.0" />
323
+ <curve x1="57.00000000000001" y1="60.0" x2="63.0" y2="101.5" x3="56.99999999999999" y3="143.0" />
324
+ </path>
325
+ <fillstroke />
326
+ <stroke />
327
+
328
+
329
+ <roundrect arcsize="13" x="0" y="0" w="66" h="53" />
330
+ <fillstroke />
331
+ <stroke />
332
+
333
+
334
+ <path>
335
+ <move x="65.5" y="49.0" />
336
+ <line x="48.5" y="74.0" />
337
+ <line x="17.0" y="74.0" />
338
+ <line x="0.0" y="49.0" />
339
+ <close />
340
+ </path>
341
+ <fillstroke />
342
+ <stroke />
343
+
344
+
345
+ <rect x="16.75" y="74" w="32" h="52" />
346
+ <fillstroke />
347
+ <stroke />
348
+
349
+
350
+ <path>
351
+ <move x="0.0" y="155.0" />
352
+ <line x="17.0" y="126.0" />
353
+ <line x="48.5" y="126.0" />
354
+ <line x="65.5" y="155.0" />
355
+ <close />
356
+ </path>
357
+ <fillstroke />
358
+ <stroke />
359
+ </foreground>
360
+ </shape><shape h="56.0" w="163.0" aspect="variable" strokewidth="inherit" name="drum_dryer" displayName="drum_dryer"><foreground><rect x="0" y="31.25" w="15" h="19.75" /><fillstroke /><stroke /><roundrect arcsize="5.85" x="20" y="4.64" w="141" h="39" /><fillstroke /><stroke /><rect x="15" y="1" w="15" h="54" /><fillstroke /><stroke /><rect x="111" y="2" w="5" h="45" /><fillstroke /><stroke /><rect x="111" y="7" w="5" h="4" /><fillstroke /><stroke /><rect x="111" y="16" w="5" h="4" /><fillstroke /><stroke /><rect x="111" y="25" w="5" h="4" /><fillstroke /><stroke /><rect x="111" y="34" w="5" h="4" /><fillstroke /><stroke /><rect x="111" y="43" w="5" h="4" /><fillstroke /><stroke /><rect x="154" y="0" w="9" h="55" /><fillstroke /><stroke /></foreground></shape><shape h="115.0" w="139.0" aspect="variable" strokewidth="inherit" name="electric_motor" displayName="electric_motor"><foreground><rect x="13.6" y="0" w="26" h="115" /><fillstroke /><stroke /><rect x="39.6" y="15" w="84" h="85" /><fillstroke /><stroke /><rect x="48.6" y="0" w="65" h="115" /><fillstroke /><stroke /><rect x="122.6" y="11" w="16" h="93" /><fillstroke /><stroke /><path><move x="13.299999999999995" y="114.69999999999999" /><line x="-1.7763568394002505e-15" y="111.44999999999999" /><line x="5.329070518200751e-15" y="3.3399999999999963" /><line x="13.300000000000002" y="0.0899999999999963" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="78.0" w="40.0" aspect="variable" strokewidth="inherit" name="electrostatic_precipitator" displayName="electrostatic_precipitator">
361
+ <foreground>
362
+ <rect x="0" y="10.31" w="40" h="57.59" />
363
+ <fillstroke />
364
+ <stroke />
365
+ <path>
366
+ <move x="39.67" y="67.69999999999999" />
367
+ <line x="36.0" y="78.00999999999999" />
368
+ <line x="4.0" y="78.00999999999999" />
369
+ <line x="0.3299999999999983" y="67.69999999999999" />
370
+ <close />
371
+ </path>
372
+ <fillstroke />
373
+ <stroke />
374
+ <path>
375
+ <move x="0.33" y="10.31" />
376
+ <line x="4.0" y="0.0" />
377
+ <line x="36.0" y="0.0" />
378
+ <line x="39.67" y="10.31" />
379
+ <close />
380
+ </path>
381
+ <fillstroke />
382
+ <stroke />
383
+ <path>
384
+ <move x="23.0" y="25.86" />
385
+ <line x="16.0" y="35.56" />
386
+ <line x="25.0" y="35.56" />
387
+ <line x="13.72" y="47.55" />
388
+ </path>
389
+ <stroke />
390
+ <path>
391
+ <move x="19.15685424949238" y="46.292893218813454" />
392
+ <line x="10.318019484660535" y="50.18198051533946" />
393
+ <line x="14.20710678118655" y="41.34314575050762" />
394
+ <close />
395
+ </path>
396
+ <fillcolor color="#696969" />
397
+ <fillstroke />
398
+ <stroke />
399
+ </foreground>
400
+ </shape><shape h="244.0" w="131.0" aspect="variable" strokewidth="inherit" name="elevator" displayName="elevator">
401
+ <foreground>
402
+
403
+ <path>
404
+ <move x="1.24" y="216.38" />
405
+ <line x="50.47" y="216.38" />
406
+ <line x="50.47" y="0" />
407
+ <line x="122.29" y="0" />
408
+ <line x="130.296" y="27.62" />
409
+ <line x="78" y="27.62" />
410
+ <line x="78" y="244" />
411
+ <line x="9" y="244" />
412
+ <close />
413
+ </path>
414
+ <fillstroke />
415
+ <stroke />
416
+
417
+ </foreground>
418
+ </shape><shape h="244.0" w="131.0" aspect="variable" strokewidth="inherit" name="elevator_left" displayName="elevator_left">
419
+ <foreground>
420
+
421
+ <path>
422
+ <move x="0" y="27.62" />
423
+ <line x="8" y="0" />
424
+ <line x="78" y="0" />
425
+ <line x="78" y="216.38" />
426
+ <line x="130" y="216.38" />
427
+ <line x="122" y="244" />
428
+ <line x="50.47" y="244" />
429
+ <line x="50.47" y="27.62" />
430
+ <close />
431
+ </path>
432
+ <fillstroke />
433
+ <stroke />
434
+
435
+ </foreground>
436
+ </shape><shape h="40.0" w="40.0" aspect="variable" strokewidth="inherit" name="fan" displayName="fan"><foreground><ellipse x="0.0" y="0.0" w="40.0" h="40.0" /><fillstroke /><stroke /><path><move x="0.28" y="17.28" /><curve x1="35.28" y1="7.36" x2="35.28" x3="35.28" y2="7.36" y3="7.36" /></path><fillstroke /><stroke /><path><move x="0.2" y="22.44" /><curve x1="36.76" y1="31.08" x2="36.76" x3="36.76" y2="31.08" y3="31.08" /></path><fillstroke /><stroke /></foreground></shape><shape h="178.0" w="41.0" aspect="variable" strokewidth="inherit" name="feeder _power_line" displayName="feeder _power_line"><foreground><path><move x="19.36" y="169.0" /><line x="19.5" y="13.37" /></path><fillstroke /><stroke /><path><move x="19.51" y="8.12" /><line x="23.0" y="15.12" /><line x="19.5" y="13.37" /><line x="16.0" y="15.12" /><close /></path><fillstroke /><stroke /><rect x="8" y="85.84" w="23" h="20" /><fillstroke /><stroke /><path><move x="0.31" y="169.0" /><line x="0.29" y="138.86" /><line x="19.0" y="138.84" /></path><stroke /><path><move x="19.0" y="68.84" /><line x="38.86" y="68.86" /><line x="38.93" y="44.72" /></path><stroke /></foreground></shape><shape h="131.0" w="25.0" aspect="variable" strokewidth="inherit" name="feeder_busbar" displayName="feeder_busbar">
437
+ <foreground>
438
+ <ellipse x="0.2699999999999996" y="104.89" w="24.8" h="24.8" />
439
+ <fillstroke />
440
+ <stroke />
441
+ <path>
442
+ <move x="12.775898384862245" y="118.9" />
443
+ <line x="19.704101615137752" y="122.9" />
444
+ </path>
445
+ <fillstroke />
446
+ <stroke />
447
+ <path>
448
+ <move x="12.97" y="111.0" />
449
+ <line x="12.97" y="119.0" />
450
+ </path>
451
+ <fillstroke />
452
+ <stroke />
453
+ <path>
454
+ <move x="13.434101615137756" y="118.9" />
455
+ <line x="6.505898384862246" y="122.9" />
456
+ </path>
457
+ <fillstroke />
458
+ <stroke />
459
+ <ellipse x="0.40000000000000036" y="85.19" w="24.8" h="24.8" />
460
+ <fillstroke />
461
+ <stroke />
462
+ <path>
463
+ <move x="12.775898384862245" y="97.73" />
464
+ <line x="19.704101615137752" y="101.73" />
465
+ </path>
466
+ <fillstroke />
467
+ <stroke />
468
+ <path>
469
+ <move x="12.97" y="89.83" />
470
+ <line x="12.97" y="97.83" />
471
+ </path>
472
+ <fillstroke />
473
+ <stroke />
474
+ <path>
475
+ <move x="13.434101615137756" y="97.73" />
476
+ <line x="6.505898384862246" y="101.73" />
477
+ </path>
478
+ <fillstroke />
479
+ <stroke />
480
+ <path>
481
+ <move x="12.8" y="84.99" />
482
+ <line x="12.86" y="30.0" />
483
+ <line x="12.78" y="12.89" />
484
+ </path>
485
+ <fillstroke />
486
+ <stroke />
487
+ <path>
488
+ <move x="23.799999999999997" y="61.99" />
489
+ <line x="1.799999999999999" y="61.99" />
490
+ <line x="1.8000000000000025" y="42.99" />
491
+ <line x="23.800000000000004" y="42.99" />
492
+ <close />
493
+ </path>
494
+ <fillstroke />
495
+ <stroke />
496
+ <fillcolor color="#696969" />
497
+ <path>
498
+ <move x="12.76" y="7.64" />
499
+ <line x="15.12" y="14.63" />
500
+ <line x="12.78" y="12.89" />
501
+ <line x="10.46" y="14.65" />
502
+ <close />
503
+ </path>
504
+ <fillstroke />
505
+ <stroke />
506
+
507
+ <ellipse x="0.40000000000000036" y="104.89" w="24.8" h="24.8" />
508
+
509
+ <stroke />
510
+ </foreground>
511
+ </shape><shape h="191.0" w="39.0" aspect="variable" strokewidth="inherit" name="feeder_with_generator" displayName="feeder_with_generator">
512
+ <foreground>
513
+
514
+
515
+ <path>
516
+ <move x="17.36" y="162.28" />
517
+ <line x="17.51" y="0.28" />
518
+ </path>
519
+ <fillstroke />
520
+ <stroke />
521
+
522
+
523
+ <rect x="6" y="79.12" w="23" h="20" />
524
+ <fillstroke />
525
+ <stroke />
526
+
527
+
528
+ <path>
529
+ <move x="17.0" y="24.12" />
530
+ <line x="36.86" y="24.14" />
531
+ <line x="36.93" y="0.0" />
532
+ </path>
533
+ <stroke />
534
+
535
+
536
+ <ellipse x="0.0" y="157.28" w="33.3" h="33.3" />
537
+ <fillstroke />
538
+ <stroke />
539
+
540
+
541
+ <path>
542
+ <move x="4.58" y="173.93" />
543
+ <curve x1="10.0" y1="160.98" x2="16.0" y2="172.98" x3="17.0" y3="176.98" />
544
+ <curve x1="18.73" y1="180.93" x2="22.0" y2="184.98" x3="28.73" y3="173.93" />
545
+ </path>
546
+ <fillstroke />
547
+ <stroke />
548
+ </foreground>
549
+ </shape><shape h="191.0" w="188.0" aspect="variable" strokewidth="inherit" name="filling_station" displayName="filling_station">
550
+ <foreground>
551
+ <rect x="0" y="0" w="188" h="190.64" />
552
+ <fillstroke />
553
+ <stroke />
554
+
555
+ <path>
556
+ <move x="21" y="130.77" />
557
+ <line x="31" y="130.77" />
558
+ <arc sweep-flag="1" rx="5" ry="5" x="41" y="130.77" />
559
+ <line x="51" y="130.77" />
560
+ <arc sweep-flag="1" large-arc-flag="1" rx="5" ry="6.5" x="51" y="136.24" />
561
+ <line x="41" y="136.24" />
562
+ <arc sweep-flag="1" rx="5" ry="5" x="31" y="136.24" />
563
+ <line x="21" y="136.24" />
564
+ <arc sweep-flag="1" large-arc-flag="1" rx="5" ry="6.5" x="21" y="130.77" />
565
+ <close />
566
+ </path>
567
+ <fillstroke />
568
+ <stroke />
569
+
570
+ <rect x="40" y="29.32" w="67" h="49" />
571
+ <fillstroke />
572
+ <stroke />
573
+
574
+ <rect x="40" y="29.32" w="34" h="21" />
575
+ <fillstroke />
576
+ <stroke />
577
+
578
+ <rect x="60" y="57.32" w="24" h="14" />
579
+ <fillstroke />
580
+ <stroke />
581
+
582
+ <rect x="107" y="37.32" w="7" h="17" />
583
+ <fillstroke />
584
+ <stroke />
585
+
586
+ <ellipse x="85.0" y="37.32" w="9.0" h="13.0" />
587
+ <fillstroke />
588
+ <stroke />
589
+
590
+ <rect x="68" y="105.32" w="52" h="56" />
591
+ <fillstroke />
592
+ <stroke />
593
+
594
+ <rect x="126" y="105.32" w="52" h="56" />
595
+ <fillstroke />
596
+ <stroke />
597
+
598
+ <ellipse x="129.0" y="125.32" w="11.0" h="16.0" />
599
+ <fillstroke />
600
+ <stroke />
601
+
602
+ <ellipse x="146.5" y="125.32" w="11.0" h="16.0" />
603
+ <fillstroke />
604
+ <stroke />
605
+
606
+ <ellipse x="164.0" y="125.32" w="11.0" h="16.0" />
607
+ <fillstroke />
608
+ <stroke />
609
+
610
+ <path>
611
+ <move x="79" y="130.77" />
612
+ <line x="89" y="130.77" />
613
+ <arc sweep-flag="1" rx="5" ry="5" x="99" y="130.77" />
614
+ <line x="109" y="130.77" />
615
+ <arc sweep-flag="1" large-arc-flag="1" rx="5" ry="6.5" x="109" y="136.24" />
616
+ <line x="99" y="136.24" />
617
+ <arc sweep-flag="1" rx="5" ry="5" x="89" y="136.24" />
618
+ <line x="79" y="136.24" />
619
+ <arc sweep-flag="1" large-arc-flag="1" rx="5" ry="6.5" x="79" y="130.77" />
620
+ <close />
621
+ </path>
622
+ <fillstroke />
623
+ <stroke />
624
+
625
+ <rect x="10" y="105.32" w="52" h="56" />
626
+ <stroke />
627
+ </foreground>
628
+ </shape><shape h="40.0" w="73.0" aspect="variable" strokewidth="inherit" name="filter" displayName="filter">
629
+ <foreground>
630
+
631
+
632
+ <roundrect arcsize="6" x="3.13" y="0" w="65.75" h="22.4" />
633
+ <fillstroke />
634
+ <stroke />
635
+
636
+
637
+ <path>
638
+ <move x="72.0" y="15.360000000000003" />
639
+ <line x="67.39" y="28.000000000000004" />
640
+ <line x="4.609999999999999" y="27.999999999999996" />
641
+ <line x="0.0" y="15.359999999999996" />
642
+ <close />
643
+ </path>
644
+ <fillstroke />
645
+ <stroke />
646
+
647
+
648
+ <path>
649
+ <move x="60.39999999999999" y="28.16" />
650
+ <line x="57.78999999999999" y="40.0" />
651
+ <line x="15.029999999999994" y="39.99999999999999" />
652
+ <line x="12.419999999999995" y="28.159999999999993" />
653
+ <close />
654
+ </path>
655
+ <fillstroke />
656
+ <stroke />
657
+ </foreground>
658
+ </shape><shape h="61.0" w="81.0" aspect="variable" strokewidth="inherit" name="filter_dedusting" displayName="filter_dedusting"><foreground><rect x="0" y="0" w="81" h="60" /><fillstroke /><stroke /><path><move x="0.0" y="20.0" /><line x="81.0" y="20.0" /></path><fillstroke /><stroke /><path><move x="0.0" y="41.43" /><line x="81.0" y="41.43" /></path><fillstroke /><stroke /><path><move x="19.919999999999998" y="0.0" /><line x="19.920000000000005" y="60.0" /></path><fillstroke /><stroke /><path><move x="40.5" y="0.0" /><line x="40.5" y="60.0" /></path><fillstroke /><stroke /><path><move x="59.75" y="0.0" /><line x="59.75" y="60.0" /></path><fillstroke /><stroke /></foreground></shape><shape h="38.0" w="101.0" aspect="variable" strokewidth="inherit" name="fire" displayName="fire">
659
+ <foreground>
660
+ <path>
661
+ <move x="-0.41" y="14.7" />
662
+ <line x="16.83" y="3.99" />
663
+ <line x="14.01" y="10.8" />
664
+ <line x="42.85" y="-0.3" />
665
+ <line x="38.16" y="4.71" />
666
+ <line x="64.17" y="2.48" />
667
+ <line x="59.48" y="5.43" />
668
+ <line x="82.35" y="8.03" />
669
+ <line x="80.81" y="11.15" />
670
+ <line x="100.0" y="19.02" />
671
+ <line x="76.54" y="28.33" />
672
+ <line x="79.84" y="32.45" />
673
+ <line x="56.64" y="32.45" />
674
+ <line x="61.03" y="34.67" />
675
+ <line x="35.33" y="32.45" />
676
+ <line x="39.71" y="37.45" />
677
+ <line x="13.99" y="25.46" />
678
+ <line x="16.83" y="31.19" />
679
+ <line x="-0.41" y="26.35" />
680
+ <line x="-0.41" y="14.2" />
681
+ <close />
682
+ </path>
683
+ <fillstroke />
684
+ <stroke />
685
+ </foreground>
686
+ </shape><shape h="51.0" w="61.0" aspect="variable" strokewidth="inherit" name="gate_doubleout" displayName="gate_doubleout"><foreground><path><move x="33.41974488218369" y="19.977929565774033" /><line x="16.6368983545521" y="43.94631838166994" /><line x="4.112063597373407" y="35.17633466986245" /><line x="20.894910125005005" y="11.207945853966542" /><close /></path><fillstroke /><stroke /><path><move x="40.23116259018858" y="11.205490097887171" /><line x="57.01400911782019" y="35.17387891378307" /><line x="44.4891743606415" y="43.94386262559057" /><line x="27.706327833009894" y="19.975473809694666" /><close /></path><fillstroke /><stroke /><path><move x="18.4" y="0.0" /><line x="42.57" y="0.0" /><line x="42.57" y="15.11" /><line x="30.49" y="24.17" /><line x="18.4" y="15.11" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="25.0" w="25.0" aspect="variable" strokewidth="inherit" name="gate_valve" displayName="gate_valve"><foreground><rect x="0" y="0" w="24" h="24" /><fillstroke /><stroke /><path><move x="24.0" y="24.0" /><curve x1="0.0" y1="0.0" x2="0.0" x3="0.0" y2="0.0" y3="0.0" /></path><fillstroke /><stroke /></foreground></shape><shape h="194.0" w="191.0" aspect="variable" strokewidth="inherit" name="GraphiteDippingSuspension" displayName="GraphiteDippingSuspension">
687
+ <foreground>
688
+ <rect x="0" y="0" w="191" h="194" />
689
+ <fillstroke />
690
+ <stroke />
691
+
692
+ <path>
693
+ <move x="92" y="18" />
694
+ <line x="104" y="18" />
695
+ <line x="104" y="140" />
696
+ <line x="135" y="140" />
697
+ <line x="135" y="178" />
698
+ <line x="123" y="178" />
699
+ <line x="123" y="152" />
700
+ <line x="104" y="152" />
701
+ <line x="104" y="178" />
702
+ <line x="92" y="178" />
703
+ <line x="92" y="152" />
704
+ <line x="73" y="152" />
705
+ <line x="73" y="178" />
706
+ <line x="61" y="178" />
707
+ <line x="61" y="140" />
708
+ <line x="92" y="140" />
709
+ <close />
710
+ </path>
711
+ <fillstroke />
712
+ <stroke />
713
+
714
+ <ellipse x="149.0" y="78.0" w="14.0" h="9.0" />
715
+ <fillstroke />
716
+ <stroke />
717
+ <path>
718
+ <move x="112.26" y="81.49" />
719
+ <line x="151.73" y="78.88" />
720
+ </path>
721
+ <fillstroke />
722
+ <stroke />
723
+ <path>
724
+ <move x="112.89" y="90.34" />
725
+ <line x="149.77" y="80.29" />
726
+ </path>
727
+ <fillstroke />
728
+ <stroke />
729
+ <path>
730
+ <move x="116.68" y="97.38" />
731
+ <line x="149.21" y="81.19" />
732
+ </path>
733
+ <fillstroke />
734
+ <stroke />
735
+ <path>
736
+ <move x="124.51" y="100.73" />
737
+ <line x="149.32" y="81.63" />
738
+ </path>
739
+ <fillstroke />
740
+ <stroke />
741
+ <ellipse x="149.0" y="78.0" w="14.0" h="9.0" />
742
+ <fillstroke />
743
+ <stroke />
744
+ <ellipse x="32.0" y="78.0" w="14.0" h="9.0" />
745
+ <fillstroke />
746
+ <stroke />
747
+ <path>
748
+ <move x="45.27" y="80.12" />
749
+ <line x="84.0" y="81.0" />
750
+ </path>
751
+ <fillstroke />
752
+ <stroke />
753
+ <path>
754
+ <move x="45.86" y="81.4" />
755
+ <line x="85.0" y="90.0" />
756
+ </path>
757
+ <fillstroke />
758
+ <stroke />
759
+ <path>
760
+ <move x="46.0" y="82.5" />
761
+ <line x="72.34" y="100.06" />
762
+ </path>
763
+ <fillstroke />
764
+ <stroke />
765
+ <path>
766
+ <move x="45.86" y="81.63" />
767
+ <line x="83.0" y="98.0" />
768
+ </path>
769
+ <fillstroke />
770
+ <stroke />
771
+ <ellipse x="32.0" y="78.0" w="14.0" h="9.0" />
772
+ <fillstroke />
773
+ <stroke />
774
+ <path>
775
+ <move x="45.27" y="80.12" />
776
+ <line x="84.0" y="81.0" />
777
+ </path>
778
+ <fillstroke />
779
+ <stroke />
780
+ <path>
781
+ <move x="45.86" y="81.4" />
782
+ <line x="85.0" y="90.0" />
783
+ </path>
784
+ <fillstroke />
785
+ <stroke />
786
+ <path>
787
+ <move x="46.0" y="82.5" />
788
+ <line x="72.34" y="100.06" />
789
+ </path>
790
+ <fillstroke />
791
+ <stroke />
792
+ <path>
793
+ <move x="45.86" y="81.63" />
794
+ <line x="83.0" y="98.0" />
795
+ </path>
796
+ <fillstroke />
797
+ <stroke />
798
+ <path>
799
+ <move x="112.26" y="81.49" />
800
+ <line x="151.73" y="78.88" />
801
+ </path>
802
+ <fillstroke />
803
+ <stroke />
804
+ <path>
805
+ <move x="112.89" y="90.34" />
806
+ <line x="149.77" y="80.29" />
807
+ </path>
808
+ <fillstroke />
809
+ <stroke />
810
+ <path>
811
+ <move x="116.68" y="97.38" />
812
+ <line x="149.21" y="81.19" />
813
+ </path>
814
+ <fillstroke />
815
+ <stroke />
816
+ <path>
817
+ <move x="124.51" y="100.73" />
818
+ <line x="149.32" y="81.63" />
819
+ </path>
820
+ <fillstroke />
821
+ <stroke />
822
+ </foreground>
823
+ </shape><shape h="59.0" w="88.0" aspect="variable" strokewidth="inherit" name="grizzly" displayName="grizzly">
824
+ <foreground>
825
+ <path>
826
+ <move x="42.0" y="59.0" />
827
+ <line x="88.0" y="59" />
828
+ <line x="88.0" y="46.5" />
829
+ <line x="46.0" y="0.5" />
830
+ <line x="0.0" y="0.5" />
831
+ <line x="42.0" y="47.0" />
832
+ <line x="42.0" y="59.0" />
833
+ <close />
834
+ </path>
835
+ <fillstroke />
836
+ <stroke />
837
+ <path>
838
+ <move x="76.0" y="59.0" />
839
+ <line x="76.0" y="47.0" />
840
+ <line x="33.0" y="0.5" />
841
+ </path>
842
+ <stroke />
843
+ <path>
844
+ <move x="54.0" y="59.0" />
845
+ <line x="54.0" y="47.0" />
846
+ <line x="11.0" y="0.5" />
847
+ </path>
848
+ <stroke />
849
+ <path>
850
+ <move x="65.0" y="59.0" />
851
+ <line x="65.0" y="47.0" />
852
+ <line x="22.0" y="0.5" />
853
+ </path>
854
+ <stroke />
855
+ </foreground>
856
+ </shape><shape h="82.0" w="77.0" aspect="variable" strokewidth="inherit" name="hammer_mill" displayName="hammer_mill"><foreground><path><move x="-0.0026774517975631795" y="19.25891126772399" /><line x="38.34372329194891" y="19.096276708051086" /><line x="38.18108873227601" y="57.442677451797564" /><close /></path><fillstroke /><stroke /><roundrect arcsize="8.7" x="9.28" y="23.61" w="67" h="58" /><fillstroke /><stroke /><ellipse x="19.28" y="30.61" w="47.16" h="42.0" /><fillstroke /><stroke /></foreground></shape><shape h="38.0" w="56.0" aspect="variable" strokewidth="inherit" name="heater" displayName="heater">
857
+ <foreground>
858
+ <rect x="0" y="0" w="56" h="38" />
859
+ <fillstroke />
860
+ <stroke />
861
+ <path>
862
+ <move x="14.0" y="38.0" />
863
+ <line x="14.0" y="20.0" />
864
+ </path>
865
+ <stroke />
866
+ <path>
867
+ <move x="42.0" y="38.0" />
868
+ <line x="42.0" y="20.0" />
869
+ </path>
870
+ <stroke />
871
+ <path>
872
+ <move x="14.0" y="20.0" />
873
+ <curve x1="14.32" y1="0.0" x2="19.73" y2="19.5" x3="19.73" y3="19.5" />
874
+ </path>
875
+ <stroke />
876
+ <path>
877
+ <move x="19.73" y="19.5" />
878
+ <curve x1="22.91" y1="40.5" x2="25.45" y2="19.0" x3="25.45" y3="19.0" />
879
+ <curve x1="28.0" y1="0.5" x2="31.18" y2="19.5" x3="31.18" y3="19.5" />
880
+ </path>
881
+ <stroke />
882
+ <path>
883
+ <move x="31.18" y="19.5" />
884
+ <curve x1="34.05" y1="40.5" x2="37.55" y2="18.75" x3="37.55" y3="18.75" />
885
+ <curve x1="41.05" y1="0.0" x2="42.0" y2="20.0" x3="42.0" y3="20.0" />
886
+ </path>
887
+ <stroke />
888
+ </foreground>
889
+ </shape><shape h="51.0" w="50.0" aspect="variable" strokewidth="inherit" name="heat_exchanger" displayName="heat_exchanger">
890
+ <foreground>
891
+ <path>
892
+ <move x="25.0" y="0.0" />
893
+ <line x="50.0" y="25.0" />
894
+ <line x="25.0" y="50.0" />
895
+ <line x="0.0" y="25.0" />
896
+ <close />
897
+ </path>
898
+ <fillstroke />
899
+ <stroke />
900
+ <path>
901
+ <move x="24.95" y="50.0" />
902
+ <line x="25.0" y="32.0" />
903
+ <line x="24.95" y="0.0" />
904
+ </path>
905
+ <fillcolor color="#696969" />
906
+ <fillstroke />
907
+ <stroke />
908
+ <path>
909
+ <move x="28.5" y="40.5" />
910
+ <line x="25.0" y="48.5" />
911
+ <line x="21.5" y="40.5" />
912
+ <close />
913
+ </path>
914
+ <fillstroke />
915
+ <stroke />
916
+ <path>
917
+ <move x="21.5" y="9.5" />
918
+ <line x="25.0" y="1.5" />
919
+ <line x="28.5" y="9.5" />
920
+ <close />
921
+ </path>
922
+ <fillstroke />
923
+ <stroke />
924
+ </foreground>
925
+ </shape><shape h="119.0" w="60.0" aspect="variable" strokewidth="inherit" name="ligature_feeding_machine" displayName="ligature_feeding_machine"><foreground><path><move x="60.0" y="0.0" /><line x="60.0" y="118.0" /><line x="3.552713678800501e-15" y="118.0" /><line x="-3.552713678800501e-15" y="0.0" /><close /></path><fillstroke /><stroke /><ellipse x="3.5" y="4.0" w="53.0" h="53.0" /><stroke /><ellipse x="3.5" y="60.0" w="53.0" h="53.0" /><stroke /></foreground></shape><shape h="113.0" w="71.0" aspect="variable" strokewidth="inherit" name="loeshe_mill" displayName="loeshe_mill"><foreground><rect x="0" y="4" w="71" h="34" /><fillstroke /><stroke /><rect x="5" y="38" w="61" h="71" /><fillstroke /><stroke /><rect x="0" y="109" w="71" h="4" /><fillstroke /><stroke /><rect x="19.5" y="0" w="32" h="4" /><fillstroke /><stroke /></foreground></shape><shape h="101.0" w="58.0" aspect="variable" strokewidth="inherit" name="LVAZ" displayName="LVAZ"><foreground><path><move x="0.05" y="8.0" /><line x="56.05" y="8.0" /><line x="56.05" y="80.0" /><line x="28.05" y="100.0" /><line x="0.05" y="80.0" /><close /></path><fillstroke /><stroke /><ellipse x="0.03999999999999915" y="0.0" w="56.0" h="17.0" /><fillstroke /><stroke /><path><move x="0.04" y="80.0" /><line x="56.04" y="80.0" /></path><fillstroke /><stroke /></foreground></shape><shape h="171.0" w="178.0" aspect="variable" strokewidth="inherit" name="MachineCleaningRod" displayName="MachineCleaningRod">
926
+ <foreground>
927
+ <rect x="0.26" y="0.24" w="178" h="171" />
928
+ <fillstroke />
929
+ <stroke />
930
+ <path>
931
+ <move x="85.03" y="49.6" />
932
+ <line x="93.49" y="49.6" />
933
+ <line x="93.49" y="134.42" />
934
+ <line x="115.36" y="134.42" />
935
+ <line x="115.36" y="161.54" />
936
+ <line x="106.89" y="161.54" />
937
+ <line x="106.89" y="142.76" />
938
+ <line x="93.49" y="142.76" />
939
+ <line x="93.49" y="161.54" />
940
+ <line x="85.03" y="161.54" />
941
+ <line x="85.03" y="142.76" />
942
+ <line x="71.63" y="142.76" />
943
+ <line x="71.63" y="161.54" />
944
+ <line x="63.17" y="161.54" />
945
+ <line x="63.17" y="134.42" />
946
+ <line x="85.03" y="134.42" />
947
+ <close />
948
+ </path>
949
+ <fillstroke />
950
+ <stroke />
951
+ <rect x="13.77" y="24.92" w="150.97" h="14.1" />
952
+ <fillstroke />
953
+ <stroke />
954
+ <rect x="40.8" y="39.02" w="9.32" h="93.43" />
955
+ <fillstroke />
956
+ <stroke />
957
+ <path>
958
+ <move x="25.89" y="125.4" />
959
+ <line x="42.66" y="125.4" />
960
+ </path>
961
+ <fillstroke />
962
+ <stroke />
963
+ <path>
964
+ <move x="25.89" y="120.99" />
965
+ <line x="42.66" y="120.99" />
966
+ </path>
967
+ <fillstroke />
968
+ <stroke />
969
+ <path>
970
+ <move x="25.89" y="116.59" />
971
+ <line x="42.66" y="116.59" />
972
+ </path>
973
+ <fillstroke />
974
+ <stroke />
975
+ <path>
976
+ <move x="25.89" y="112.18" />
977
+ <line x="42.66" y="112.18" />
978
+ </path>
979
+ <fillstroke />
980
+ <stroke />
981
+ <path>
982
+ <move x="25.89" y="107.77" />
983
+ <line x="42.66" y="107.77" />
984
+ </path>
985
+ <fillstroke />
986
+ <stroke />
987
+ <path>
988
+ <move x="25.89" y="103.37" />
989
+ <line x="42.66" y="103.37" />
990
+ </path>
991
+ <fillstroke />
992
+ <stroke />
993
+ <path>
994
+ <move x="25.89" y="98.96" />
995
+ <line x="42.66" y="98.96" />
996
+ </path>
997
+ <fillstroke />
998
+ <stroke />
999
+ <path>
1000
+ <move x="25.89" y="94.55" />
1001
+ <line x="42.66" y="94.55" />
1002
+ </path>
1003
+ <fillstroke />
1004
+ <stroke />
1005
+ <path>
1006
+ <move x="25.89" y="90.14" />
1007
+ <line x="42.66" y="90.14" />
1008
+ </path>
1009
+ <fillstroke />
1010
+ <stroke />
1011
+ <path>
1012
+ <move x="25.89" y="85.74" />
1013
+ <line x="42.66" y="85.74" />
1014
+ </path>
1015
+ <fillstroke />
1016
+ <stroke />
1017
+ <path>
1018
+ <move x="25.89" y="81.33" />
1019
+ <line x="42.66" y="81.33" />
1020
+ </path>
1021
+ <fillstroke />
1022
+ <stroke />
1023
+ <path>
1024
+ <move x="25.89" y="76.92" />
1025
+ <line x="42.66" y="76.92" />
1026
+ </path>
1027
+ <fillstroke />
1028
+ <stroke />
1029
+ <path>
1030
+ <move x="25.89" y="72.52" />
1031
+ <line x="42.66" y="72.52" />
1032
+ </path>
1033
+ <fillstroke />
1034
+ <stroke />
1035
+ <path>
1036
+ <move x="25.89" y="68.11" />
1037
+ <line x="42.66" y="68.11" />
1038
+ </path>
1039
+ <fillstroke />
1040
+ <stroke />
1041
+ <path>
1042
+ <move x="25.89" y="63.7" />
1043
+ <line x="42.66" y="63.7" />
1044
+ </path>
1045
+ <fillstroke />
1046
+ <stroke />
1047
+ <path>
1048
+ <move x="25.89" y="59.29" />
1049
+ <line x="42.66" y="59.29" />
1050
+ </path>
1051
+ <fillstroke />
1052
+ <stroke />
1053
+ <path>
1054
+ <move x="25.89" y="54.89" />
1055
+ <line x="42.66" y="54.89" />
1056
+ </path>
1057
+ <fillstroke />
1058
+ <stroke />
1059
+ <path>
1060
+ <move x="47.32" y="119.23" />
1061
+ <line x="64.1" y="119.23" />
1062
+ </path>
1063
+ <fillstroke />
1064
+ <stroke />
1065
+ <path>
1066
+ <move x="47.32" y="114.82" />
1067
+ <line x="64.1" y="114.82" />
1068
+ </path>
1069
+ <fillstroke />
1070
+ <stroke />
1071
+ <path>
1072
+ <move x="47.32" y="110.42" />
1073
+ <line x="64.1" y="110.42" />
1074
+ </path>
1075
+ <fillstroke />
1076
+ <stroke />
1077
+ <path>
1078
+ <move x="47.32" y="106.01" />
1079
+ <line x="64.1" y="106.01" />
1080
+ </path>
1081
+ <fillstroke />
1082
+ <stroke />
1083
+ <path>
1084
+ <move x="47.32" y="101.6" />
1085
+ <line x="64.1" y="101.6" />
1086
+ </path>
1087
+ <fillstroke />
1088
+ <stroke />
1089
+ <path>
1090
+ <move x="47.32" y="97.2" />
1091
+ <line x="64.1" y="97.2" />
1092
+ </path>
1093
+ <fillstroke />
1094
+ <stroke />
1095
+ <path>
1096
+ <move x="47.32" y="92.79" />
1097
+ <line x="64.1" y="92.79" />
1098
+ </path>
1099
+ <fillstroke />
1100
+ <stroke />
1101
+ <path>
1102
+ <move x="47.32" y="88.38" />
1103
+ <line x="64.1" y="88.38" />
1104
+ </path>
1105
+ <fillstroke />
1106
+ <stroke />
1107
+ <path>
1108
+ <move x="47.32" y="83.97" />
1109
+ <line x="64.1" y="83.97" />
1110
+ </path>
1111
+ <fillstroke />
1112
+ <stroke />
1113
+ <path>
1114
+ <move x="47.32" y="79.57" />
1115
+ <line x="64.1" y="79.57" />
1116
+ </path>
1117
+ <fillstroke />
1118
+ <stroke />
1119
+ <path>
1120
+ <move x="47.32" y="75.16" />
1121
+ <line x="64.1" y="75.16" />
1122
+ </path>
1123
+ <fillstroke />
1124
+ <stroke />
1125
+ <path>
1126
+ <move x="47.32" y="70.75" />
1127
+ <line x="64.1" y="70.75" />
1128
+ </path>
1129
+ <fillstroke />
1130
+ <stroke />
1131
+ <path>
1132
+ <move x="47.32" y="66.35" />
1133
+ <line x="64.1" y="66.35" />
1134
+ </path>
1135
+ <fillstroke />
1136
+ <stroke />
1137
+ <path>
1138
+ <move x="47.32" y="61.94" />
1139
+ <line x="64.1" y="61.94" />
1140
+ </path>
1141
+ <fillstroke />
1142
+ <stroke />
1143
+ <path>
1144
+ <move x="47.32" y="57.53" />
1145
+ <line x="64.1" y="57.53" />
1146
+ </path>
1147
+ <fillstroke />
1148
+ <stroke />
1149
+ <path>
1150
+ <move x="47.32" y="128.05" />
1151
+ <line x="64.1" y="128.05" />
1152
+ </path>
1153
+ <fillstroke />
1154
+ <stroke />
1155
+ <path>
1156
+ <move x="47.32" y="123.64" />
1157
+ <line x="64.1" y="123.64" />
1158
+ </path>
1159
+ <fillstroke />
1160
+ <stroke />
1161
+ <rect x="128.4" y="39.02" w="9.32" h="93.43" />
1162
+ <fillstroke />
1163
+ <stroke />
1164
+ <path>
1165
+ <move x="113.49" y="125.4" />
1166
+ <line x="130.27" y="125.4" />
1167
+ </path>
1168
+ <fillstroke />
1169
+ <stroke />
1170
+ <path>
1171
+ <move x="113.49" y="120.99" />
1172
+ <line x="130.27" y="120.99" />
1173
+ </path>
1174
+ <fillstroke />
1175
+ <stroke />
1176
+ <path>
1177
+ <move x="113.49" y="116.59" />
1178
+ <line x="130.27" y="116.59" />
1179
+ </path>
1180
+ <fillstroke />
1181
+ <stroke />
1182
+ <path>
1183
+ <move x="113.49" y="112.18" />
1184
+ <line x="130.27" y="112.18" />
1185
+ </path>
1186
+ <fillstroke />
1187
+ <stroke />
1188
+ <path>
1189
+ <move x="113.49" y="107.77" />
1190
+ <line x="130.27" y="107.77" />
1191
+ </path>
1192
+ <fillstroke />
1193
+ <stroke />
1194
+ <path>
1195
+ <move x="113.49" y="103.37" />
1196
+ <line x="130.27" y="103.37" />
1197
+ </path>
1198
+ <fillstroke />
1199
+ <stroke />
1200
+ <path>
1201
+ <move x="113.49" y="98.96" />
1202
+ <line x="130.27" y="98.96" />
1203
+ </path>
1204
+ <fillstroke />
1205
+ <stroke />
1206
+ <path>
1207
+ <move x="113.49" y="94.55" />
1208
+ <line x="130.27" y="94.55" />
1209
+ </path>
1210
+ <fillstroke />
1211
+ <stroke />
1212
+ <path>
1213
+ <move x="113.49" y="90.14" />
1214
+ <line x="130.27" y="90.14" />
1215
+ </path>
1216
+ <fillstroke />
1217
+ <stroke />
1218
+ <path>
1219
+ <move x="113.49" y="85.74" />
1220
+ <line x="130.27" y="85.74" />
1221
+ </path>
1222
+ <fillstroke />
1223
+ <stroke />
1224
+ <path>
1225
+ <move x="113.49" y="81.33" />
1226
+ <line x="130.27" y="81.33" />
1227
+ </path>
1228
+ <fillstroke />
1229
+ <stroke />
1230
+ <path>
1231
+ <move x="113.49" y="76.92" />
1232
+ <line x="130.27" y="76.92" />
1233
+ </path>
1234
+ <fillstroke />
1235
+ <stroke />
1236
+ <path>
1237
+ <move x="113.49" y="72.52" />
1238
+ <line x="130.27" y="72.52" />
1239
+ </path>
1240
+ <fillstroke />
1241
+ <stroke />
1242
+ <path>
1243
+ <move x="113.49" y="68.11" />
1244
+ <line x="130.27" y="68.11" />
1245
+ </path>
1246
+ <fillstroke />
1247
+ <stroke />
1248
+ <path>
1249
+ <move x="113.49" y="63.7" />
1250
+ <line x="130.27" y="63.7" />
1251
+ </path>
1252
+ <fillstroke />
1253
+ <stroke />
1254
+ <path>
1255
+ <move x="113.49" y="59.29" />
1256
+ <line x="130.27" y="59.29" />
1257
+ </path>
1258
+ <fillstroke />
1259
+ <stroke />
1260
+ <path>
1261
+ <move x="113.49" y="54.89" />
1262
+ <line x="130.27" y="54.89" />
1263
+ </path>
1264
+ <fillstroke />
1265
+ <stroke />
1266
+ <path>
1267
+ <move x="134.92" y="119.23" />
1268
+ <line x="151.7" y="119.23" />
1269
+ </path>
1270
+ <fillstroke />
1271
+ <stroke />
1272
+ <path>
1273
+ <move x="134.92" y="114.82" />
1274
+ <line x="151.7" y="114.82" />
1275
+ </path>
1276
+ <fillstroke />
1277
+ <stroke />
1278
+ <path>
1279
+ <move x="134.92" y="110.42" />
1280
+ <line x="151.7" y="110.42" />
1281
+ </path>
1282
+ <fillstroke />
1283
+ <stroke />
1284
+ <path>
1285
+ <move x="134.92" y="106.01" />
1286
+ <line x="151.7" y="106.01" />
1287
+ </path>
1288
+ <fillstroke />
1289
+ <stroke />
1290
+ <path>
1291
+ <move x="134.92" y="101.6" />
1292
+ <line x="151.7" y="101.6" />
1293
+ </path>
1294
+ <fillstroke />
1295
+ <stroke />
1296
+ <path>
1297
+ <move x="134.92" y="97.2" />
1298
+ <line x="151.7" y="97.2" />
1299
+ </path>
1300
+ <fillstroke />
1301
+ <stroke />
1302
+ <path>
1303
+ <move x="134.92" y="92.79" />
1304
+ <line x="151.7" y="92.79" />
1305
+ </path>
1306
+ <fillstroke />
1307
+ <stroke />
1308
+ <path>
1309
+ <move x="134.92" y="88.38" />
1310
+ <line x="151.7" y="88.38" />
1311
+ </path>
1312
+ <fillstroke />
1313
+ <stroke />
1314
+ <path>
1315
+ <move x="134.92" y="83.97" />
1316
+ <line x="151.7" y="83.97" />
1317
+ </path>
1318
+ <fillstroke />
1319
+ <stroke />
1320
+ <path>
1321
+ <move x="134.92" y="79.57" />
1322
+ <line x="151.7" y="79.57" />
1323
+ </path>
1324
+ <fillstroke />
1325
+ <stroke />
1326
+ <path>
1327
+ <move x="134.92" y="75.16" />
1328
+ <line x="151.7" y="75.16" />
1329
+ </path>
1330
+ <fillstroke />
1331
+ <stroke />
1332
+ <path>
1333
+ <move x="134.92" y="70.75" />
1334
+ <line x="151.7" y="70.75" />
1335
+ </path>
1336
+ <fillstroke />
1337
+ <stroke />
1338
+ <path>
1339
+ <move x="134.92" y="66.35" />
1340
+ <line x="151.7" y="66.35" />
1341
+ </path>
1342
+ <fillstroke />
1343
+ <stroke />
1344
+ <path>
1345
+ <move x="134.92" y="61.94" />
1346
+ <line x="151.7" y="61.94" />
1347
+ </path>
1348
+ <fillstroke />
1349
+ <stroke />
1350
+ <path>
1351
+ <move x="134.92" y="57.53" />
1352
+ <line x="151.7" y="57.53" />
1353
+ </path>
1354
+ <fillstroke />
1355
+ <stroke />
1356
+ <path>
1357
+ <move x="134.92" y="128.05" />
1358
+ <line x="151.7" y="128.05" />
1359
+ </path>
1360
+ <fillstroke />
1361
+ <stroke />
1362
+ <path>
1363
+ <move x="134.92" y="123.64" />
1364
+ <line x="151.7" y="123.64" />
1365
+ </path>
1366
+ <fillstroke />
1367
+ <stroke />
1368
+ </foreground>
1369
+ </shape><shape h="116.0" w="114.0" aspect="variable" strokewidth="inherit" name="MachineFinalCleaningStubEnd" displayName="MachineFinalCleaningStubEnd">
1370
+ <foreground>
1371
+ <rect x="0" y="0" w="114" h="116" />
1372
+ <fillstroke />
1373
+ <stroke />
1374
+ <path>
1375
+ <move x="8.63" y="99.06" />
1376
+ <line x="13.0" y="106.0" />
1377
+ <line x="26.0" y="106.0" />
1378
+ <line x="26.0" y="21.0" />
1379
+ <line x="26.0" y="31.0" />
1380
+ <line x="28.0" y="39.0" />
1381
+ <line x="31.0" y="39.0" />
1382
+ <line x="32.0" y="32.0" />
1383
+ <line x="34.0" y="39.0" />
1384
+ <line x="36.0" y="39.0" />
1385
+ <line x="39.0" y="32.0" />
1386
+ <line x="39.0" y="24.0" />
1387
+ <line x="25.0" y="10.0" />
1388
+ <line x="14.0" y="10.0" />
1389
+ <line x="14.0" y="99.0" />
1390
+ <line x="8.52" y="98.95" />
1391
+ <close />
1392
+ </path>
1393
+ <stroke />
1394
+ <path>
1395
+ <move x="104.11" y="99.06" />
1396
+ <line x="101.0" y="106.0" />
1397
+ <line x="87.0" y="106.0" />
1398
+ <line x="87.0" y="21.0" />
1399
+ <line x="87.0" y="32.0" />
1400
+ <line x="85.0" y="39.0" />
1401
+ <line x="83.0" y="39.0" />
1402
+ <line x="81.0" y="33.0" />
1403
+ <line x="79.0" y="39.0" />
1404
+ <line x="77.0" y="39.0" />
1405
+ <line x="74.0" y="33.0" />
1406
+ <line x="74.0" y="25.0" />
1407
+ <line x="88.0" y="10.0" />
1408
+ <line x="99.0" y="10.0" />
1409
+ <line x="99.0" y="99.0" />
1410
+ <line x="104.0" y="98.95" />
1411
+ <close />
1412
+ </path>
1413
+ <stroke />
1414
+ <roundrect arcsize="40" x="41.21" y="91.1" w="30.59" h="5" />
1415
+ <stroke />
1416
+ <ellipse x="33.0" y="73.0" w="3.9999999999999996" h="2.9999999999999996" />
1417
+ <stroke />
1418
+ <ellipse x="33.0" y="90.0" w="3.9999999999999996" h="2.9999999999999996" />
1419
+ <stroke />
1420
+ <ellipse x="76.0" y="73.0" w="3.9999999999999996" h="2.9999999999999996" />
1421
+ <stroke />
1422
+ <ellipse x="76.0" y="90.0" w="3.9999999999999996" h="2.9999999999999996" />
1423
+ <stroke />
1424
+ <path>
1425
+ <move x="37.0" y="74.5" />
1426
+ <line x="48.0" y="74.5" />
1427
+ </path>
1428
+ <stroke />
1429
+ <path>
1430
+ <move x="37.0" y="91.45" />
1431
+ <line x="48.57" y="91.45" />
1432
+ </path>
1433
+ <stroke />
1434
+ <path>
1435
+ <move x="37.35096464368121" y="91.12723079889126" />
1436
+ <line x="48.06903535631879" y="88.65276920110874" />
1437
+ </path>
1438
+ <stroke />
1439
+ <path>
1440
+ <move x="37.384663756410546" y="91.18467039993075" />
1441
+ <line x="46.395336243589455" y="84.87532960006925" />
1442
+ </path>
1443
+ <stroke />
1444
+ <path>
1445
+ <move x="76.30000000000001" y="75.11" />
1446
+ <line x="64.73" y="75.11" />
1447
+ </path>
1448
+ <stroke />
1449
+ <path>
1450
+ <move x="75.92903535631879" y="75.43276920110874" />
1451
+ <line x="65.2109646436812" y="77.90723079889126" />
1452
+ </path>
1453
+ <stroke />
1454
+ <path>
1455
+ <move x="75.89533624358945" y="75.37532960006925" />
1456
+ <line x="66.88466375641055" y="81.68467039993075" />
1457
+ </path>
1458
+ <stroke />
1459
+ <path>
1460
+ <move x="75.46469881599037" y="91.54527180210906" />
1461
+ <line x="65.98710966356674" y="84.90899243352746" />
1462
+ </path>
1463
+ <stroke />
1464
+ <path>
1465
+ <move x="74.99951120011734" y="91.60033626378753" />
1466
+ <line x="64.80048879988267" y="87.47966373621249" />
1467
+ </path>
1468
+ <stroke />
1469
+ <path>
1470
+ <move x="76.0" y="91.54" />
1471
+ <line x="64.0" y="91.54" />
1472
+ </path>
1473
+ <stroke />
1474
+ <path>
1475
+ <move x="37.000488799882675" y="74.43966373621248" />
1476
+ <line x="47.19951120011733" y="78.56033626378752" />
1477
+ </path>
1478
+ <stroke />
1479
+ <path>
1480
+ <move x="36.994663756410546" y="74.82532960006925" />
1481
+ <line x="46.005336243589454" y="81.13467039993076" />
1482
+ </path>
1483
+ <stroke />
1484
+ <path>
1485
+ <move x="54.47" y="28.0" />
1486
+ <line x="59.0" y="28.0" />
1487
+ <line x="59.0" y="76.0" />
1488
+ <line x="69.0" y="76.0" />
1489
+ <line x="69.0" y="92.0" />
1490
+ <line x="65.0" y="92.0" />
1491
+ <line x="65.0" y="81.0" />
1492
+ <line x="59.0" y="81.0" />
1493
+ <line x="59.0" y="92.0" />
1494
+ <line x="54.0" y="92.0" />
1495
+ <line x="54.0" y="81.0" />
1496
+ <line x="48.0" y="81.0" />
1497
+ <line x="48.0" y="92.0" />
1498
+ <line x="44.0" y="92.0" />
1499
+ <line x="44.0" y="76.0" />
1500
+ <line x="54.0" y="76.0" />
1501
+ <line x="54.0" y="28.0" />
1502
+ <close />
1503
+ </path>
1504
+ <stroke />
1505
+ </foreground>
1506
+ </shape><shape h="172.0" w="107.0" aspect="variable" strokewidth="inherit" name="MachineInductionDryingNipple" displayName="MachineInductionDryingNipple">
1507
+ <foreground>
1508
+ <rect x="0" y="0" w="107" h="172" />
1509
+ <fillstroke />
1510
+ <stroke />
1511
+ <path>
1512
+ <move x="18.82" y="150.72" />
1513
+ <line x="26.0" y="163.5" />
1514
+ <line x="31.0" y="158.5" />
1515
+ <line x="39.0" y="170.0" />
1516
+ </path>
1517
+ <stroke />
1518
+ <path>
1519
+ <move x="51.0" y="150.0" />
1520
+ <line x="58.58" y="163.5" />
1521
+ <line x="63.58" y="158.5" />
1522
+ <line x="71.58" y="170.0" />
1523
+ </path>
1524
+ <stroke />
1525
+ <path>
1526
+ <move x="83.4" y="150.66" />
1527
+ <line x="90.58" y="163.5" />
1528
+ <line x="95.58" y="158.5" />
1529
+ <line x="103.58" y="170.0" />
1530
+ </path>
1531
+ <stroke />
1532
+ <ellipse x="76.0" y="144.0" w="10.999999999999998" h="6.999999999999999" />
1533
+ <stroke />
1534
+ <ellipse x="42.0" y="144.0" w="10.999999999999998" h="6.999999999999999" />
1535
+ <fillstroke />
1536
+ <stroke />
1537
+ <ellipse x="11.0" y="144.0" w="10.999999999999998" h="6.999999999999999" />
1538
+ <stroke />
1539
+ <path>
1540
+ <move x="42.89" y="4.04" />
1541
+ <line x="43.0" y="99.0" />
1542
+ <line x="12.0" y="99.0" />
1543
+ <line x="12.0" y="137.0" />
1544
+ <line x="24.0" y="137.0" />
1545
+ <line x="24.0" y="111.0" />
1546
+ <line x="43.0" y="111.0" />
1547
+ <line x="43.0" y="138.0" />
1548
+ <line x="55.0" y="138.0" />
1549
+ <line x="55.0" y="111.0" />
1550
+ <line x="74.0" y="111.0" />
1551
+ <line x="74.0" y="137.0" />
1552
+ <line x="86.0" y="137.0" />
1553
+ <line x="86.0" y="99.0" />
1554
+ <line x="55.0" y="99.0" />
1555
+ <line x="55.0" y="4.0" />
1556
+ <line x="43.0" y="4.0" />
1557
+ <close />
1558
+ </path>
1559
+ <stroke />
1560
+ </foreground>
1561
+ </shape><shape h="110.0" w="119.0" aspect="variable" strokewidth="inherit" name="MachineMovablePouringCrucible" displayName="MachineMovablePouringCrucible"><foreground><rect x="0" y="0" w="119" h="110" /><fillstroke /><stroke /><rect x="13.44" y="26.19" w="59.5" h="59.37" /><fillstroke /><stroke /><path><move x="94.57" y="26.1" /><curve x1="87.01" y1="46.56" x2="90.85" y2="54.71" x3="90.85" y3="54.71" /><curve x1="94.69" y1="62.86" x2="98.85" y2="55.0" x3="98.85" y3="55.0" /><curve x1="103.01" y1="47.14" x2="94.57" y2="26.24" x3="94.57" y3="26.24" /></path><stroke /><path><move x="73.0" y="35.0" /><line x="92.0" y="35.0" /></path><stroke /><path><move x="73.1" y="73.07" /><line x="106.0" y="73.0" /><line x="106.0" y="35.0" /><line x="98.0" y="35.0" /></path><stroke /></foreground></shape><shape h="116.0" w="114.0" aspect="variable" strokewidth="inherit" name="Machine_Cleaning_Nipple" displayName="Machine_Cleaning_Nipple"><foreground><rect x="45.5" y="76.5" w="25" h="4.77" /><fillstroke /><stroke /><rect x="45.5" y="81.27" w="4.05" h="10.34" /><fillstroke /><stroke /><rect x="66.5" y="81" w="4" h="10.6" /><fillstroke /><stroke /><rect x="55.97" y="28" w="4.05" h="64" /><fillstroke /><stroke /><rect x="46" y="78.48" w="3.1" h="10.68" /><fillstroke /><stroke /><rect x="66.92" y="77" w="3.1" h="10.68" /><fillstroke /><stroke /><path><move x="63.370000000000005" y="76.99" /><line x="63.370000000000005" y="80.75999999999999" /><line x="53.03" y="80.76" /><line x="53.03" y="76.99" /><close /></path><fillstroke /><stroke /><rect x="56.46" y="73" w="3.1" h="10.68" /><fillstroke /><stroke /><rect x="0" y="0" w="114" h="116" /><fillstroke /><stroke /><path><move x="9.63" y="99.06" /><line x="14.0" y="106.0" /><line x="27.0" y="106.0" /><line x="27.0" y="21.0" /><line x="27.0" y="31.0" /><line x="29.0" y="39.0" /><line x="32.0" y="39.0" /><line x="33.0" y="32.0" /><line x="35.0" y="39.0" /><line x="37.0" y="39.0" /><line x="40.0" y="32.0" /><line x="40.0" y="24.0" /><line x="26.0" y="10.0" /><line x="15.0" y="10.0" /><line x="15.0" y="99.0" /><line x="9.52" y="98.95" /></path><fillstroke /><stroke /><path><move x="105.11" y="99.06" /><line x="102.0" y="106.0" /><line x="88.0" y="106.0" /><line x="88.0" y="21.0" /><line x="88.0" y="32.0" /><line x="86.0" y="39.0" /><line x="84.0" y="39.0" /><line x="82.0" y="33.0" /><line x="80.0" y="39.0" /><line x="78.0" y="39.0" /><line x="75.0" y="33.0" /><line x="75.0" y="25.0" /><line x="89.0" y="10.0" /><line x="100.0" y="10.0" /><line x="100.0" y="99.0" /><line x="105.0" y="98.95" /></path><fillstroke /><stroke /><ellipse x="34.0" y="73.0" w="4.0" h="3.0" /><fillstroke /><stroke /><ellipse x="34.0" y="90.0" w="4.0" h="3.0" /><fillstroke /><stroke /><ellipse x="77.0" y="73.0" w="4.0" h="3.0" /><fillstroke /><stroke /><ellipse x="77.0" y="90.0" w="4.0" h="3.0" /><fillstroke /><stroke /><path><move x="38.0" y="74.5" /><line x="49.0" y="74.5" /></path><fillstroke /><stroke /><path><move x="38.0" y="91.45" /><line x="49.57" y="91.45" /></path><fillstroke /><stroke /><path><move x="38.35096464368121" y="91.12723079889126" /><line x="49.06903535631879" y="88.65276920110874" /></path><fillstroke /><stroke /><path><move x="38.384663756410546" y="91.18467039993075" /><line x="47.395336243589455" y="84.87532960006925" /></path><fillstroke /><stroke /><path><move x="77.30000000000001" y="75.11" /><line x="65.73" y="75.11" /></path><fillstroke /><stroke /><path><move x="76.92903535631879" y="75.43276920110874" /><line x="66.2109646436812" y="77.90723079889126" /></path><fillstroke /><stroke /><path><move x="76.89533624358945" y="75.37532960006925" /><line x="67.88466375641055" y="81.68467039993075" /></path><fillstroke /><stroke /><path><move x="76.46469881599037" y="91.54527180210906" /><line x="66.98710966356674" y="84.90899243352746" /></path><fillstroke /><stroke /><path><move x="75.99951120011734" y="91.60033626378753" /><line x="65.80048879988267" y="87.47966373621249" /></path><fillstroke /><stroke /><path><move x="77.0" y="91.54" /><line x="65.0" y="91.54" /></path><fillstroke /><stroke /><path><move x="37.994663756410546" y="74.82532960006925" /><line x="47.005336243589454" y="81.13467039993076" /></path><fillstroke /><stroke /><path><move x="38.000488799882675" y="74.43966373621248" /><line x="48.19951120011733" y="78.56033626378752" /></path><fillstroke /><stroke /></foreground></shape><shape h="90.0" w="84.0" aspect="variable" strokewidth="inherit" name="magnet" displayName="magnet">
1562
+ <foreground>
1563
+ <rect x="0" y="0" w="83.08" h="90" />
1564
+ <fillstroke />
1565
+ <stroke />
1566
+
1567
+ <path>
1568
+ <move x="12.69" y="73.84" />
1569
+ <line x="12.69" y="40.38" />
1570
+ <arc sweep-flag="1" rx="22.5" ry="22.5" x="71.54" y="40.38" />
1571
+ <line x="71.54" y="73.84" />
1572
+ <line x="60" y="73.84" />
1573
+ <line x="60" y="40.38" />
1574
+ <arc rx="17.9" ry="17.9" x="24.23" y="40.38" />
1575
+ <line x="24.23" y="73.84" />
1576
+
1577
+ <close />
1578
+ </path>
1579
+ <fillstroke />
1580
+ <stroke />
1581
+
1582
+ </foreground>
1583
+ </shape><shape h="84.0" w="70.0" aspect="variable" strokewidth="inherit" name="MB" displayName="MB">
1584
+ <foreground>
1585
+
1586
+
1587
+ <path>
1588
+ <move x="7.0" y="0.43" />
1589
+ <line x="7.0" y="82.43" />
1590
+ <line x="58.0" y="82.43" />
1591
+ <line x="58.0" y="62.0" />
1592
+ </path>
1593
+ <stroke />
1594
+
1595
+
1596
+ <path>
1597
+ <move x="58.0" y="63.0" />
1598
+ <curve x1="58.0" y1="61.0" x2="60.0" y2="61.0" x3="62.0" y3="61.0" />
1599
+ <curve x1="64.0" y1="61.0" x2="66.0" y2="61.0" x3="65.5" y3="63.0" />
1600
+ <curve x1="65.0" y1="65.0" x2="64.5" y2="66.0" x3="63.0" y3="67.0" />
1601
+ <curve x1="61.0" y1="68.5" x2="58.0" y2="70.0" x3="55.0" y3="69.0" />
1602
+ <curve x1="52.0" y1="68.0" x2="51.0" y2="66.0" x3="50.5" y3="64.0" />
1603
+ <curve x1="50.0" y1="62.0" x2="50.5" y2="60.0" x3="51.5" y3="57.5" />
1604
+ <curve x1="53.0" y1="55.0" x2="55.5" y2="55.0" x3="58.0" y3="55.0" />
1605
+ <curve x1="58.13" y1="53.46" x2="58.13" x3="58.13" y2="53.46" y3="53.46" />
1606
+ </path>
1607
+ <stroke />
1608
+
1609
+
1610
+ <path>
1611
+ <move x="58.0" y="0.0" />
1612
+ <line x="58.0" y="54.0" />
1613
+ </path>
1614
+ <stroke />
1615
+
1616
+
1617
+ <rect x="0" y="37.5" w="14" h="14" />
1618
+ <fillstroke />
1619
+ <stroke />
1620
+
1621
+ <path>
1622
+ <move x="7.0" y="14.33" />
1623
+ <line x="16.86" y="14.29" />
1624
+ <line x="16.88" y="0.0" />
1625
+ </path>
1626
+ <stroke />
1627
+
1628
+
1629
+ <path>
1630
+ <move x="58.0" y="14.33" />
1631
+ <line x="67.86" y="14.29" />
1632
+ <line x="67.88" y="0.0" />
1633
+ </path>
1634
+ <stroke />
1635
+ </foreground>
1636
+ </shape><shape h="48.0" w="154.0" aspect="variable" strokewidth="inherit" name="MIKA" displayName="MIKA"><foreground><roundrect arcsize="5.85" x="5" y="4.64" w="141" h="39" /><fillstroke /><stroke /><rect x="0" y="1" w="15" h="47" /><fillstroke /><stroke /><rect x="58" y="2" w="5" h="45" /><fillstroke /><stroke /><rect x="58" y="7" w="5" h="4" /><fillstroke /><stroke /><rect x="58" y="16" w="5" h="4" /><fillstroke /><stroke /><rect x="58" y="25" w="5" h="4" /><fillstroke /><stroke /><rect x="58" y="34" w="5" h="4" /><fillstroke /><stroke /><rect x="58" y="43" w="5" h="4" /><fillstroke /><stroke /><rect x="139" y="0" w="15" h="48" /><fillstroke /><stroke /></foreground></shape><shape h="71.0" w="90.0" aspect="variable" strokewidth="inherit" name="mill" displayName="mill">
1637
+ <foreground>
1638
+
1639
+
1640
+ <roundrect arcsize="5" x="0" y="17" w="90" h="38" />
1641
+ <fillstroke />
1642
+ <stroke />
1643
+
1644
+
1645
+ <roundrect arcsize="6.27" x="14" y="7.5" w="71" h="57" />
1646
+ <fillstroke />
1647
+ <stroke />
1648
+
1649
+
1650
+ <rect x="70" y="0.5" w="11" h="71" />
1651
+ <fillstroke />
1652
+ <stroke />
1653
+ </foreground>
1654
+ </shape><shape h="222.0" w="301.0" aspect="variable" strokewidth="inherit" name="mixer" displayName="mixer"><foreground><path><move x="40.18" y="0.0" /><line x="260.18" y="0.0" /><line x="300.18" y="32.0" /><line x="300.18" y="76.0" /><line x="0.18" y="76.0" /><line x="0.18" y="32.0" /><close /></path><fillstroke /><stroke /><rect x="16.88" y="75.9" w="266.59" h="92.32" /><fillstroke /><stroke /><rect x="1.9" y="163.43" w="296.55" h="58" /><fillstroke /><stroke /></foreground></shape><shape h="101.0" w="48.0" aspect="variable" strokewidth="inherit" name="mixer_2" displayName="mixer_2"><foreground><rect x="0" y="10.11" w="48" h="79.78" /><fillstroke /><stroke /><ellipse x="0.0" y="-0.0023595505617990398" w="48.0" h="20.224719101123597" /><fillstroke /><stroke /><ellipse x="0.0" y="79.7776404494382" w="48.0" h="20.224719101123597" /><fillstroke /><stroke /><rect x="0" y="10.11" w="48" h="79.78" /><fillstroke /><stroke /><path><move x="23.999999999999996" y="0.0" /><line x="24.000000000000004" y="76.4" /></path><fillstroke /><stroke /><path><move x="11.76" y="70.79" /><line x="24.0" y="75.84" /><line x="11.76" y="80.9" /><close /><move x="36.24" y="70.79" /><line x="24.0" y="75.84" /><line x="36.24" y="80.9" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="69.0" w="47.0" aspect="variable" strokewidth="inherit" name="PKN" displayName="PKN"><foreground><rect x="0" y="18.83" w="47" h="39.17" /><fillstroke /><stroke /><path><move x="0.0" y="18.83" /><line x="3.61" y="7.0" /><line x="43.19" y="7.0" /><line x="46.8" y="18.83" /><close /></path><fillstroke /><stroke /><path><move x="46.81" y="58.0" /><line x="42.2" y="68.6" /><line x="4.810000000000002" y="68.6" /><line x="0.20000000000000284" y="58.0" /><close /></path><fillstroke /><stroke /><rect x="15" y="1.77" w="16" h="5.23" /><fillstroke /><stroke /></foreground></shape><shape h="120.0" w="117.0" aspect="variable" strokewidth="inherit" name="PressButtStripper" displayName="PressButtStripper">
1655
+ <foreground>
1656
+ <rect x="0" y="0" w="116.32" h="120" />
1657
+ <fillstroke />
1658
+ <stroke />
1659
+ <rect x="34.41" y="26.5" w="41" h="59" />
1660
+ <fillstroke />
1661
+ <stroke />
1662
+ <rect x="7.16" y="30.5" w="24" h="51" />
1663
+ <fillstroke />
1664
+ <stroke />
1665
+ <rect x="7.16" y="49.5" w="11.5" h="14" />
1666
+ <fillstroke />
1667
+ <stroke />
1668
+
1669
+ <path>
1670
+ <move x="79.16" y="12.5" />
1671
+ <line x="72.16" y="12.5" />
1672
+ <line x="69.16" y="19.5" />
1673
+ <line x="38.16" y="23.5" />
1674
+ <line x="79.16" y="23.5" />
1675
+ </path>
1676
+ <fillstroke />
1677
+ <stroke />
1678
+ <path>
1679
+ <move x="79.16" y="88.5" />
1680
+ <line x="72.16" y="88.5" />
1681
+ <line x="38.16" y="88.5" />
1682
+ <line x="70.16" y="93.5" />
1683
+ <line x="72.16" y="99.5" />
1684
+ <line x="79.16" y="99.5" />
1685
+ </path>
1686
+ <fillstroke />
1687
+ <stroke />
1688
+ <rect x="79.16" y="7.5" w="24" h="99" />
1689
+ <fillstroke />
1690
+ <stroke />
1691
+ <rect x="53.47" y="62.5" w="2.88" h="6" />
1692
+ <fillstroke />
1693
+ <stroke />
1694
+ <ellipse x="48.41" y="68.5" w="13.0" h="13.0" />
1695
+ <fillstroke />
1696
+ <stroke />
1697
+ <rect x="53.47" y="43.5" w="2.88" h="6" />
1698
+ <fillstroke />
1699
+ <stroke />
1700
+ <ellipse x="48.41" y="49.5" w="13.0" h="13.0" />
1701
+ <fillstroke />
1702
+ <stroke />
1703
+ <ellipse x="48.41" y="30.5" w="13.0" h="13.0" />
1704
+ <fillstroke />
1705
+ <stroke />
1706
+ <rect x="91.66" y="48.5" w="11.5" h="14" />
1707
+ <fillstroke />
1708
+ <stroke />
1709
+ </foreground>
1710
+ </shape><shape h="120.0" w="117.0" aspect="variable" strokewidth="inherit" name="PressRemovalCastIronThimble" displayName="PressRemovalCastIronThimble">
1711
+ <foreground>
1712
+ <rect x="0" y="-0.25" w="116.32" h="120" />
1713
+ <fillstroke />
1714
+ <stroke />
1715
+ <rect x="18.84" y="17" w="25" h="85.5" />
1716
+ <fillstroke />
1717
+ <stroke />
1718
+ <rect x="43.84" y="17" w="60" h="16" />
1719
+ <fillstroke />
1720
+ <stroke />
1721
+ <rect x="43.84" y="86.5" w="60" h="16" />
1722
+ <fillstroke />
1723
+ <stroke />
1724
+ <path>
1725
+ <move x="20.84" y="89.0" />
1726
+ <line x="41.84" y="89.0" />
1727
+ <line x="41.84" y="82.0" />
1728
+ <line x="28.84" y="73.0" />
1729
+ <line x="41.84" y="65.0" />
1730
+ <line x="41.84" y="55.0" />
1731
+ <line x="27.84" y="46.0" />
1732
+ <line x="41.84" y="39.0" />
1733
+ <line x="41.84" y="30.0" />
1734
+ <line x="18.84" y="30.0" />
1735
+ <line x="18.84" y="89.0" />
1736
+ <line x="20.84" y="89.0" />
1737
+ </path>
1738
+ <fillstroke />
1739
+ <stroke />
1740
+ </foreground>
1741
+ </shape><shape h="33.0" w="36.0" aspect="variable" strokewidth="inherit" name="pump" displayName="pump"><foreground><rect x="16.7" y="-0.42" w="19.3" h="9.86" /><fillstroke /><stroke /><ellipse x="-0.004999999999999005" y="-0.4250000000000007" w="32.85" h="32.85" /><fillstroke /><stroke /></foreground></shape><shape h="33.0" w="37.0" aspect="variable" strokewidth="inherit" name="pump_left" displayName="pump_left">
1742
+ <foreground>
1743
+ <path>
1744
+ <move x="5.0" y="10" />
1745
+ <line x="0.0" y="10" />
1746
+ <line x="0.0" y="0" />
1747
+ <line x="20.0" y="0" />
1748
+ </path>
1749
+ <fillstroke />
1750
+ <stroke />
1751
+ <ellipse x="3.5" y="0" w="33.0" h="33.0" />
1752
+ <fillstroke />
1753
+ <stroke />
1754
+ </foreground>
1755
+ </shape><shape h="66.0" w="120.0" aspect="variable" strokewidth="inherit" name="refrigerator" displayName="refrigerator">
1756
+ <foreground>
1757
+
1758
+
1759
+ <rect x="0" y="6" w="120" h="60" />
1760
+ <fillstroke />
1761
+ <stroke />
1762
+
1763
+
1764
+ <path>
1765
+ <move x="0" y="6" />
1766
+ <line x="2" y="0" />
1767
+ <line x="118" y="0" />
1768
+ <line x="120" y="6" />
1769
+ <close />
1770
+ </path>
1771
+ <fillstroke />
1772
+ <stroke />
1773
+ </foreground>
1774
+ </shape><shape h="60.0" w="109.0" aspect="variable" strokewidth="inherit" name="roller_crusher" displayName="roller_crusher"><foreground><rect x="-0.07" y="0" w="109" h="60" /><fillstroke /><stroke /><ellipse x="7.93" y="3.5" w="53.0" h="53.0" /><fillstroke /><stroke /><ellipse x="48.93000000000001" y="3.5" w="53.0" h="53.0" /><fillstroke /><stroke /><ellipse x="8.0" y="3.5" w="53.0" h="53.0" /><stroke /></foreground></shape><shape h="55.0" w="174.0" aspect="variable" strokewidth="inherit" name="rotary_kiln" displayName="rotary_kiln"><foreground><roundrect arcsize="5.85" x="0" y="4.64" w="174" h="39" /><fillstroke /><stroke /><rect x="10" y="1" w="15" h="54" /><fillstroke /><stroke /><rect x="68" y="2" w="5" h="45" /><fillstroke /><stroke /><rect x="68" y="7" w="5" h="4" /><fillstroke /><stroke /><rect x="68" y="16" w="5" h="4" /><fillstroke /><stroke /><rect x="68" y="25" w="5" h="4" /><fillstroke /><stroke /><rect x="68" y="34" w="5" h="4" /><fillstroke /><stroke /><rect x="68" y="43" w="5" h="4" /><fillstroke /><stroke /><rect x="149" y="0" w="15" h="54" /><fillstroke /><stroke /></foreground></shape><shape h="200.0" w="61.0" aspect="variable" strokewidth="inherit" name="scrubber" displayName="scrubber">
1775
+ <foreground>
1776
+
1777
+
1778
+ <path>
1779
+ <move x="21.99" y="0" />
1780
+ <line x="39.01" y="0" />
1781
+ <line x="39.01" y="39" />
1782
+ <line x="61" y="61.97" />
1783
+ <line x="61" y="200" />
1784
+ <line x="0" y="200" />
1785
+ <line x="0" y="61.97" />
1786
+ <line x="21.99" y="39" />
1787
+ <close />
1788
+ </path>
1789
+ <fillstroke />
1790
+ <stroke />
1791
+ </foreground>
1792
+ </shape><shape h="118.0" w="32.0" aspect="variable" strokewidth="inherit" name="self-evaporator" displayName="self-evaporator"><foreground><ellipse x="0.0" y="0.0" w="32.0" h="24.16" /><fillstroke /><stroke /><ellipse x="0.0" y="92.0" w="32.0" h="26.0" /><fillstroke /><stroke /><rect x="0" y="15.86" w="32" h="88.28" /><fillstroke /><stroke /><rect x="0" y="12.21" w="32" h="3.65" /><fillstroke /><stroke /></foreground></shape><shape h="29.0" w="79.0" aspect="variable" strokewidth="inherit" name="separator" displayName="separator">
1793
+ <foreground>
1794
+
1795
+
1796
+ <rect x="60.83" y="15.54" w="18.17" h="13.46" />
1797
+ <fillstroke />
1798
+ <stroke />
1799
+
1800
+
1801
+ <path>
1802
+ <move x="0.0" y="0.0" />
1803
+ <line x="70.31" y="14.5" />
1804
+ <line x="0.0" y="29.0" />
1805
+ <close />
1806
+ </path>
1807
+ <fillstroke />
1808
+ <stroke />
1809
+
1810
+
1811
+ <rect x="0" y="0" w="79" h="16.57" />
1812
+ <fillstroke />
1813
+ <stroke />
1814
+ </foreground>
1815
+ </shape><shape h="70.0" w="234.0" aspect="variable" strokewidth="inherit" name="Shnek" displayName="Shnek">
1816
+ <foreground>
1817
+ <rect x="0" y="0" w="17" h="70" />
1818
+ <fillstroke />
1819
+ <stroke />
1820
+ <rect x="17" y="9.5" w="165" h="51" />
1821
+ <fillstroke />
1822
+ <stroke />
1823
+ <rect x="182" y="0" w="17" h="70" />
1824
+ <fillstroke />
1825
+ <stroke />
1826
+ <rect x="199" y="25" w="35" h="20" />
1827
+ <fillstroke />
1828
+ <stroke />
1829
+ <rect x="17" y="19.3" w="163" h="5.2" />
1830
+ <fillstroke />
1831
+ <stroke />
1832
+ <path>
1833
+ <move x="22.7" y="24.44" />
1834
+ <curve x1="24.93" y1="28.47" x2="27.15" y2="32.5" x3="31.62" y3="23.56" />
1835
+ <curve x1="34.39" y1="18.03" x2="37.15" y2="12.5" x3="39.43" y3="15.77" />
1836
+ <curve x1="41.72" y1="19.05" x2="41.72" x3="41.72" y2="19.05" y3="19.05" />
1837
+ </path>
1838
+ <stroke />
1839
+ <path>
1840
+ <move x="45.0" y="24.44" />
1841
+ <curve x1="47.22" y1="28.47" x2="49.44" y2="32.5" x3="53.91" y3="23.56" />
1842
+ <curve x1="56.68" y1="18.03" x2="59.44" y2="12.5" x3="61.72" y3="15.77" />
1843
+ <curve x1="64.01" y1="19.05" x2="64.01" x3="64.01" y2="19.05" y3="19.05" />
1844
+ </path>
1845
+ <stroke />
1846
+ <path>
1847
+ <move x="67.0" y="24.44" />
1848
+ <curve x1="69.22" y1="28.47" x2="71.44" y2="32.5" x3="75.91" y3="23.56" />
1849
+ <curve x1="78.68" y1="18.03" x2="81.44" y2="12.5" x3="83.72" y3="15.77" />
1850
+ <curve x1="86.01" y1="19.05" x2="86.01" x3="86.01" y2="19.05" y3="19.05" />
1851
+ </path>
1852
+ <stroke />
1853
+ <path>
1854
+ <move x="89.0" y="24.44" />
1855
+ <curve x1="91.22" y1="28.47" x2="93.44" y2="32.5" x3="97.91" y3="23.56" />
1856
+ <curve x1="100.68" y1="18.03" x2="103.44" y2="12.5" x3="105.72" y3="15.77" />
1857
+ <curve x1="108.01" y1="19.05" x2="108.01" x3="108.01" y2="19.05" y3="19.05" />
1858
+ </path>
1859
+ <stroke />
1860
+ <path>
1861
+ <move x="112.0" y="24.44" />
1862
+ <curve x1="114.23" y1="28.47" x2="116.45" y2="32.5" x3="120.92" y3="23.56" />
1863
+ <curve x1="123.69" y1="18.03" x2="126.45" y2="12.5" x3="128.73" y3="15.77" />
1864
+ <curve x1="131.02" y1="19.05" x2="131.02" x3="131.02" y2="19.05" y3="19.05" />
1865
+ </path>
1866
+ <stroke />
1867
+ <path>
1868
+ <move x="134.0" y="24.44" />
1869
+ <curve x1="136.22" y1="28.47" x2="138.44" y2="32.5" x3="142.91" y3="23.56" />
1870
+ <curve x1="145.68" y1="18.03" x2="148.44" y2="12.5" x3="150.72" y3="15.77" />
1871
+ <curve x1="153.01" y1="19.05" x2="153.01" x3="153.01" y2="19.05" y3="19.05" />
1872
+ </path>
1873
+ <stroke />
1874
+ <path>
1875
+ <move x="156.0" y="24.44" />
1876
+ <curve x1="158.23" y1="28.47" x2="160.45" y2="32.5" x3="164.92" y3="23.56" />
1877
+ <curve x1="167.69" y1="18.03" x2="170.45" y2="12.5" x3="172.73" y3="15.77" />
1878
+ <curve x1="175.02" y1="19.05" x2="175.02" x3="175.02" y2="19.05" y3="19.05" />
1879
+ </path>
1880
+ <stroke />
1881
+ <rect x="17" y="44.3" w="163" h="5.2" />
1882
+ <fillstroke />
1883
+ <stroke />
1884
+ <path>
1885
+ <move x="22.7" y="49.44" />
1886
+ <curve x1="24.93" y1="53.47" x2="27.15" y2="57.5" x3="31.62" y3="48.56" />
1887
+ <curve x1="34.39" y1="43.03" x2="37.15" y2="37.5" x3="39.43" y3="40.77" />
1888
+ <curve x1="41.72" y1="44.05" x2="41.72" x3="41.72" y2="44.05" y3="44.05" />
1889
+ </path>
1890
+ <stroke />
1891
+ <path>
1892
+ <move x="45.0" y="49.44" />
1893
+ <curve x1="47.22" y1="53.47" x2="49.44" y2="57.5" x3="53.91" y3="48.56" />
1894
+ <curve x1="56.68" y1="43.03" x2="59.44" y2="37.5" x3="61.72" y3="40.77" />
1895
+ <curve x1="64.01" y1="44.05" x2="64.01" x3="64.01" y2="44.05" y3="44.05" />
1896
+ </path>
1897
+ <stroke />
1898
+ <path>
1899
+ <move x="67.0" y="49.44" />
1900
+ <curve x1="69.22" y1="53.47" x2="71.44" y2="57.5" x3="75.91" y3="48.56" />
1901
+ <curve x1="78.68" y1="43.03" x2="81.44" y2="37.5" x3="83.72" y3="40.77" />
1902
+ <curve x1="86.01" y1="44.05" x2="86.01" x3="86.01" y2="44.05" y3="44.05" />
1903
+ </path>
1904
+ <stroke />
1905
+ <path>
1906
+ <move x="88.99" y="49.44" />
1907
+ <curve x1="91.22" y1="53.47" x2="93.44" y2="57.5" x3="97.91" y3="48.56" />
1908
+ <curve x1="100.68" y1="43.03" x2="103.44" y2="37.5" x3="105.72" y3="40.77" />
1909
+ <curve x1="108.01" y1="44.05" x2="108.01" x3="108.01" y2="44.05" y3="44.05" />
1910
+ </path>
1911
+ <stroke />
1912
+ <path>
1913
+ <move x="112.01" y="49.44" />
1914
+ <curve x1="114.23" y1="53.47" x2="116.45" y2="57.5" x3="120.92" y3="48.56" />
1915
+ <curve x1="123.69" y1="43.03" x2="126.45" y2="37.5" x3="128.73" y3="40.77" />
1916
+ <curve x1="131.02" y1="44.05" x2="131.02" x3="131.02" y2="44.05" y3="44.05" />
1917
+ </path>
1918
+ <stroke />
1919
+ <path>
1920
+ <move x="133.99" y="49.44" />
1921
+ <curve x1="136.22" y1="53.47" x2="138.44" y2="57.5" x3="142.91" y3="48.56" />
1922
+ <curve x1="145.68" y1="43.03" x2="148.44" y2="37.5" x3="150.72" y3="40.77" />
1923
+ <curve x1="153.01" y1="44.05" x2="153.01" x3="153.01" y2="44.05" y3="44.05" />
1924
+ </path>
1925
+ <stroke />
1926
+ <path>
1927
+ <move x="156.01" y="49.44" />
1928
+ <curve x1="158.23" y1="53.47" x2="160.45" y2="57.5" x3="164.92" y3="48.56" />
1929
+ <curve x1="167.69" y1="43.03" x2="170.45" y2="37.5" x3="172.73" y3="40.77" />
1930
+ <curve x1="175.02" y1="44.05" x2="175.02" x3="175.02" y2="44.05" y3="44.05" />
1931
+ </path>
1932
+ <stroke />
1933
+ </foreground>
1934
+ </shape><shape h="106.0" w="81.0" aspect="variable" strokewidth="inherit" name="silo" displayName="silo"><foreground><path><move x="0.0" y="0.0" /><line x="80.0" y="0.0" /><line x="80.0" y="87.0" /><line x="40.0" y="106.0" /><line x="0.0" y="87.0" /><close /></path><fillstroke /><stroke /><path><move x="0.0" y="87.0" /><line x="80.0" y="87.0" /></path><fillstroke /><stroke /></foreground></shape><shape h="28.0" w="186.0" aspect="variable" strokewidth="inherit" name="steam_line_section" displayName="steam_line_section"><foreground><rect x="0" y="7.23" w="185" h="13.77" /><fillstroke /><stroke /><rect x="6" y="0" w="8" h="28" /><fillstroke /><stroke /><rect x="169" y="0" w="8" h="28" /><fillstroke /><stroke /></foreground></shape><shape h="163.0" w="107.0" aspect="variable" strokewidth="inherit" name="stop_valve" displayName="stop_valve">
1935
+ <foreground>
1936
+ <rect x="38" y="51" w="31" h="19" />
1937
+ <fillstroke />
1938
+ <stroke />
1939
+ <rect x="38" y="51" w="31" h="19" />
1940
+ <fillstroke />
1941
+ <stroke />
1942
+ <rect x="44" y="0" w="19" h="144" />
1943
+ <fillstroke />
1944
+ <stroke />
1945
+ <path>
1946
+ <move x="0" y="144" />
1947
+ <line x="107" y="144" />
1948
+ <line x="107" y="163" />
1949
+ <line x="94" y="163" />
1950
+ <line x="94" y="157" />
1951
+ <line x="76" y="157" />
1952
+ <line x="76" y="163" />
1953
+ <line x="63" y="163" />
1954
+ <line x="63" y="157" />
1955
+ <line x="44" y="157" />
1956
+ <line x="44" y="163" />
1957
+ <line x="31" y="163" />
1958
+ <line x="31" y="157" />
1959
+ <line x="13" y="157" />
1960
+ <line x="13" y="163" />
1961
+ <line x="0" y="163" />
1962
+ <close />
1963
+ </path>
1964
+ <fillstroke />
1965
+ <stroke />
1966
+ </foreground>
1967
+ </shape><shape h="160.0" w="62.0" aspect="variable" strokewidth="inherit" name="supercharger" displayName="supercharger"><foreground><path><move x="0.07" y="112.16" /><line x="59.94" y="112.02" /></path><fillstroke /><stroke /><roundrect arcsize="7.17" x="0.24" y="89.76" w="59.76" h="70.24" /><fillstroke /><stroke /><rect x="10.53" y="26.93" w="39.18" h="47.61" /><fillstroke /><stroke /><roundrect arcsize="2.15" x="5.03" y="0" w="50.2" h="26.93" /><fillstroke /><stroke /><rect x="5.52" y="37.46" w="49.22" h="4.68" /><fillstroke /><stroke /><path><move x="53.39" y="116.28999999999999" /><line x="53.39" y="154.53" /></path><fillstroke /><stroke /><path><move x="48.98" y="116.28999999999999" /><line x="48.98" y="154.53" /></path><fillstroke /><stroke /><path><move x="44.57" y="116.28999999999999" /><line x="44.57" y="154.53" /></path><fillstroke /><stroke /><path><move x="40.16" y="116.28999999999999" /><line x="40.16" y="154.54" /></path><fillstroke /><stroke /><path><move x="35.76" y="116.28" /><line x="35.76" y="154.53" /></path><fillstroke /><stroke /><path><move x="31.35" y="116.28" /><line x="31.35" y="154.53" /></path><fillstroke /><stroke /><path><move x="26.94" y="116.28999999999999" /><line x="26.94" y="154.53" /></path><fillstroke /><stroke /><path><move x="22.53" y="116.28999999999999" /><line x="22.53" y="154.53" /></path><fillstroke /><stroke /><path><move x="18.12" y="116.28999999999999" /><line x="18.12" y="154.53" /></path><fillstroke /><stroke /><path><move x="13.709999999999999" y="116.28999999999999" /><line x="13.710000000000003" y="154.54" /></path><fillstroke /><stroke /><path><move x="9.309999999999999" y="116.28" /><line x="9.310000000000002" y="154.53" /></path><fillstroke /><stroke /><path><move x="4.8999999999999995" y="116.28999999999999" /><line x="4.900000000000001" y="154.53" /></path><fillstroke /><stroke /><path><move x="0.07" y="112.16" /><line x="59.94" y="112.02" /></path><fillstroke /><stroke /><rect x="27.18" y="74.54" w="5.88" h="37.46" /><fillstroke /><stroke /></foreground></shape><shape h="61.0" w="120.0" aspect="variable" strokewidth="inherit" name="tank" displayName="tank"><foreground><roundrect arcsize="9" x="0" y="0" w="120" h="60" /><fillstroke /><stroke /></foreground></shape><shape h="22.0" w="80.0" aspect="variable" strokewidth="inherit" name="thickener" displayName="thickener"><foreground><path><move x="0.0" y="0.0" /><line x="80.0" y="0.0" /><line x="80.0" y="4.58" /><line x="40.0" y="22.0" /><line x="0.0" y="4.58" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="123.0" w="111.0" aspect="variable" strokewidth="inherit" name="transformers_auxiliary" displayName="transformers_auxiliary">
1968
+ <foreground>
1969
+ <ellipse x="85.69999999999999" y="97.19999999999999" w="24.8" h="24.8" />
1970
+ <fillstroke />
1971
+ <stroke />
1972
+ <path>
1973
+ <move x="93.3" y="114.39999999999999" />
1974
+ <line x="98.1" y="104.79999999999998" />
1975
+ <line x="102.9" y="114.39999999999999" />
1976
+ <close />
1977
+ </path>
1978
+ <fillstroke />
1979
+ <stroke />
1980
+ <ellipse x="64.69999999999999" y="97.19999999999999" w="24.8" h="24.8" />
1981
+ <fillstroke />
1982
+ <stroke />
1983
+ <path>
1984
+ <move x="72.3" y="114.39999999999999" />
1985
+ <line x="77.1" y="104.79999999999998" />
1986
+ <line x="81.9" y="114.39999999999999" />
1987
+ <close />
1988
+ </path>
1989
+ <fillstroke />
1990
+ <stroke />
1991
+ <ellipse x="74.69999999999999" y="79.0" w="24.8" h="24.8" />
1992
+ <fillstroke />
1993
+ <stroke />
1994
+ <path>
1995
+ <move x="82.3" y="95.6" />
1996
+ <line x="87.1" y="85.99999999999999" />
1997
+ <line x="91.89999999999999" y="95.6" />
1998
+ <close />
1999
+ </path>
2000
+ <fillstroke />
2001
+ <stroke />
2002
+ <ellipse x="64.69999999999999" y="97.19999999999999" w="24.8" h="24.8" />
2003
+
2004
+ <stroke />
2005
+ <path>
2006
+ <move x="87.1" y="79.0" />
2007
+ <line x="87.11" y="2.0" />
2008
+ <line x="87.06" y="0.53" />
2009
+ </path>
2010
+ <fillstroke />
2011
+ <stroke />
2012
+ <path>
2013
+ <move x="87.5" y="21.0" />
2014
+ <line x="105.56" y="21.0" />
2015
+ <line x="105.56" y="0.0" />
2016
+ </path>
2017
+
2018
+ <stroke />
2019
+ <path>
2020
+ <move x="12.1" y="79.0" />
2021
+ <line x="12.11" y="24.0" />
2022
+ <line x="12.06" y="0.53" />
2023
+ </path>
2024
+
2025
+ <stroke />
2026
+ <path>
2027
+ <move x="23.099999999999998" y="54.0" />
2028
+ <line x="1.099999999999996" y="54.0" />
2029
+ <line x="1.0999999999999996" y="35.0" />
2030
+ <line x="23.1" y="35.0" />
2031
+ <close />
2032
+ </path>
2033
+ <fillstroke />
2034
+ <stroke />
2035
+ <path>
2036
+ <move x="12.46" y="21.0" />
2037
+ <line x="30.44" y="21.0" />
2038
+ <line x="30.44" y="0.0" />
2039
+ </path>
2040
+
2041
+ <stroke />
2042
+ <path>
2043
+ <move x="98.1" y="54.0" />
2044
+ <line x="76.1" y="54.0" />
2045
+ <line x="76.1" y="35.0" />
2046
+ <line x="98.1" y="35.0" />
2047
+ <close />
2048
+ </path>
2049
+ <fillstroke />
2050
+ <stroke />
2051
+ <ellipse x="-0.4299999999999997" y="96.89999999999999" w="24.8" h="24.8" />
2052
+ <fillstroke />
2053
+ <stroke />
2054
+ <ellipse x="-0.3000000000000007" y="79.19999999999999" w="24.8" h="24.8" />
2055
+ <fillstroke />
2056
+ <stroke />
2057
+ <ellipse x="-0.4299999999999997" y="96.89999999999999" w="24.8" h="24.8" />
2058
+
2059
+ <stroke />
2060
+ <ellipse x="85.69999999999999" y="97.19999999999999" w="24.8" h="24.8" />
2061
+
2062
+ <stroke />
2063
+ </foreground>
2064
+ </shape><shape h="134.0" w="126.0" aspect="variable" strokewidth="inherit" name="transformer_communication" displayName="transformer_communication">
2065
+ <foreground>
2066
+ <rect x="12.4" y="78" w="22" h="19" />
2067
+ <fillstroke />
2068
+ <stroke />
2069
+ <ellipse x="0.0" y="9.999999999999998" w="24.8" h="24.8" />
2070
+ <fillstroke />
2071
+ <stroke />
2072
+ <path>
2073
+ <move x="7.600000000000004" y="27.2" />
2074
+ <line x="12.4" y="17.6" />
2075
+ <line x="17.200000000000003" y="27.2" />
2076
+ <close />
2077
+ </path>
2078
+ <fillstroke />
2079
+ <stroke />
2080
+ <ellipse x="21.0" y="9.999999999999998" w="24.8" h="24.8" />
2081
+ <fillstroke />
2082
+ <stroke />
2083
+ <path>
2084
+ <move x="28.6" y="27.199999999999996" />
2085
+ <line x="33.4" y="17.599999999999994" />
2086
+ <line x="38.2" y="27.199999999999996" />
2087
+ <close />
2088
+ </path>
2089
+ <fillstroke />
2090
+ <stroke />
2091
+ <ellipse x="10.999999999999998" y="28.200000000000003" w="24.8" h="24.8" />
2092
+ <fillstroke />
2093
+ <stroke />
2094
+ <path>
2095
+ <move x="23.005898384862245" y="40.1" />
2096
+ <line x="29.934101615137752" y="44.1" />
2097
+ </path>
2098
+ <fillstroke />
2099
+ <stroke />
2100
+ <path>
2101
+ <move x="23.2" y="32.2" />
2102
+ <line x="23.2" y="40.2" />
2103
+ </path>
2104
+ <fillstroke />
2105
+ <stroke />
2106
+ <path>
2107
+ <move x="23.664101615137753" y="40.1" />
2108
+ <line x="16.735898384862246" y="44.1" />
2109
+ </path>
2110
+ <fillstroke />
2111
+ <stroke />
2112
+ <ellipse x="0.0" y="9.999999999999998" w="24.8" h="24.8" />
2113
+
2114
+ <stroke />
2115
+ <path>
2116
+ <move x="23.4" y="53.0" />
2117
+ <line x="23.44" y="130.0" />
2118
+ <line x="23.45" y="131.47" />
2119
+ </path>
2120
+
2121
+ <stroke />
2122
+ <path>
2123
+ <move x="23.0" y="111.0" />
2124
+ <line x="5.0" y="111.0" />
2125
+ <line x="5.0" y="132.0" />
2126
+ </path>
2127
+
2128
+ <stroke />
2129
+ <ellipse x="96.0" y="10.999999999999998" w="24.8" h="24.8" />
2130
+ <fillstroke />
2131
+ <stroke />
2132
+ <path>
2133
+ <move x="103.60000000000001" y="28.20000000000001" />
2134
+ <line x="108.4" y="18.6" />
2135
+ <line x="113.2" y="28.20000000000001" />
2136
+ <close />
2137
+ </path>
2138
+ <fillstroke />
2139
+ <stroke />
2140
+ <ellipse x="75.0" y="10.999999999999998" w="24.8" h="24.8" />
2141
+ <fillstroke />
2142
+ <stroke />
2143
+ <path>
2144
+ <move x="87.00589838486225" y="22.9" />
2145
+ <line x="93.93410161513775" y="26.9" />
2146
+ </path>
2147
+ <fillstroke />
2148
+ <stroke />
2149
+ <path>
2150
+ <move x="87.2" y="15.0" />
2151
+ <line x="87.2" y="23.0" />
2152
+ </path>
2153
+ <fillstroke />
2154
+ <stroke />
2155
+ <path>
2156
+ <move x="87.66410161513775" y="22.9" />
2157
+ <line x="80.73589838486225" y="26.9" />
2158
+ </path>
2159
+ <fillstroke />
2160
+ <stroke />
2161
+ <ellipse x="86.0" y="28.0" w="24.8" h="24.8" />
2162
+ <fillstroke />
2163
+ <stroke />
2164
+ <path>
2165
+ <move x="98.00589838486225" y="39.9" />
2166
+ <line x="104.93410161513775" y="43.9" />
2167
+ </path>
2168
+ <fillstroke />
2169
+ <stroke />
2170
+ <path>
2171
+ <move x="98.2" y="32.0" />
2172
+ <line x="98.2" y="40.0" />
2173
+ </path>
2174
+ <fillstroke />
2175
+ <stroke />
2176
+ <path>
2177
+ <move x="98.66410161513775" y="39.9" />
2178
+ <line x="91.73589838486225" y="43.9" />
2179
+ </path>
2180
+
2181
+ <stroke />
2182
+ <ellipse x="75.0" y="10.999999999999998" w="24.8" h="24.8" />
2183
+
2184
+ <stroke />
2185
+ <path>
2186
+ <move x="98.4" y="53.0" />
2187
+ <line x="98.44" y="130.0" />
2188
+ <line x="98.44" y="131.47" />
2189
+ </path>
2190
+
2191
+ <stroke />
2192
+ <rect x="87.4" y="78" w="22" h="19" />
2193
+ <fillstroke />
2194
+ <stroke />
2195
+ <path>
2196
+ <move x="98.04" y="111.0" />
2197
+ <line x="80.0" y="111.0" />
2198
+ <line x="80.0" y="132.0" />
2199
+ </path>
2200
+
2201
+ <stroke />
2202
+ <path>
2203
+ <move x="23.0" y="64.0" />
2204
+ <line x="46.0" y="64.0" />
2205
+ </path>
2206
+ <fillstroke />
2207
+ <stroke />
2208
+ <path>
2209
+ <move x="45.5" y="63.5" />
2210
+ <line x="45.5" y="53.5" />
2211
+ </path>
2212
+ <fillstroke />
2213
+ <stroke />
2214
+ <path>
2215
+ <move x="98.0" y="63.5" />
2216
+ <line x="121.0" y="63.5" />
2217
+ </path>
2218
+ <fillstroke />
2219
+ <stroke />
2220
+ <path>
2221
+ <move x="120.5" y="63.0" />
2222
+ <line x="120.5" y="53.0" />
2223
+ </path>
2224
+ <fillstroke />
2225
+ <stroke />
2226
+ <path>
2227
+ <move x="33.4" y="10.0" />
2228
+ <line x="33.4" y="0.0" />
2229
+ </path>
2230
+ <fillstroke />
2231
+ <stroke />
2232
+ <path>
2233
+ <move x="12.4" y="10.0" />
2234
+ <line x="12.4" y="1.7763568394002505e-15" />
2235
+ </path>
2236
+ <fillstroke />
2237
+ <stroke />
2238
+ <path>
2239
+ <move x="108.0" y="11.0" />
2240
+ <line x="108.0" y="1.0" />
2241
+ </path>
2242
+ <fillstroke />
2243
+ <stroke />
2244
+ <path>
2245
+ <move x="87.0" y="11.0" />
2246
+ <line x="87.0" y="1.0" />
2247
+ </path>
2248
+ <fillstroke />
2249
+ <stroke />
2250
+ <ellipse x="21.0" y="9.999999999999998" w="24.8" h="24.8" />
2251
+
2252
+ <stroke />
2253
+ <ellipse x="96.0" y="10.999999999999998" w="24.8" h="24.8" />
2254
+
2255
+ <stroke />
2256
+ </foreground>
2257
+ </shape><shape h="133.0" w="126.0" aspect="variable" strokewidth="inherit" name="transformer_voltage" displayName="transformer_voltage">
2258
+ <foreground>
2259
+ <ellipse x="16.0" y="0.0" w="40.0" h="36.0" />
2260
+ <fillstroke />
2261
+ <stroke />
2262
+ <ellipse x="0.0" y="26.0" w="40.0" h="36.0" />
2263
+ <fillstroke />
2264
+ <stroke />
2265
+ <ellipse x="32.0" y="26.0" w="40.0" h="36.0" />
2266
+ <fillstroke />
2267
+ <stroke />
2268
+ <rect x="103" y="17" w="22" h="35" />
2269
+ <fillstroke />
2270
+ <stroke />
2271
+ <path>
2272
+ <move x="36.0" y="59.0" />
2273
+ <line x="36.0" y="88.0" />
2274
+ </path>
2275
+ <fillstroke />
2276
+ <stroke />
2277
+ <path>
2278
+ <move x="35.95" y="85.33" />
2279
+ <line x="36.0" y="88.0" />
2280
+ <line x="114.0" y="88.0" />
2281
+ <line x="114.0" y="52.0" />
2282
+ </path>
2283
+ <stroke />
2284
+ <path>
2285
+ <move x="72.0" y="88.0" />
2286
+ <line x="72.0" y="132.0" />
2287
+ </path>
2288
+ <fillstroke />
2289
+ <stroke />
2290
+ <rect x="103" y="47" w="22" h="5" />
2291
+ <fillstroke />
2292
+ <stroke />
2293
+ <rect x="103" y="42" w="22" h="5" />
2294
+ <fillstroke />
2295
+ <stroke />
2296
+ <rect x="103" y="37" w="22" h="5" />
2297
+ <fillstroke />
2298
+ <stroke />
2299
+ <rect x="103" y="32" w="22" h="5" />
2300
+ <fillstroke />
2301
+ <stroke />
2302
+ <rect x="103" y="27" w="22" h="5" />
2303
+ <fillstroke />
2304
+ <stroke />
2305
+ <rect x="103" y="22" w="22" h="5" />
2306
+ <fillstroke />
2307
+ <stroke />
2308
+ <ellipse x="0.0" y="26.0" w="40.0" h="36.0" />
2309
+
2310
+ <stroke />
2311
+ <ellipse x="16.0" y="0.0" w="40.0" h="36.0" />
2312
+
2313
+ <stroke />
2314
+ </foreground>
2315
+ </shape><shape h="55.0" w="174.0" aspect="variable" strokewidth="inherit" name="tubular_apparatus" displayName="tubular_apparatus">
2316
+ <foreground>
2317
+ <path>
2318
+ <move x="11.54" y="13.14" />
2319
+ <line x="0.0" y="1.0" />
2320
+ <line x="23.0" y="1.0" />
2321
+ <line x="23.09" y="6.16" />
2322
+ </path>
2323
+ <fillstroke />
2324
+ <stroke />
2325
+ <roundrect arcsize="11.5" x="11.13" y="6" w="162" h="39" />
2326
+ <fillstroke />
2327
+ <stroke />
2328
+ <rect x="158.13" y="0.5" w="15" h="54" />
2329
+ <fillstroke />
2330
+ <stroke />
2331
+ <rect x="79.13" y="3" w="5" h="45" />
2332
+ <fillstroke />
2333
+ <stroke />
2334
+ <rect x="79.13" y="6" w="5" h="4" />
2335
+ <fillstroke />
2336
+ <stroke />
2337
+ <rect x="79.13" y="14" w="5" h="4" />
2338
+ <fillstroke />
2339
+ <stroke />
2340
+ <rect x="79.13" y="22" w="5" h="4" />
2341
+ <fillstroke />
2342
+ <stroke />
2343
+ <rect x="79.13" y="31" w="5" h="4" />
2344
+ <fillstroke />
2345
+ <stroke />
2346
+ <rect x="79.13" y="40" w="5" h="4" />
2347
+ <fillstroke />
2348
+ <stroke />
2349
+ </foreground>
2350
+ </shape><shape h="68.0" w="91.0" aspect="variable" strokewidth="inherit" name="turbogenerator" displayName="turbogenerator"><foreground><path><move x="0.0" y="34.78" /><line x="58.0" y="34.78" /></path><stroke /><path><move x="6.639999999999997" y="12.770000000000003" /><line x="17.06" y="18.14" /><line x="17.060000000000002" y="51.92" /><line x="6.640000000000001" y="57.29" /><close /></path><fillstroke /><stroke /><path><move x="45.92" y="67.51" /><line x="28.700000000000003" y="55.28" /><line x="28.700000000000003" y="12.719999999999999" /><line x="45.92" y="0.4900000000000091" /><close /></path><fillstroke /><stroke /><ellipse x="57.43801082543972" y="18.25801082543973" w="32.54397834912054" h="32.54397834912054" /><fillstroke /><stroke /><path><move x="62.06" y="34.79" /><curve x1="67.52" y1="21.93" x2="73.57" y2="33.84" x3="73.57" y3="33.84" /><curve x1="79.61" y1="45.75" x2="86.39" y2="34.79" x3="86.39" y3="34.79" /></path><stroke /></foreground></shape><shape h="129.0" w="53.0" aspect="variable" strokewidth="inherit" name="vacuum_crystallizer" displayName="vacuum_crystallizer"><foreground><path><move x="47.57" y="80.99000000000001" /><line x="47.57" y="92.58000000000001" /></path><fillstroke /><stroke /><path><move x="4.57" y="80.99000000000001" /><line x="4.57" y="92.58000000000001" /></path><fillstroke /><stroke /><rect x="8.63" y="0" w="34" h="34" /><fillstroke /><stroke /><rect x="4.67" y="80.68" w="42.66" h="11.89" /><fillstroke /><stroke /><rect x="13.46" y="34.27" w="24.88" h="39.31" /><fillstroke /><stroke /><path><move x="5.08" y="80.63" /><line x="13.08" y="73.57" /><line x="38.72" y="73.57" /><line x="46.72" y="80.63" /><close /></path><fillstroke /><stroke /><path><move x="46.72" y="92.72" /><line x="38.72" y="99.78" /><line x="13.079999999999998" y="99.78" /><line x="5.079999999999998" y="92.72" /><close /></path><fillstroke /><stroke /><rect x="13.46" y="99.78" w="24.88" h="24.19" /><fillstroke /><stroke /><path><move x="34.44999999999999" y="122.17999999999999" /><line x="25.89" y="128.01999999999998" /><line x="17.320000000000007" y="122.17999999999999" /><close /></path><fillstroke /><stroke /></foreground></shape><shape h="144.0" w="88.0" aspect="variable" strokewidth="inherit" name="vacuum_deaerator" displayName="vacuum_deaerator"><foreground><rect x="8.8" y="0" w="70.4" h="108.95" /><fillstroke /><stroke /><rect x="0" y="14.38" w="8.8" h="6.81" /><fillstroke /><stroke /><rect x="79.2" y="14.38" w="8.8" h="6.81" /><fillstroke /><stroke /><rect x="0" y="83.98" w="8.8" h="6.81" /><fillstroke /><stroke /><path><move x="79.2" y="108.95000000000002" /><line x="57.199999999999996" y="130.89000000000001" /><line x="30.799999999999997" y="130.89" /><line x="8.799999999999997" y="108.95000000000002" /><close /></path><fillstroke /><stroke /><rect x="30.31" y="130.89" w="27.38" h="12.11" /><fillstroke /><stroke /></foreground></shape><shape h="32.0" w="40.0" aspect="variable" strokewidth="inherit" name="valve_horizontal" displayName="valve_horizontal">
2351
+ <foreground>
2352
+ <rect x="8" y="0.53" w="24" h="9.28" />
2353
+ <fillstroke />
2354
+ <stroke />
2355
+ <path>
2356
+ <move x="0" y="7.88" />
2357
+ <line x="20" y="19.94" />
2358
+ <line x="0" y="32.0" />
2359
+ <close />
2360
+ <move x="40" y="7.88" />
2361
+ <line x="20" y="19.94" />
2362
+ <line x="40" y="32.0" />
2363
+ <close />
2364
+ </path>
2365
+ <fillstroke />
2366
+ <stroke />
2367
+ <path>
2368
+ <move x="20" y="18.03" />
2369
+ <line x="20" y="9.81" />
2370
+ </path>
2371
+ <stroke />
2372
+ <path>
2373
+ <move x="20" y="19.5" />
2374
+ <line x="20" y="18.03" />
2375
+ </path>
2376
+ <stroke />
2377
+ </foreground>
2378
+ </shape><shape h="40.0" w="32.0" aspect="variable" strokewidth="inherit" name="valve_vertical" displayName="valve_vertical">
2379
+ <foreground>
2380
+ <path>
2381
+ <move x="31.409999999999997" y="8.239999999999998" />
2382
+ <line x="31.41" y="32.239999999999995" />
2383
+ <line x="22.13" y="32.239999999999995" />
2384
+ <line x="22.13" y="8.239999999999998" />
2385
+ <close />
2386
+ </path>
2387
+ <fillstroke />
2388
+ <stroke />
2389
+ <path>
2390
+ <move x="24.059999999999995" y="0.23999999999999844" />
2391
+ <line x="12.0" y="20.24" />
2392
+ <line x="-0.0600000000000005" y="0.23999999999999844" />
2393
+ <close />
2394
+ <move x="24.060000000000002" y="40.239999999999995" />
2395
+ <line x="12.0" y="20.24" />
2396
+ <line x="-0.059999999999996945" y="40.239999999999995" />
2397
+ <close />
2398
+ </path>
2399
+ <fillstroke />
2400
+ <stroke />
2401
+ <path>
2402
+ <move x="12.12" y="20.16" />
2403
+ <line x="22.13" y="20.24" />
2404
+ </path>
2405
+ <stroke />
2406
+ </foreground>
2407
+ </shape><shape h="250.0" w="78.0" aspect="variable" strokewidth="inherit" name="vertical_apparatus" displayName="vertical_apparatus"><foreground><rect x="0" y="0" w="77.83" h="179.25" /><fillstroke /><stroke /><path><move x="40.08999999999999" y="179.24" /><line x="20.050000000000004" y="249.99" /><line x="1.4210854715202004e-14" y="179.24" /><close /></path><fillstroke /><stroke /><path><move x="77.82000000000001" y="179.25" /><line x="58.37" y="250.0" /><line x="38.90999999999998" y="179.25" /><close /></path><fillstroke /><stroke /></foreground></shape></shapes>
mnemo-studio-tool/web/Widget.json ADDED
The diff for this file is too large to render. See raw diff
 
web-new/src/App.css CHANGED
@@ -1,6 +1,7 @@
1
  .app-shell {
2
  display: grid;
3
  grid-template-rows: auto 1fr;
 
4
  min-height: 100%;
5
  background:
6
  radial-gradient(circle at top left, rgba(14, 165, 164, 0.14), transparent 34%),
@@ -71,11 +72,13 @@
71
 
72
  .main-content {
73
  display: grid;
 
74
  overflow: hidden;
75
  }
76
 
77
  .editor-layout {
78
- grid-template-columns: minmax(0, 1fr) 420px;
 
79
  }
80
 
81
  .workspace-stack {
@@ -174,6 +177,7 @@
174
 
175
  .sidebar {
176
  min-width: 0;
 
177
  overflow-y: auto;
178
  overflow-x: hidden;
179
  display: flex;
@@ -186,6 +190,7 @@
186
 
187
  .panel-section {
188
  min-width: 0;
 
189
  padding: 16px;
190
  border-radius: 16px;
191
  background: linear-gradient(180deg, rgba(30, 41, 59, 0.94), rgba(15, 23, 42, 0.94));
@@ -197,11 +202,14 @@
197
  display: flex;
198
  align-items: center;
199
  justify-content: space-between;
 
200
  gap: 10px;
201
  margin-bottom: 14px;
202
  }
203
 
204
  .panel-head h2 {
 
 
205
  font-size: 15px;
206
  font-weight: 650;
207
  }
@@ -209,6 +217,7 @@
209
  .panel-note {
210
  color: var(--text-muted);
211
  font-size: 11px;
 
212
  }
213
 
214
  .panel-grid {
@@ -314,11 +323,15 @@
314
  }
315
 
316
  .xml-preview-section {
 
 
317
  min-height: 260px;
318
  }
319
 
320
  .xml-preview {
 
321
  width: 100%;
 
322
  min-height: 220px;
323
  resize: vertical;
324
  border: 1px solid rgba(148, 163, 184, 0.18);
@@ -456,8 +469,9 @@
456
 
457
  .inspector-panel {
458
  display: flex;
 
459
  flex-direction: column;
460
- min-height: 0;
461
  overflow: hidden;
462
  }
463
 
@@ -479,6 +493,15 @@
479
  font-weight: 600;
480
  }
481
 
 
 
 
 
 
 
 
 
 
482
  .inspector-editor {
483
  padding-bottom: 12px;
484
  margin-bottom: 10px;
@@ -591,6 +614,26 @@
591
  font-weight: 600;
592
  }
593
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  .ann-meta {
595
  display: flex;
596
  flex-wrap: wrap;
 
1
  .app-shell {
2
  display: grid;
3
  grid-template-rows: auto 1fr;
4
+ height: 100%;
5
  min-height: 100%;
6
  background:
7
  radial-gradient(circle at top left, rgba(14, 165, 164, 0.14), transparent 34%),
 
72
 
73
  .main-content {
74
  display: grid;
75
+ min-height: 0;
76
  overflow: hidden;
77
  }
78
 
79
  .editor-layout {
80
+ grid-template-columns: 200px minmax(0, 1fr) 380px;
81
+ min-height: 0;
82
  }
83
 
84
  .workspace-stack {
 
177
 
178
  .sidebar {
179
  min-width: 0;
180
+ min-height: 0;
181
  overflow-y: auto;
182
  overflow-x: hidden;
183
  display: flex;
 
190
 
191
  .panel-section {
192
  min-width: 0;
193
+ flex-shrink: 0;
194
  padding: 16px;
195
  border-radius: 16px;
196
  background: linear-gradient(180deg, rgba(30, 41, 59, 0.94), rgba(15, 23, 42, 0.94));
 
202
  display: flex;
203
  align-items: center;
204
  justify-content: space-between;
205
+ flex-wrap: wrap;
206
  gap: 10px;
207
  margin-bottom: 14px;
208
  }
209
 
210
  .panel-head h2 {
211
+ margin: 0;
212
+ min-width: 0;
213
  font-size: 15px;
214
  font-weight: 650;
215
  }
 
217
  .panel-note {
218
  color: var(--text-muted);
219
  font-size: 11px;
220
+ flex-shrink: 0;
221
  }
222
 
223
  .panel-grid {
 
323
  }
324
 
325
  .xml-preview-section {
326
+ display: flex;
327
+ flex-direction: column;
328
  min-height: 260px;
329
  }
330
 
331
  .xml-preview {
332
+ flex: 1 1 auto;
333
  width: 100%;
334
+ min-width: 0;
335
  min-height: 220px;
336
  resize: vertical;
337
  border: 1px solid rgba(148, 163, 184, 0.18);
 
469
 
470
  .inspector-panel {
471
  display: flex;
472
+ flex: 1 0 320px;
473
  flex-direction: column;
474
+ min-height: 320px;
475
  overflow: hidden;
476
  }
477
 
 
493
  font-weight: 600;
494
  }
495
 
496
+ .inspector-meta.xml-ok {
497
+ color: #86efac;
498
+ }
499
+
500
+ .inspector-meta.xml-warn {
501
+ color: #fbbf24;
502
+ font-weight: 600;
503
+ }
504
+
505
  .inspector-editor {
506
  padding-bottom: 12px;
507
  margin-bottom: 10px;
 
614
  font-weight: 600;
615
  }
616
 
617
+ .ann-xml-status {
618
+ display: inline-flex;
619
+ align-items: center;
620
+ margin-left: auto;
621
+ padding: 2px 6px;
622
+ border-radius: 999px;
623
+ font-size: 10px;
624
+ font-weight: 700;
625
+ }
626
+
627
+ .ann-xml-status.ok {
628
+ background: rgba(34, 197, 94, 0.14);
629
+ color: #86efac;
630
+ }
631
+
632
+ .ann-xml-status.warn {
633
+ background: rgba(245, 158, 11, 0.16);
634
+ color: #fbbf24;
635
+ }
636
+
637
  .ann-meta {
638
  display: flex;
639
  flex-wrap: wrap;
web-new/src/App.jsx CHANGED
@@ -1,6 +1,7 @@
1
  import { useCallback, useMemo, useRef, useState } from 'react';
2
  import './App.css';
3
  import { analyzeLayout, generateImportXml, runOcr } from './api';
 
4
  import ImageUpload from './components/ImageUpload';
5
  import Inspector from './components/Inspector';
6
  import SvgWorkspace from './components/SvgWorkspace';
@@ -10,7 +11,7 @@ import { useKeyboard } from './hooks/useKeyboard';
10
  import * as A from './store/actions';
11
  import { useDispatch, useStore } from './store/context';
12
  import { baseName, downloadText, fileToDataUrl } from './utils/files';
13
- import { boundsOfPoints, rectToPoints } from './utils/geometry';
14
  import { uid } from './utils/uid';
15
 
16
 
@@ -27,6 +28,7 @@ const UID_TRANSLATION = {
27
  };
28
 
29
  const STRICT_UID_RE = /^(?:N|NO|№)?\s*\d{1,4}$/i;
 
30
 
31
 
32
  function loadImageElement(dataUrl) {
@@ -44,6 +46,12 @@ function normalizeBindingUid(value) {
44
  if (!text) return '';
45
  const translated = Array.from(text, (char) => UID_TRANSLATION[char] ?? char).join('');
46
  const compact = translated.replace(/\s+/g, '');
 
 
 
 
 
 
47
  if (/[.,;:]/.test(compact)) return '';
48
  if (!STRICT_UID_RE.test(compact)) return '';
49
  const digits = translated.replace(/\D+/g, '');
@@ -77,6 +85,7 @@ function buildAnnotationPayload(annotation) {
77
  return {
78
  id: annotation.id,
79
  number: annotation.number,
 
80
  bindingUid: explicitBindingUid,
81
  detectedKind: annotation.detectedKind || '',
82
  type: annotation.type || 'rect',
@@ -97,15 +106,54 @@ function buildAnnotationPayload(annotation) {
97
  }
98
 
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  function detectedKindFromDetectionItem(annotation) {
101
  const explicitKind = String(annotation?.detectedKind || annotation?.kind || '').trim().toLowerCase();
102
  if (explicitKind && explicitKind !== 'other') return explicitKind;
103
  const category = String(annotation?.category || '').trim().toLowerCase();
104
- if (category === 'table') return 'group';
105
- if (category === 'value') return 'cell';
 
 
106
  if (category === 'pipe') return 'arrow';
107
- if (category === 'equipment') return 'equipment';
108
  if (category === 'indicator') return 'indicator';
 
109
  return explicitKind || 'other';
110
  }
111
 
@@ -135,28 +183,38 @@ function buildAutoAnnotationsFromDetectionItems(items, existingAnnotations) {
135
  const validPoints = item.points
136
  .map((point) => ({ x: Number(point?.x) || 0, y: Number(point?.y) || 0 }))
137
  .filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
 
 
138
  const bounds = boundsOfPoints(validPoints);
139
  const x = Number(bounds.x) || 0;
140
  const y = Number(bounds.y) || 0;
141
  const width = Math.max(1, Number(bounds.width) || 0);
142
  const height = Math.max(1, Number(bounds.height) || 0);
143
-
144
- while (usedNumbers.has(fallbackNumber)) fallbackNumber += 1;
145
- const number = fallbackNumber;
146
- fallbackNumber += 1;
 
 
 
 
 
 
 
147
  usedNumbers.add(number);
148
 
149
- const type = item?.type === 'polygon' || kind === 'arrow'
150
- ? (validPoints.length >= 3 ? 'polygon' : 'rect')
151
- : 'rect';
152
  const points = type === 'polygon'
153
- ? validPoints
154
  : rectToPoints({ x, y, width, height });
155
  const bindingUid = normalizeBindingUid(item?.bindingUid);
156
 
157
  created.push({
158
  id: uid('ann'),
159
  number,
 
160
  bindingUid,
161
  detectedKind: kind,
162
  type,
@@ -183,11 +241,13 @@ function formatDetectionCounts(counts) {
183
  const safe = counts || {};
184
  const parts = [`всего ${Number(safe.total) || 0}`];
185
  const metrics = [
186
- ['группы', Number(safe.groups) || 0],
187
- ['ячейки', Number(safe.cells) || 0],
 
 
188
  ['стрелки', Number(safe.arrows) || 0],
 
189
  ['ocr-ячейки', Number(safe.ocrCells) || 0],
190
- ['оборудование', Number(safe.equipment) || 0],
191
  ['индикаторы', Number(safe.indicators) || 0],
192
  ['прочее', Number(safe.other) || 0],
193
  ];
@@ -211,8 +271,8 @@ function formatSheetReason(reason) {
211
  }
212
 
213
 
214
- const AUTO_DETECTOR_MODE = 'colab-pipeline';
215
- const AUTO_DETECTOR_LABEL = 'src-colab-hybrid-v2';
216
 
217
 
218
  function AppShell() {
@@ -220,6 +280,7 @@ function AppShell() {
220
  const dispatch = useDispatch();
221
  const fileInputRef = useRef(null);
222
  const [errorText, setErrorText] = useState('');
 
223
 
224
  const {
225
  deleteSelected,
@@ -231,6 +292,10 @@ function AppShell() {
231
  () => state.annotations.find((annotation) => annotation.id === state.selectedId) || null,
232
  [state.annotations, state.selectedId],
233
  );
 
 
 
 
234
 
235
  const setStatus = useCallback((text) => {
236
  dispatch({ type: A.SET_STATUS, payload: text });
@@ -270,11 +335,12 @@ function AppShell() {
270
  dispatch({
271
  type: A.SET_PROCESSING,
272
  payload: {
 
273
  ready: false,
274
  components: [],
275
  counts: {},
276
  summary: '',
277
- detector: AUTO_DETECTOR_LABEL,
278
  lastAnalyzedAt: '',
279
  },
280
  });
@@ -309,6 +375,7 @@ function AppShell() {
309
  if (!state.imageDataUrl || !selectedAnnotation) return;
310
 
311
  try {
 
312
  dispatch({ type: A.SET_BUSY, payload: true });
313
  setErrorText('');
314
  const bounds = annotationBounds(selectedAnnotation);
@@ -342,6 +409,7 @@ function AppShell() {
342
  } catch (error) {
343
  setErrorText(error.message || 'OCR для выделенного элемента завершился ошибкой.');
344
  } finally {
 
345
  dispatch({ type: A.SET_BUSY, payload: false });
346
  }
347
  }, [dispatch, selectedAnnotation, setStatus, state.imageDataUrl]);
@@ -350,13 +418,29 @@ function AppShell() {
350
  if (!state.imageDataUrl) return;
351
 
352
  try {
 
353
  dispatch({ type: A.SET_BUSY, payload: true });
354
  setErrorText('');
355
- setStatus('Автораспознавание: запуск hybrid colab/src detector...');
 
 
 
 
 
 
 
 
 
 
 
356
  const result = await analyzeLayout({
357
  imageDataUrl: state.imageDataUrl,
358
  config: {
359
  detectorMode: AUTO_DETECTOR_MODE,
 
 
 
 
360
  sensitivity: state.config.sensitivity,
361
  imageCorrection: state.config.imageCorrection,
362
  enhanceImage: state.config.enhanceImage,
@@ -364,11 +448,23 @@ function AppShell() {
364
  },
365
  });
366
 
 
 
 
 
 
 
 
 
 
 
367
  const manualAnnotations = state.annotations.filter((annotation) => annotation.source !== 'auto');
368
  const created = buildAutoAnnotationsFromDetectionItems(result.overlays, manualAnnotations);
 
369
  dispatch({
370
  type: A.SET_PROCESSING,
371
  payload: {
 
372
  ready: true,
373
  width: Number(result.imageWidth) || state.imageWidth,
374
  height: Number(result.imageHeight) || state.imageHeight,
@@ -376,7 +472,7 @@ function AppShell() {
376
  components: Array.isArray(result.overlays) ? result.overlays : [],
377
  counts: result.counts || {},
378
  summary: result.summary || '',
379
- detector: result.engine || 'src-hybrid-layout-v2',
380
  lastAnalyzedAt: new Date().toISOString(),
381
  },
382
  });
@@ -395,8 +491,18 @@ function AppShell() {
395
  dispatch({ type: A.SELECT_ANNOTATION, payload: created[0].id });
396
  setStatus(`${result.summary || 'Автораспознавание завершено.'} Создано автоаннотаций: ${created.length}.`);
397
  } catch (error) {
 
 
 
 
 
 
 
 
 
398
  setErrorText(error.message || 'Автораспознавание завершилось ошибкой.');
399
  } finally {
 
400
  dispatch({ type: A.SET_BUSY, payload: false });
401
  }
402
  }, [dispatch, setStatus, setWidgetExport, state.annotations, state.config.enhanceImage, state.config.ignoreLines, state.config.imageCorrection, state.config.sensitivity, state.imageDataUrl, state.imageHeight, state.imageWidth]);
@@ -408,10 +514,12 @@ function AppShell() {
408
  dispatch({
409
  type: A.SET_PROCESSING,
410
  payload: {
 
411
  ready: false,
412
  components: [],
413
  counts: {},
414
  summary: '',
 
415
  lastAnalyzedAt: '',
416
  },
417
  });
@@ -428,38 +536,18 @@ function AppShell() {
428
  dispatch({
429
  type: A.SET_PROCESSING,
430
  payload: {
 
431
  ready: false,
432
  components: [],
433
  counts: {},
434
  summary: '',
 
435
  lastAnalyzedAt: '',
436
  },
437
  });
438
  setStatus('Автоматические аннотации удалены.');
439
  }, [dispatch, setStatus, state.annotations, state.processing]);
440
 
441
- const applyResolvedBindings = useCallback((bindings) => {
442
- if (!Array.isArray(bindings) || !bindings.length) return;
443
- const bindingsByUid = new Map();
444
- bindings.forEach((binding) => {
445
- const key = normalizeBindingUid(binding.uid);
446
- if (key && !bindingsByUid.has(key)) bindingsByUid.set(key, binding);
447
- });
448
-
449
- const nextAnnotations = state.annotations.map((annotation) => {
450
- const key = normalizeBindingUid(annotation.bindingUid);
451
- const binding = bindingsByUid.get(key);
452
- if (!binding) return annotation;
453
- return {
454
- ...annotation,
455
- contextPath: annotation.contextPath || binding.omPath || '',
456
- widgetNameOverride: annotation.widgetNameOverride || binding.widget || '',
457
- };
458
- });
459
-
460
- dispatch({ type: A.SET_ANNOTATIONS, payload: nextAnnotations });
461
- }, [dispatch, state.annotations]);
462
-
463
  const handleGenerateImport = useCallback(async () => {
464
  if (!state.imageDataUrl || !state.annotations.length) {
465
  setErrorText('Сначала загрузите изображение и создайте или проверьте аннотации элементов схемы.');
@@ -467,6 +555,7 @@ function AppShell() {
467
  }
468
 
469
  try {
 
470
  dispatch({ type: A.SET_BUSY, payload: true });
471
  setErrorText('');
472
  setStatus('Генерация import XML...');
@@ -481,7 +570,12 @@ function AppShell() {
481
  excelPath: state.widgetExport.excelPath || undefined,
482
  });
483
 
484
- applyResolvedBindings(result.bindings);
 
 
 
 
 
485
  setWidgetExport({
486
  generatedXmlText: result.xmlText,
487
  mnemoTitle: result.title || state.widgetExport.mnemoTitle,
@@ -492,6 +586,7 @@ function AppShell() {
492
  exportedCount: result.exportedCount,
493
  unmatchedNumbers: result.unmatchedNumbers || [],
494
  missingVisuals: result.missingVisuals || [],
 
495
  lastGeneratedAt: new Date().toISOString(),
496
  });
497
  setStatus(
@@ -500,9 +595,10 @@ function AppShell() {
500
  } catch (error) {
501
  setErrorText(error.message || 'Не удалось сформировать import XML.');
502
  } finally {
 
503
  dispatch({ type: A.SET_BUSY, payload: false });
504
  }
505
- }, [applyResolvedBindings, dispatch, setStatus, setWidgetExport, state.annotations, state.imageDataUrl, state.imageName, state.widgetExport]);
506
 
507
  const handleDownloadXml = useCallback(() => {
508
  const xmlText = String(state.widgetExport.generatedXmlText || '').trim();
@@ -514,6 +610,51 @@ function AppShell() {
514
  downloadText(xmlText, defaultName, 'application/xml;charset=utf-8');
515
  }, [state.imageName, state.widgetExport.generatedXmlText, state.widgetExport.sheetResolvedName]);
516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
  useKeyboard({
518
  onUndo: () => dispatch({ type: A.UNDO }),
519
  onRedo: () => dispatch({ type: A.REDO }),
@@ -570,19 +711,19 @@ function AppShell() {
570
  </button>
571
  {hasImage && (
572
  <>
573
- <button className="btn btn-warn" onClick={handleAutoDetect} disabled={state.isBusy}>
574
- {state.isBusy ? <><span className="spinner" /> Анализ...</> : 'Автораспознавание'}
575
  </button>
576
- <button className="btn btn-ghost" onClick={handleSelectedOcr} disabled={!selectedAnnotation || state.isBusy}>
577
- OCR выделенного
578
  </button>
579
- <button className="btn btn-primary" onClick={handleGenerateImport} disabled={!state.annotations.length || state.isBusy}>
580
- {state.isBusy ? <><span className="spinner" /> Генерация...</> : 'Сформировать XML'}
581
  </button>
582
  <button className="btn btn-success" onClick={handleDownloadXml} disabled={!hasGeneratedXml}>
583
  Скачать XML
584
  </button>
585
- <button className="btn btn-ghost" onClick={handleReset} disabled={state.isBusy}>
586
  Сбросить
587
  </button>
588
  </>
@@ -595,13 +736,14 @@ function AppShell() {
595
  <ImageUpload onImageLoad={handleImageLoad} />
596
  <div className="empty-hints">
597
  <h3>Что умеет новая реализация</h3>
598
- <p>Кнопка `Автораспознавание` запускает гибридный `src`-детектор: ячейки идут по логике `mnemo_ocr_colab.ipynb`, а группы и стрелки добираются layout-анализатором.</p>
599
- <p>Все найденные элементы попадают в аннотации и затем выгружаются в import XML. То, что авторазбор не нашёл, можно дорисовать вручную и выгрузить тем же путём.</p>
600
  <p>Файл Приложения сервер найдёт автоматически в проекте. При необходимости путь можно переопределить после загрузки изображения.</p>
601
  </div>
602
  </div>
603
  ) : (
604
  <div className="main-content editor-layout">
 
605
  <section className="workspace-stack">
606
  <Toolbar
607
  onUndo={() => dispatch({ type: A.UNDO })}
@@ -619,7 +761,7 @@ function AppShell() {
619
  <span className="panel-note">
620
  {state.processing.lastAnalyzedAt
621
  ? new Date(state.processing.lastAnalyzedAt).toLocaleString()
622
- : 'ещё не запускалось'}
623
  </span>
624
  </div>
625
 
@@ -630,7 +772,13 @@ function AppShell() {
630
  </div>
631
  <div className="summary-item">
632
  <span>Сводка</span>
633
- <strong>{state.processing.ready ? formatDetectionCounts(state.processing.counts) : 'ожидание анализа'}</strong>
 
 
 
 
 
 
634
  </div>
635
  </div>
636
 
@@ -695,8 +843,8 @@ function AppShell() {
695
  </div>
696
 
697
  <div className="panel-actions">
698
- <button className="btn btn-primary" onClick={handleGenerateImport} disabled={!state.annotations.length || state.isBusy}>
699
- {state.isBusy ? <><span className="spinner" /> ...</> : 'Сформировать XML'}
700
  </button>
701
  <button className="btn btn-success" onClick={handleDownloadXml} disabled={!hasGeneratedXml}>
702
  Скачать
@@ -741,20 +889,9 @@ function AppShell() {
741
  )}
742
  </section>
743
 
744
- <Inspector onOcrSelected={handleSelectedOcr} onDelete={deleteSelected} />
745
 
746
- <section className="panel-section xml-preview-section">
747
- <div className="panel-head">
748
- <h2>XML Preview</h2>
749
- <span className="panel-note">{hasGeneratedXml ? `${state.widgetExport.generatedXmlText.length} символов` : 'пусто'}</span>
750
- </div>
751
- <textarea
752
- className="xml-preview"
753
- readOnly
754
- value={state.widgetExport.generatedXmlText || ''}
755
- placeholder="После генерации здесь появится import XML."
756
- />
757
- </section>
758
  </aside>
759
  </div>
760
  )}
 
1
  import { useCallback, useMemo, useRef, useState } from 'react';
2
  import './App.css';
3
  import { analyzeLayout, generateImportXml, runOcr } from './api';
4
+ import ElementLibraryPanel from './components/ElementLibraryPanel';
5
  import ImageUpload from './components/ImageUpload';
6
  import Inspector from './components/Inspector';
7
  import SvgWorkspace from './components/SvgWorkspace';
 
11
  import * as A from './store/actions';
12
  import { useDispatch, useStore } from './store/context';
13
  import { baseName, downloadText, fileToDataUrl } from './utils/files';
14
+ import { boundsOfPoints, rectToPoints, isLikelyRect, simplifyClosedPolygon, dedupeClosePoints } from './utils/geometry';
15
  import { uid } from './utils/uid';
16
 
17
 
 
28
  };
29
 
30
  const STRICT_UID_RE = /^(?:N|NO|№)?\s*\d{1,4}$/i;
31
+ const COMPOSITE_UID_RE = /^(?:N|NO|№)?\s*(\d{1,4})\.(\d{1,2})$/i;
32
 
33
 
34
  function loadImageElement(dataUrl) {
 
46
  if (!text) return '';
47
  const translated = Array.from(text, (char) => UID_TRANSLATION[char] ?? char).join('');
48
  const compact = translated.replace(/\s+/g, '');
49
+ const compositeMatch = compact.match(COMPOSITE_UID_RE);
50
+ if (compositeMatch) {
51
+ const baseDigits = compositeMatch[1].replace(/^0+/, '') || compositeMatch[1];
52
+ const suffixDigits = compositeMatch[2].replace(/^0+/, '') || compositeMatch[2];
53
+ return `${baseDigits}.${suffixDigits}`;
54
+ }
55
  if (/[.,;:]/.test(compact)) return '';
56
  if (!STRICT_UID_RE.test(compact)) return '';
57
  const digits = translated.replace(/\D+/g, '');
 
85
  return {
86
  id: annotation.id,
87
  number: annotation.number,
88
+ displayNumber: annotation.displayNumber ?? null,
89
  bindingUid: explicitBindingUid,
90
  detectedKind: annotation.detectedKind || '',
91
  type: annotation.type || 'rect',
 
106
  }
107
 
108
 
109
+ function mergeImportResultsIntoAnnotations(annotations, bindings, annotationResults) {
110
+ const bindingsByUid = new Map();
111
+ if (Array.isArray(bindings)) {
112
+ bindings.forEach((binding) => {
113
+ const key = normalizeBindingUid(binding?.uid);
114
+ if (key && !bindingsByUid.has(key)) bindingsByUid.set(key, binding);
115
+ });
116
+ }
117
+
118
+ const resultsById = new Map();
119
+ if (Array.isArray(annotationResults)) {
120
+ annotationResults.forEach((item) => {
121
+ const id = String(item?.id || '').trim();
122
+ if (id) resultsById.set(id, item);
123
+ });
124
+ }
125
+
126
+ return (Array.isArray(annotations) ? annotations : []).map((annotation) => {
127
+ const key = normalizeBindingUid(annotation.bindingUid);
128
+ const binding = bindingsByUid.get(key);
129
+ const result = resultsById.get(annotation.id);
130
+ return {
131
+ ...annotation,
132
+ contextPath: annotation.contextPath || binding?.omPath || '',
133
+ widgetNameOverride: annotation.widgetNameOverride || binding?.widget || '',
134
+ xmlStatus: String(result?.status || '').trim(),
135
+ xmlMatched: Boolean(result?.matched),
136
+ xmlExported: Boolean(result?.exported),
137
+ xmlNeedsAttention: Boolean(result?.needsAttention),
138
+ xmlMissingVisuals: Array.isArray(result?.missingVisuals) ? result.missingVisuals : [],
139
+ xmlResolvedVisuals: Array.isArray(result?.resolvedVisuals) ? result.resolvedVisuals : [],
140
+ };
141
+ });
142
+ }
143
+
144
+
145
  function detectedKindFromDetectionItem(annotation) {
146
  const explicitKind = String(annotation?.detectedKind || annotation?.kind || '').trim().toLowerCase();
147
  if (explicitKind && explicitKind !== 'other') return explicitKind;
148
  const category = String(annotation?.category || '').trim().toLowerCase();
149
+ if (category === 'widget') return 'widget';
150
+ if (category === 'static') return 'form';
151
+ if (category === 'table') return 'form';
152
+ if (category === 'value') return 'widget';
153
  if (category === 'pipe') return 'arrow';
154
+ if (category === 'equipment') return 'shape';
155
  if (category === 'indicator') return 'indicator';
156
+ if (category === 'text') return explicitKind || 'text-label';
157
  return explicitKind || 'other';
158
  }
159
 
 
183
  const validPoints = item.points
184
  .map((point) => ({ x: Number(point?.x) || 0, y: Number(point?.y) || 0 }))
185
  .filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
186
+ const reducedPoints = simplifyClosedPolygon(dedupeClosePoints(validPoints, 1.2), 1.4);
187
+ const shapePoints = reducedPoints.length >= 3 ? reducedPoints : validPoints;
188
  const bounds = boundsOfPoints(validPoints);
189
  const x = Number(bounds.x) || 0;
190
  const y = Number(bounds.y) || 0;
191
  const width = Math.max(1, Number(bounds.width) || 0);
192
  const height = Math.max(1, Number(bounds.height) || 0);
193
+ const rawDisplayNumber = Number.parseInt(String(item?.displayNumber ?? item?.number ?? ''), 10);
194
+ const displayNumber = Number.isFinite(rawDisplayNumber) && rawDisplayNumber > 0
195
+ ? rawDisplayNumber
196
+ : null;
197
+
198
+ let number = displayNumber;
199
+ if (!Number.isFinite(number) || usedNumbers.has(number)) {
200
+ while (usedNumbers.has(fallbackNumber)) fallbackNumber += 1;
201
+ number = fallbackNumber;
202
+ fallbackNumber += 1;
203
+ }
204
  usedNumbers.add(number);
205
 
206
+ const type = item?.type === 'rect'
207
+ ? 'rect'
208
+ : (shapePoints.length >= 3 && !isLikelyRect(shapePoints) ? 'polygon' : 'rect');
209
  const points = type === 'polygon'
210
+ ? shapePoints
211
  : rectToPoints({ x, y, width, height });
212
  const bindingUid = normalizeBindingUid(item?.bindingUid);
213
 
214
  created.push({
215
  id: uid('ann'),
216
  number,
217
+ displayNumber,
218
  bindingUid,
219
  detectedKind: kind,
220
  type,
 
241
  const safe = counts || {};
242
  const parts = [`всего ${Number(safe.total) || 0}`];
243
  const metrics = [
244
+ ['виджеты', Number(safe.widgets) || Number(safe.cells) || 0],
245
+ ['статика', Number(safe.statics) || 0],
246
+ ['формы', Number(safe.forms) || Number(safe.groups) || 0],
247
+ ['шейпы', Number(safe.shapes) || Number(safe.equipment) || 0],
248
  ['стрелки', Number(safe.arrows) || 0],
249
+ ['текст', Number(safe.text) || 0],
250
  ['ocr-ячейки', Number(safe.ocrCells) || 0],
 
251
  ['индикаторы', Number(safe.indicators) || 0],
252
  ['прочее', Number(safe.other) || 0],
253
  ];
 
271
  }
272
 
273
 
274
+ const AUTO_DETECTOR_MODE = 'hf-demo';
275
+ const AUTO_DETECTOR_LABEL = 'src-hf-demo-hybrid-v3';
276
 
277
 
278
  function AppShell() {
 
280
  const dispatch = useDispatch();
281
  const fileInputRef = useRef(null);
282
  const [errorText, setErrorText] = useState('');
283
+ const [busyAction, setBusyAction] = useState('');
284
 
285
  const {
286
  deleteSelected,
 
292
  () => state.annotations.find((annotation) => annotation.id === state.selectedId) || null,
293
  [state.annotations, state.selectedId],
294
  );
295
+ const isBusy = Boolean(state.isBusy || busyAction);
296
+ const isAnalyzing = busyAction === 'analyze';
297
+ const isGenerating = busyAction === 'generate';
298
+ const isRunningOcr = busyAction === 'ocr';
299
 
300
  const setStatus = useCallback((text) => {
301
  dispatch({ type: A.SET_STATUS, payload: text });
 
335
  dispatch({
336
  type: A.SET_PROCESSING,
337
  payload: {
338
+ pending: false,
339
  ready: false,
340
  components: [],
341
  counts: {},
342
  summary: '',
343
+ detector: '',
344
  lastAnalyzedAt: '',
345
  },
346
  });
 
375
  if (!state.imageDataUrl || !selectedAnnotation) return;
376
 
377
  try {
378
+ setBusyAction('ocr');
379
  dispatch({ type: A.SET_BUSY, payload: true });
380
  setErrorText('');
381
  const bounds = annotationBounds(selectedAnnotation);
 
409
  } catch (error) {
410
  setErrorText(error.message || 'OCR для выделенного элемента завершился ошибкой.');
411
  } finally {
412
+ setBusyAction('');
413
  dispatch({ type: A.SET_BUSY, payload: false });
414
  }
415
  }, [dispatch, selectedAnnotation, setStatus, state.imageDataUrl]);
 
418
  if (!state.imageDataUrl) return;
419
 
420
  try {
421
+ setBusyAction('analyze');
422
  dispatch({ type: A.SET_BUSY, payload: true });
423
  setErrorText('');
424
+ dispatch({
425
+ type: A.SET_PROCESSING,
426
+ payload: {
427
+ pending: true,
428
+ ready: false,
429
+ counts: {},
430
+ summary: 'Выполняется анализ схемы...',
431
+ detector: AUTO_DETECTOR_LABEL,
432
+ lastAnalyzedAt: '',
433
+ },
434
+ });
435
+ setStatus('Автораспознавание: выполняется анализ схемы...');
436
  const result = await analyzeLayout({
437
  imageDataUrl: state.imageDataUrl,
438
  config: {
439
  detectorMode: AUTO_DETECTOR_MODE,
440
+ sourceName: state.widgetExport.sourceNameHint || state.imageName || undefined,
441
+ titleHint: state.widgetExport.titleHint || state.widgetExport.mnemoTitle || undefined,
442
+ sheetName: state.widgetExport.sheetNameHint || undefined,
443
+ excelPath: state.widgetExport.excelPath || undefined,
444
  sensitivity: state.config.sensitivity,
445
  imageCorrection: state.config.imageCorrection,
446
  enhanceImage: state.config.enhanceImage,
 
448
  },
449
  });
450
 
451
+ console.log('[AutoDetect] Backend response:', {
452
+ engine: result.engine,
453
+ overlaysCount: result.overlays?.length,
454
+ counts: result.counts,
455
+ imageSize: `${result.imageWidth}x${result.imageHeight}`,
456
+ overlayKinds: (result.overlays || []).reduce((acc, o) => {
457
+ acc[o.kind] = (acc[o.kind] || 0) + 1;
458
+ return acc;
459
+ }, {}),
460
+ });
461
  const manualAnnotations = state.annotations.filter((annotation) => annotation.source !== 'auto');
462
  const created = buildAutoAnnotationsFromDetectionItems(result.overlays, manualAnnotations);
463
+ console.log('[AutoDetect] Created annotations:', created.length, 'from', result.overlays?.length, 'overlays');
464
  dispatch({
465
  type: A.SET_PROCESSING,
466
  payload: {
467
+ pending: false,
468
  ready: true,
469
  width: Number(result.imageWidth) || state.imageWidth,
470
  height: Number(result.imageHeight) || state.imageHeight,
 
472
  components: Array.isArray(result.overlays) ? result.overlays : [],
473
  counts: result.counts || {},
474
  summary: result.summary || '',
475
+ detector: result.engine || 'src-hf-demo-v1',
476
  lastAnalyzedAt: new Date().toISOString(),
477
  },
478
  });
 
491
  dispatch({ type: A.SELECT_ANNOTATION, payload: created[0].id });
492
  setStatus(`${result.summary || 'Автораспознавание завершено.'} Создано автоаннотаций: ${created.length}.`);
493
  } catch (error) {
494
+ dispatch({
495
+ type: A.SET_PROCESSING,
496
+ payload: {
497
+ pending: false,
498
+ ready: false,
499
+ summary: '',
500
+ detector: '',
501
+ },
502
+ });
503
  setErrorText(error.message || 'Автораспознавание завершилось ошибкой.');
504
  } finally {
505
+ setBusyAction('');
506
  dispatch({ type: A.SET_BUSY, payload: false });
507
  }
508
  }, [dispatch, setStatus, setWidgetExport, state.annotations, state.config.enhanceImage, state.config.ignoreLines, state.config.imageCorrection, state.config.sensitivity, state.imageDataUrl, state.imageHeight, state.imageWidth]);
 
514
  dispatch({
515
  type: A.SET_PROCESSING,
516
  payload: {
517
+ pending: false,
518
  ready: false,
519
  components: [],
520
  counts: {},
521
  summary: '',
522
+ detector: '',
523
  lastAnalyzedAt: '',
524
  },
525
  });
 
536
  dispatch({
537
  type: A.SET_PROCESSING,
538
  payload: {
539
+ pending: false,
540
  ready: false,
541
  components: [],
542
  counts: {},
543
  summary: '',
544
+ detector: '',
545
  lastAnalyzedAt: '',
546
  },
547
  });
548
  setStatus('Автоматические аннотации удалены.');
549
  }, [dispatch, setStatus, state.annotations, state.processing]);
550
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  const handleGenerateImport = useCallback(async () => {
552
  if (!state.imageDataUrl || !state.annotations.length) {
553
  setErrorText('Сначала загрузите изображение и создайте или проверьте аннотации элементов схемы.');
 
555
  }
556
 
557
  try {
558
+ setBusyAction('generate');
559
  dispatch({ type: A.SET_BUSY, payload: true });
560
  setErrorText('');
561
  setStatus('Генерация import XML...');
 
570
  excelPath: state.widgetExport.excelPath || undefined,
571
  });
572
 
573
+ const nextAnnotations = mergeImportResultsIntoAnnotations(
574
+ state.annotations,
575
+ result.bindings,
576
+ result.annotationResults,
577
+ );
578
+ dispatch({ type: A.SET_ANNOTATIONS, payload: nextAnnotations });
579
  setWidgetExport({
580
  generatedXmlText: result.xmlText,
581
  mnemoTitle: result.title || state.widgetExport.mnemoTitle,
 
586
  exportedCount: result.exportedCount,
587
  unmatchedNumbers: result.unmatchedNumbers || [],
588
  missingVisuals: result.missingVisuals || [],
589
+ annotationResults: result.annotationResults || [],
590
  lastGeneratedAt: new Date().toISOString(),
591
  });
592
  setStatus(
 
595
  } catch (error) {
596
  setErrorText(error.message || 'Не удалось сформировать import XML.');
597
  } finally {
598
+ setBusyAction('');
599
  dispatch({ type: A.SET_BUSY, payload: false });
600
  }
601
+ }, [dispatch, setStatus, setWidgetExport, state.annotations, state.imageDataUrl, state.imageName, state.widgetExport]);
602
 
603
  const handleDownloadXml = useCallback(() => {
604
  const xmlText = String(state.widgetExport.generatedXmlText || '').trim();
 
610
  downloadText(xmlText, defaultName, 'application/xml;charset=utf-8');
611
  }, [state.imageName, state.widgetExport.generatedXmlText, state.widgetExport.sheetResolvedName]);
612
 
613
+ const handlePlaceElement = useCallback((item, section) => {
614
+ if (!state.imageDataUrl) return;
615
+ const cx = state.imageWidth / 2;
616
+ const cy = state.imageHeight / 2;
617
+ const w = Number(item.width) || 80;
618
+ const h = Number(item.height) || 40;
619
+ const x = Math.max(0, cx - w / 2);
620
+ const y = Math.max(0, cy - h / 2);
621
+ const points = [
622
+ { x, y }, { x: x + w, y }, { x: x + w, y: y + h }, { x, y: y + h },
623
+ ];
624
+ const basicShape = section === 'basics' ? String(item.name || '').toLowerCase() : '';
625
+ const number = nextAnnotationNumber(state.annotations);
626
+
627
+ const annotation = {
628
+ id: uid('ann'),
629
+ number,
630
+ displayNumber: null,
631
+ bindingUid: '',
632
+ detectedKind: section === 'statics' ? 'shape' : section === 'widgets' ? 'widget' : 'other',
633
+ type: 'rect',
634
+ points,
635
+ source: 'manual',
636
+ confidence: 1.0,
637
+ note: item.displayName || item.label || item.name || '',
638
+ contextPath: '',
639
+ allowDuplicate: false,
640
+ category: section === 'statics' ? 'static' : section === 'widgets' ? 'widget' : 'other',
641
+ widgetNameOverride: section === 'widgets' ? item.name : '',
642
+ staticShapeNameOverride: section === 'statics' ? item.name : '',
643
+ basicShape,
644
+ svgPath: section === 'statics' ? String(item.svgPath || '') : '',
645
+ svgWidth: section === 'statics' ? (Number(item.width) || w) : w,
646
+ svgHeight: section === 'statics' ? (Number(item.height) || h) : h,
647
+ rotation: 0,
648
+ createdAt: Date.now(),
649
+ reviewed: true,
650
+ reviewedAt: Date.now(),
651
+ };
652
+
653
+ dispatch({ type: A.PUSH_HISTORY });
654
+ dispatch({ type: A.ADD_ANNOTATION, payload: annotation });
655
+ setStatus(`Добавлен элемент: ${item.displayName || item.name}`);
656
+ }, [dispatch, setStatus, state.annotations, state.imageDataUrl, state.imageWidth, state.imageHeight]);
657
+
658
  useKeyboard({
659
  onUndo: () => dispatch({ type: A.UNDO }),
660
  onRedo: () => dispatch({ type: A.REDO }),
 
711
  </button>
712
  {hasImage && (
713
  <>
714
+ <button className="btn btn-warn" onClick={handleAutoDetect} disabled={isBusy}>
715
+ {isAnalyzing ? <><span className="spinner" /> Анализ...</> : 'Автораспознавание'}
716
  </button>
717
+ <button className="btn btn-ghost" onClick={handleSelectedOcr} disabled={!selectedAnnotation || isBusy}>
718
+ {isRunningOcr ? <><span className="spinner" /> OCR...</> : 'OCR выделенного'}
719
  </button>
720
+ <button className="btn btn-primary" onClick={handleGenerateImport} disabled={!state.annotations.length || isBusy}>
721
+ {isGenerating ? <><span className="spinner" /> Генерация...</> : 'Сформировать XML'}
722
  </button>
723
  <button className="btn btn-success" onClick={handleDownloadXml} disabled={!hasGeneratedXml}>
724
  Скачать XML
725
  </button>
726
+ <button className="btn btn-ghost" onClick={handleReset} disabled={isBusy}>
727
  Сбросить
728
  </button>
729
  </>
 
736
  <ImageUpload onImageLoad={handleImageLoad} />
737
  <div className="empty-hints">
738
  <h3>Что умеет новая реализация</h3>
739
+ <p>Кнопка `Автораспознавание` запускает тот же локальный детектор, что использует оригинальный `MNEMO OCR HF Demo`, через `src.pipeline_hf.process_single_image`.</p>
740
+ <p>Распознанные элементы попадают в аннотации нового фронта и затем выгружаются в import XML. То, что детектор не нашёл, можно дорисовать вручную и выгрузить тем же путём.</p>
741
  <p>Файл Приложения сервер найдёт автоматически в проекте. При необходимости путь можно переопределить после загрузки изображения.</p>
742
  </div>
743
  </div>
744
  ) : (
745
  <div className="main-content editor-layout">
746
+ <ElementLibraryPanel onPlaceElement={handlePlaceElement} />
747
  <section className="workspace-stack">
748
  <Toolbar
749
  onUndo={() => dispatch({ type: A.UNDO })}
 
761
  <span className="panel-note">
762
  {state.processing.lastAnalyzedAt
763
  ? new Date(state.processing.lastAnalyzedAt).toLocaleString()
764
+ : state.processing.pending ? 'выполняется анализ' : 'ещё не запускалось'}
765
  </span>
766
  </div>
767
 
 
772
  </div>
773
  <div className="summary-item">
774
  <span>Сводка</span>
775
+ <strong>
776
+ {state.processing.pending
777
+ ? 'выполняется анализ'
778
+ : state.processing.ready
779
+ ? formatDetectionCounts(state.processing.counts)
780
+ : 'ожидание анализа'}
781
+ </strong>
782
  </div>
783
  </div>
784
 
 
843
  </div>
844
 
845
  <div className="panel-actions">
846
+ <button className="btn btn-primary" onClick={handleGenerateImport} disabled={!state.annotations.length || isBusy}>
847
+ {isGenerating ? <><span className="spinner" /> ...</> : 'Сформировать XML'}
848
  </button>
849
  <button className="btn btn-success" onClick={handleDownloadXml} disabled={!hasGeneratedXml}>
850
  Скачать
 
889
  )}
890
  </section>
891
 
892
+ {/* XML Preview removed — use Скачать XML button */}
893
 
894
+ <Inspector onOcrSelected={handleSelectedOcr} onDelete={deleteSelected} />
 
 
 
 
 
 
 
 
 
 
 
895
  </aside>
896
  </div>
897
  )}
web-new/src/api.js CHANGED
@@ -1,4 +1,23 @@
1
  const API_BASE = '/.mnemo-studio-api';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  export async function checkHealth() {
4
  const res = await fetch(`${API_BASE}/health`, { cache: 'no-store' });
@@ -20,12 +39,15 @@ export async function runOcr({ imageDataUrl, region = null, lang = 'rus+eng' })
20
 
21
  let res;
22
  try {
23
- res = await fetch(`${API_BASE}/ocr`, {
24
  method: 'POST',
25
  headers: { 'Content-Type': 'application/json' },
26
  body: JSON.stringify(body),
27
- });
28
  } catch (networkError) {
 
 
 
29
  throw new Error('Сервер недоступен. Запустите бэкенд: python app.py');
30
  }
31
 
@@ -55,12 +77,15 @@ export async function saveDatasetSample(payload) {
55
  export async function analyzeLayout(payload) {
56
  let res;
57
  try {
58
- res = await fetch(`${API_BASE}/analyze-layout`, {
59
  method: 'POST',
60
  headers: { 'Content-Type': 'application/json' },
61
  body: JSON.stringify(payload),
62
- });
63
  } catch (networkError) {
 
 
 
64
  throw new Error(
65
  'Не удалось подключиться к серверу. Убедитесь, что бэкенд запущен (python app.py) и доступен на порту 7860.'
66
  );
@@ -74,15 +99,26 @@ export async function analyzeLayout(payload) {
74
  return res.json();
75
  }
76
 
 
 
 
 
 
 
 
 
77
  export async function generateImportXml(payload) {
78
  let res;
79
  try {
80
- res = await fetch(`${API_BASE}/generate-import-xml`, {
81
  method: 'POST',
82
  headers: { 'Content-Type': 'application/json' },
83
  body: JSON.stringify(payload),
84
- });
85
  } catch (networkError) {
 
 
 
86
  throw new Error('Сервер недоступен. Запустите бэкенд: python app.py');
87
  }
88
 
 
1
  const API_BASE = '/.mnemo-studio-api';
2
+ const ANALYZE_TIMEOUT_MS = 0;
3
+ const OCR_TIMEOUT_MS = 30000;
4
+ const XML_TIMEOUT_MS = 60000;
5
+
6
+ async function fetchWithTimeout(url, options = {}, timeoutMs = 30000) {
7
+ if (!timeoutMs || timeoutMs <= 0) {
8
+ return fetch(url, options);
9
+ }
10
+ const controller = new AbortController();
11
+ const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);
12
+ try {
13
+ return await fetch(url, {
14
+ ...options,
15
+ signal: controller.signal,
16
+ });
17
+ } finally {
18
+ window.clearTimeout(timeoutId);
19
+ }
20
+ }
21
 
22
  export async function checkHealth() {
23
  const res = await fetch(`${API_BASE}/health`, { cache: 'no-store' });
 
39
 
40
  let res;
41
  try {
42
+ res = await fetchWithTimeout(`${API_BASE}/ocr`, {
43
  method: 'POST',
44
  headers: { 'Content-Type': 'application/json' },
45
  body: JSON.stringify(body),
46
+ }, OCR_TIMEOUT_MS);
47
  } catch (networkError) {
48
+ if (networkError?.name === 'AbortError') {
49
+ throw new Error('OCR превысил лимит ожидания. Попробуйте меньшую область.');
50
+ }
51
  throw new Error('Сервер недоступен. Запустите бэкенд: python app.py');
52
  }
53
 
 
77
  export async function analyzeLayout(payload) {
78
  let res;
79
  try {
80
+ res = await fetchWithTimeout(`${API_BASE}/analyze-layout`, {
81
  method: 'POST',
82
  headers: { 'Content-Type': 'application/json' },
83
  body: JSON.stringify(payload),
84
+ }, ANALYZE_TIMEOUT_MS);
85
  } catch (networkError) {
86
+ if (networkError?.name === 'AbortError') {
87
+ throw new Error('Анализ выполняется дольше обычного. Проверьте логи backend и дождитесь завершения обработки.');
88
+ }
89
  throw new Error(
90
  'Не удалось подключиться к серверу. Убедитесь, что бэкенд запущен (python app.py) и доступен на порту 7860.'
91
  );
 
99
  return res.json();
100
  }
101
 
102
+ export async function fetchElementLibrary() {
103
+ const res = await fetch(`${API_BASE}/element-library`, { cache: 'no-store' });
104
+ if (!res.ok) {
105
+ throw new Error(`Element library failed: ${res.status}`);
106
+ }
107
+ return res.json();
108
+ }
109
+
110
  export async function generateImportXml(payload) {
111
  let res;
112
  try {
113
+ res = await fetchWithTimeout(`${API_BASE}/generate-import-xml`, {
114
  method: 'POST',
115
  headers: { 'Content-Type': 'application/json' },
116
  body: JSON.stringify(payload),
117
+ }, XML_TIMEOUT_MS);
118
  } catch (networkError) {
119
+ if (networkError?.name === 'AbortError') {
120
+ throw new Error('Генерация XML превысила лимит ожидания.');
121
+ }
122
  throw new Error('Сервер недоступен. Запустите бэкенд: python app.py');
123
  }
124
 
web-new/src/components/ElementLibraryPanel.css ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .element-library-panel {
2
+ display: flex;
3
+ flex-direction: column;
4
+ background: var(--surface);
5
+ border-right: 1px solid var(--border);
6
+ overflow: hidden;
7
+ min-width: 0;
8
+ }
9
+
10
+ .lib-header {
11
+ padding: 8px 10px 6px;
12
+ font-size: 12px;
13
+ font-weight: 600;
14
+ color: var(--text);
15
+ border-bottom: 1px solid var(--border);
16
+ flex-shrink: 0;
17
+ white-space: nowrap;
18
+ overflow: hidden;
19
+ text-overflow: ellipsis;
20
+ }
21
+
22
+ .lib-search {
23
+ padding: 5px 6px;
24
+ flex-shrink: 0;
25
+ }
26
+
27
+ .lib-search input {
28
+ width: 100%;
29
+ box-sizing: border-box;
30
+ padding: 4px 6px;
31
+ font-size: 11px;
32
+ border: 1px solid var(--border);
33
+ border-radius: 4px;
34
+ background: var(--surface-alt, #1e293b);
35
+ color: var(--text);
36
+ outline: none;
37
+ }
38
+
39
+ .lib-search input:focus {
40
+ border-color: var(--accent);
41
+ }
42
+
43
+ .lib-sections-scroll {
44
+ flex: 1;
45
+ overflow-y: auto;
46
+ overflow-x: hidden;
47
+ }
48
+
49
+ .lib-section {
50
+ border-bottom: 1px solid var(--border);
51
+ }
52
+
53
+ .lib-section-header {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 4px;
57
+ width: 100%;
58
+ padding: 5px 8px;
59
+ font-size: 10px;
60
+ font-weight: 600;
61
+ color: var(--text-muted);
62
+ background: transparent;
63
+ border: none;
64
+ cursor: pointer;
65
+ text-align: left;
66
+ }
67
+
68
+ .lib-section-header:hover {
69
+ background: var(--surface-hover, rgba(255,255,255,0.04));
70
+ }
71
+
72
+ .lib-section-arrow {
73
+ font-size: 9px;
74
+ width: 10px;
75
+ flex-shrink: 0;
76
+ }
77
+
78
+ .lib-section-title {
79
+ flex: 1;
80
+ overflow: hidden;
81
+ text-overflow: ellipsis;
82
+ white-space: nowrap;
83
+ }
84
+
85
+ .lib-section-count {
86
+ font-size: 9px;
87
+ color: var(--text-muted);
88
+ opacity: 0.6;
89
+ }
90
+
91
+ .lib-section-body {
92
+ padding: 2px 4px 6px;
93
+ }
94
+
95
+ .lib-grid {
96
+ display: grid;
97
+ grid-template-columns: repeat(2, 1fr);
98
+ gap: 2px;
99
+ }
100
+
101
+ .lib-item {
102
+ display: flex;
103
+ flex-direction: column;
104
+ align-items: center;
105
+ gap: 2px;
106
+ padding: 4px 2px;
107
+ background: transparent;
108
+ border: 1px solid transparent;
109
+ border-radius: 3px;
110
+ cursor: pointer;
111
+ color: var(--text);
112
+ transition: background 0.12s, border-color 0.12s;
113
+ overflow: hidden;
114
+ }
115
+
116
+ .lib-item:hover {
117
+ background: var(--surface-hover, rgba(255,255,255,0.06));
118
+ border-color: var(--border);
119
+ }
120
+
121
+ .lib-item:active {
122
+ background: rgba(255,255,255,0.1);
123
+ }
124
+
125
+ .lib-icon {
126
+ width: 30px;
127
+ height: 30px;
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ flex-shrink: 0;
132
+ }
133
+
134
+ .lib-icon svg,
135
+ .lib-icon .lib-thumb {
136
+ width: 100%;
137
+ height: 100%;
138
+ color: var(--text-muted);
139
+ }
140
+
141
+ .lib-name {
142
+ font-size: 8px;
143
+ line-height: 1.1;
144
+ text-align: center;
145
+ color: var(--text-muted);
146
+ overflow: hidden;
147
+ text-overflow: ellipsis;
148
+ white-space: nowrap;
149
+ max-width: 100%;
150
+ width: 100%;
151
+ }
152
+
153
+ .lib-loading,
154
+ .lib-error {
155
+ padding: 16px 10px;
156
+ font-size: 11px;
157
+ color: var(--text-muted);
158
+ text-align: center;
159
+ }
160
+
161
+ .lib-error {
162
+ color: var(--danger);
163
+ }
164
+
165
+ @media (max-width: 1200px) {
166
+ .element-library-panel {
167
+ display: none;
168
+ }
169
+ }
web-new/src/components/ElementLibraryPanel.jsx ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { memo, useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { fetchElementLibrary } from '../api';
3
+ import * as A from '../store/actions';
4
+ import { useDispatch, useStore } from '../store/context';
5
+ import './ElementLibraryPanel.css';
6
+
7
+
8
+ const SECTION_ORDER = ['basics', 'statics', 'widgets'];
9
+ const SECTION_LABELS = {
10
+ basics: 'Базовые фигуры',
11
+ statics: 'Статические шейпы',
12
+ widgets: 'Динамические виджеты',
13
+ };
14
+
15
+ const BASIC_ICONS = {
16
+ rectangle: (
17
+ <svg viewBox="0 0 32 24"><rect x="2" y="2" width="28" height="20" fill="none" stroke="currentColor" strokeWidth="1.5" /></svg>
18
+ ),
19
+ text: (
20
+ <svg viewBox="0 0 32 24"><text x="16" y="17" textAnchor="middle" fontSize="14" fill="currentColor">T</text></svg>
21
+ ),
22
+ line: (
23
+ <svg viewBox="0 0 32 24"><line x1="4" y1="20" x2="28" y2="4" stroke="currentColor" strokeWidth="1.5" /></svg>
24
+ ),
25
+ arrow: (
26
+ <svg viewBox="0 0 32 24"><line x1="4" y1="12" x2="24" y2="12" stroke="currentColor" strokeWidth="1.5" /><path d="M 22 8 L 28 12 L 22 16" fill="none" stroke="currentColor" strokeWidth="1.5" /></svg>
27
+ ),
28
+ ellipse: (
29
+ <svg viewBox="0 0 32 24"><ellipse cx="16" cy="12" rx="13" ry="9" fill="none" stroke="currentColor" strokeWidth="1.5" /></svg>
30
+ ),
31
+ table: (
32
+ <svg viewBox="0 0 32 24"><rect x="2" y="2" width="28" height="20" fill="none" stroke="currentColor" strokeWidth="1.5" /><line x1="2" y1="9" x2="30" y2="9" stroke="currentColor" strokeWidth="1" /><line x1="16" y1="9" x2="16" y2="22" stroke="currentColor" strokeWidth="1" /></svg>
33
+ ),
34
+ };
35
+
36
+
37
+ function StaticShapeThumbnail({ svgPath, width, height }) {
38
+ if (!svgPath) {
39
+ return (
40
+ <svg viewBox="0 0 32 32" className="lib-thumb">
41
+ <rect x="4" y="4" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="1" strokeDasharray="3 2" />
42
+ </svg>
43
+ );
44
+ }
45
+
46
+ const vw = width || 32;
47
+ const vh = height || 32;
48
+ const pad = 2;
49
+
50
+ return (
51
+ <svg viewBox={`${-pad} ${-pad} ${vw + pad * 2} ${vh + pad * 2}`} className="lib-thumb">
52
+ <path d={svgPath} fill="none" stroke="currentColor" strokeWidth={Math.max(0.8, vw / 32)} />
53
+ </svg>
54
+ );
55
+ }
56
+
57
+
58
+ const LibraryItem = memo(function LibraryItem({ item, section, onClick }) {
59
+ const handleClick = useCallback(() => {
60
+ onClick(item, section);
61
+ }, [item, section, onClick]);
62
+
63
+ if (section === 'basics') {
64
+ return (
65
+ <button className="lib-item" onClick={handleClick} title={item.label}>
66
+ <div className="lib-icon">{BASIC_ICONS[item.icon] || BASIC_ICONS.rectangle}</div>
67
+ <span className="lib-name">{item.label}</span>
68
+ </button>
69
+ );
70
+ }
71
+
72
+ if (section === 'statics') {
73
+ return (
74
+ <button className="lib-item" onClick={handleClick} title={item.displayName}>
75
+ <div className="lib-icon">
76
+ <StaticShapeThumbnail svgPath={item.svgPath} width={item.width} height={item.height} />
77
+ </div>
78
+ <span className="lib-name">{item.displayName}</span>
79
+ </button>
80
+ );
81
+ }
82
+
83
+ const catColor = {
84
+ equipment: '#f97316',
85
+ valve: '#8b5cf6',
86
+ indicator: '#22c55e',
87
+ value: '#eab308',
88
+ table: '#60a5fa',
89
+ static: '#94a3b8',
90
+ arrow: '#ff4f87',
91
+ other: '#6b7280',
92
+ }[item.category] || '#6b7280';
93
+
94
+ return (
95
+ <button className="lib-item" onClick={handleClick} title={item.displayName}>
96
+ <div className="lib-icon">
97
+ <svg viewBox="0 0 32 32" className="lib-thumb">
98
+ <rect x="2" y="2" width="28" height="28" rx="3" fill={catColor} opacity="0.15" stroke={catColor} strokeWidth="1" />
99
+ <text x="16" y="18" textAnchor="middle" fontSize="8" fill={catColor} fontWeight="bold">W</text>
100
+ </svg>
101
+ </div>
102
+ <span className="lib-name">{item.displayName}</span>
103
+ </button>
104
+ );
105
+ });
106
+
107
+
108
+ function CollapsibleSection({ title, count, defaultOpen, children }) {
109
+ const [open, setOpen] = useState(defaultOpen ?? true);
110
+
111
+ return (
112
+ <div className={`lib-section ${open ? 'open' : 'closed'}`}>
113
+ <button className="lib-section-header" onClick={() => setOpen((v) => !v)}>
114
+ <span className="lib-section-arrow">{open ? '▾' : '▸'}</span>
115
+ <span className="lib-section-title">{title}</span>
116
+ <span className="lib-section-count">{count}</span>
117
+ </button>
118
+ {open && <div className="lib-section-body">{children}</div>}
119
+ </div>
120
+ );
121
+ }
122
+
123
+
124
+ export default function ElementLibraryPanel({ onPlaceElement }) {
125
+ const state = useStore();
126
+ const dispatch = useDispatch();
127
+ const [search, setSearch] = useState('');
128
+ const [error, setError] = useState('');
129
+
130
+ useEffect(() => {
131
+ if (state.elementLibrary.loaded) return;
132
+ let cancelled = false;
133
+ fetchElementLibrary()
134
+ .then((data) => {
135
+ if (!cancelled) {
136
+ dispatch({ type: A.SET_ELEMENT_LIBRARY, payload: data });
137
+ }
138
+ })
139
+ .catch((err) => {
140
+ if (!cancelled) setError(err.message);
141
+ });
142
+ return () => { cancelled = true; };
143
+ }, [state.elementLibrary.loaded, dispatch]);
144
+
145
+ const filtered = useMemo(() => {
146
+ const q = search.trim().toLowerCase();
147
+ const lib = state.elementLibrary;
148
+ if (!q) return lib;
149
+ return {
150
+ basics: lib.basics.filter((i) => (i.label || i.name).toLowerCase().includes(q)),
151
+ statics: lib.statics.filter((i) => (i.displayName || i.name).toLowerCase().includes(q)),
152
+ widgets: lib.widgets.filter((i) => (i.displayName || i.name).toLowerCase().includes(q)),
153
+ };
154
+ }, [search, state.elementLibrary]);
155
+
156
+ const handleClick = useCallback((item, section) => {
157
+ if (onPlaceElement) onPlaceElement(item, section);
158
+ }, [onPlaceElement]);
159
+
160
+ if (error) {
161
+ return (
162
+ <aside className="element-library-panel">
163
+ <div className="lib-header">Библиотека</div>
164
+ <div className="lib-error">{error}</div>
165
+ </aside>
166
+ );
167
+ }
168
+
169
+ if (!state.elementLibrary.loaded) {
170
+ return (
171
+ <aside className="element-library-panel">
172
+ <div className="lib-header">Библиотека</div>
173
+ <div className="lib-loading"><span className="spinner" /> Загрузка...</div>
174
+ </aside>
175
+ );
176
+ }
177
+
178
+ return (
179
+ <aside className="element-library-panel">
180
+ <div className="lib-header">Библиотека</div>
181
+ <div className="lib-search">
182
+ <input
183
+ type="text"
184
+ placeholder="Поиск..."
185
+ value={search}
186
+ onChange={(e) => setSearch(e.target.value)}
187
+ />
188
+ </div>
189
+
190
+ <div className="lib-sections-scroll">
191
+ {SECTION_ORDER.map((section) => {
192
+ const items = filtered[section] || [];
193
+ if (!items.length && search) return null;
194
+ return (
195
+ <CollapsibleSection
196
+ key={section}
197
+ title={SECTION_LABELS[section]}
198
+ count={items.length}
199
+ defaultOpen={section === 'basics'}
200
+ >
201
+ <div className="lib-grid">
202
+ {items.map((item) => (
203
+ <LibraryItem
204
+ key={item.name}
205
+ item={item}
206
+ section={section}
207
+ onClick={handleClick}
208
+ />
209
+ ))}
210
+ </div>
211
+ </CollapsibleSection>
212
+ );
213
+ })}
214
+ </div>
215
+ </aside>
216
+ );
217
+ }
web-new/src/components/Inspector.jsx CHANGED
@@ -1,9 +1,72 @@
1
  import { useCallback } from 'react';
2
  import { useStore, useDispatch } from '../store/context';
3
  import * as A from '../store/actions';
4
- import { ALLOWED_CATEGORIES, CATEGORY_LABELS, CATEGORY_COLORS } from '../constants';
5
  import { boundsOfPoints, polygonArea, round } from '../utils/geometry';
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  export default function Inspector({ onOcrSelected, onDelete }) {
8
  const state = useStore();
9
  const dispatch = useDispatch();
@@ -19,11 +82,28 @@ export default function Inspector({ onOcrSelected, onDelete }) {
19
  });
20
  }, [dispatch, selectedId]);
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  const filtered = annotations.filter((a) => {
23
  if (!filterText) return true;
24
  const q = filterText.toLowerCase();
25
  return (
26
  String(a.number).includes(q)
 
27
  || (a.bindingUid || '').includes(q)
28
  || (a.note || '').toLowerCase().includes(q)
29
  || (a.category || '').toLowerCase().includes(q)
@@ -47,12 +127,17 @@ export default function Inspector({ onOcrSelected, onDelete }) {
47
  {selectedAnn ? (
48
  <div className="inspector-editor">
49
  <div className="inspector-badges">
50
- <span className="badge" style={{ background: `${CATEGORY_COLORS[selectedAnn.category]}22`, color: CATEGORY_COLORS[selectedAnn.category] }}>
51
- {CATEGORY_LABELS[selectedAnn.category] || selectedAnn.category}
52
  </span>
53
- <span className="inspector-meta">#{selectedAnn.number}</span>
54
  {selectedAnn.detectedKind && <span className="inspector-meta">{selectedAnn.detectedKind}</span>}
55
  {selectedAnn.bindingUid && <span className="inspector-meta uid">UID {selectedAnn.bindingUid}</span>}
 
 
 
 
 
56
  </div>
57
 
58
  <div className="panel-grid">
@@ -81,11 +166,11 @@ export default function Inspector({ onOcrSelected, onDelete }) {
81
  <label className="field">
82
  <span>Категория</span>
83
  <select
84
- value={selectedAnn.category}
85
- onChange={(e) => updateField('category', e.target.value)}
86
  >
87
- {ALLOWED_CATEGORIES.map((c) => (
88
- <option key={c} value={c}>{CATEGORY_LABELS[c]}</option>
89
  ))}
90
  </select>
91
  </label>
@@ -144,6 +229,11 @@ export default function Inspector({ onOcrSelected, onDelete }) {
144
  return `${round(b.width)}×${round(b.height)} px · ${selectedAnn.points.length} точек · ${selectedAnn.source}`;
145
  })()}
146
  </div>
 
 
 
 
 
147
  </div>
148
  ) : (
149
  <div className="inspector-empty">Выберите элемент для редактирования</div>
@@ -175,14 +265,19 @@ export default function Inspector({ onOcrSelected, onDelete }) {
175
  className={`ann-item${ann.id === selectedId ? ' active' : ''}`}
176
  onClick={() => dispatch({ type: A.SELECT_ANNOTATION, payload: ann.id })}
177
  >
178
- <span className="ann-num" style={{ color: CATEGORY_COLORS[ann.category] }}>
179
- {ann.number}
180
  </span>
181
  <div className="ann-body">
182
- <div className="ann-title">
183
- {CATEGORY_LABELS[ann.category] || ann.category}
184
- {ann.bindingUid && <span className="ann-uid">UID {ann.bindingUid}</span>}
185
- </div>
 
 
 
 
 
186
  <div className="ann-meta">
187
  {ann.note && <span>{ann.note.slice(0, 30)}</span>}
188
  {ann.contextPath && <span>{ann.contextPath.split('\\').pop()}</span>}
 
1
  import { useCallback } from 'react';
2
  import { useStore, useDispatch } from '../store/context';
3
  import * as A from '../store/actions';
4
+ import { CATEGORY_SELECT_OPTIONS, CATEGORY_LABELS, CATEGORY_COLORS } from '../constants';
5
  import { boundsOfPoints, polygonArea, round } from '../utils/geometry';
6
 
7
+ function annotationLabel(annotation) {
8
+ const detectedKind = String(annotation?.detectedKind || '').trim().toLowerCase();
9
+ const category = String(annotation?.category || '').trim().toLowerCase();
10
+ const isNumberedAuto = annotation?.source === 'auto' && (
11
+ detectedKind === 'widget'
12
+ || detectedKind === 'shape'
13
+ || detectedKind === 'indicator'
14
+ || category === 'widget'
15
+ || category === 'indicator'
16
+ );
17
+ if (annotation?.source === 'auto' && !isNumberedAuto) return '';
18
+ const displayNumber = Number.parseInt(String(annotation?.displayNumber ?? ''), 10);
19
+ if (Number.isFinite(displayNumber) && displayNumber > 0) return String(displayNumber);
20
+ const bindingUid = String(annotation?.bindingUid || '').trim();
21
+ if (bindingUid) return bindingUid;
22
+ if (annotation?.source === 'auto') return '';
23
+ const localNumber = Number.parseInt(String(annotation?.number ?? ''), 10);
24
+ if (Number.isFinite(localNumber) && localNumber > 0) return String(localNumber);
25
+ return '';
26
+ }
27
+
28
+ function inspectorMetaLabel(annotation) {
29
+ const publicLabel = annotationLabel(annotation);
30
+ if (publicLabel) return `#${publicLabel}`;
31
+ const localNumber = Number.parseInt(String(annotation?.number ?? ''), 10);
32
+ if (Number.isFinite(localNumber) && localNumber > 0) return `лок. #${localNumber}`;
33
+ return 'без номера';
34
+ }
35
+
36
+ function xmlStatusLabel(annotation) {
37
+ if (annotation?.xmlNeedsAttention) return 'XML: проверить';
38
+ if (annotation?.xmlExported) return annotation?.xmlMatched ? 'XML: экспортирован' : 'XML: экспортирован без Excel';
39
+ return '';
40
+ }
41
+
42
+ function annotationCategoryOptionId(annotation) {
43
+ const kind = String(annotation?.detectedKind || '').trim().toLowerCase();
44
+ const category = String(annotation?.category || '').trim().toLowerCase();
45
+ if (kind === 'shape') return 'static-shape';
46
+ if (kind === 'arrow') return 'static-arrow';
47
+ if (kind === 'form' || kind.startsWith('text-')) return 'static-form';
48
+ if (kind === 'widget' || category === 'widget') return 'widget';
49
+ if (category === 'static') return 'static-form';
50
+ return category || 'widget';
51
+ }
52
+
53
+ function annotationCategoryOption(annotation) {
54
+ const optionId = annotationCategoryOptionId(annotation);
55
+ return CATEGORY_SELECT_OPTIONS.find((option) => option.id === optionId) || null;
56
+ }
57
+
58
+ function annotationCategoryLabel(annotation) {
59
+ const option = annotationCategoryOption(annotation);
60
+ if (option) return option.label;
61
+ return CATEGORY_LABELS[annotation?.category] || String(annotation?.category || 'Другое');
62
+ }
63
+
64
+ function annotationCategoryColor(annotation) {
65
+ const option = annotationCategoryOption(annotation);
66
+ if (option?.color) return option.color;
67
+ return CATEGORY_COLORS[annotation?.category] || CATEGORY_COLORS.other;
68
+ }
69
+
70
  export default function Inspector({ onOcrSelected, onDelete }) {
71
  const state = useStore();
72
  const dispatch = useDispatch();
 
82
  });
83
  }, [dispatch, selectedId]);
84
 
85
+ const updateCategoryOption = useCallback((optionId) => {
86
+ if (!selectedId) return;
87
+ const option = CATEGORY_SELECT_OPTIONS.find((item) => item.id === optionId);
88
+ if (!option) return;
89
+ dispatch({
90
+ type: A.UPDATE_ANNOTATION,
91
+ payload: {
92
+ id: selectedId,
93
+ changes: {
94
+ category: option.category,
95
+ detectedKind: option.detectedKind,
96
+ },
97
+ },
98
+ });
99
+ }, [dispatch, selectedId]);
100
+
101
  const filtered = annotations.filter((a) => {
102
  if (!filterText) return true;
103
  const q = filterText.toLowerCase();
104
  return (
105
  String(a.number).includes(q)
106
+ || String(a.displayNumber || '').includes(q)
107
  || (a.bindingUid || '').includes(q)
108
  || (a.note || '').toLowerCase().includes(q)
109
  || (a.category || '').toLowerCase().includes(q)
 
127
  {selectedAnn ? (
128
  <div className="inspector-editor">
129
  <div className="inspector-badges">
130
+ <span className="badge" style={{ background: `${annotationCategoryColor(selectedAnn)}22`, color: annotationCategoryColor(selectedAnn) }}>
131
+ {annotationCategoryLabel(selectedAnn)}
132
  </span>
133
+ <span className="inspector-meta">{inspectorMetaLabel(selectedAnn)}</span>
134
  {selectedAnn.detectedKind && <span className="inspector-meta">{selectedAnn.detectedKind}</span>}
135
  {selectedAnn.bindingUid && <span className="inspector-meta uid">UID {selectedAnn.bindingUid}</span>}
136
+ {xmlStatusLabel(selectedAnn) && (
137
+ <span className={`inspector-meta ${selectedAnn.xmlNeedsAttention ? 'xml-warn' : 'xml-ok'}`}>
138
+ {xmlStatusLabel(selectedAnn)}
139
+ </span>
140
+ )}
141
  </div>
142
 
143
  <div className="panel-grid">
 
166
  <label className="field">
167
  <span>Категория</span>
168
  <select
169
+ value={annotationCategoryOptionId(selectedAnn)}
170
+ onChange={(e) => updateCategoryOption(e.target.value)}
171
  >
172
+ {CATEGORY_SELECT_OPTIONS.map((option) => (
173
+ <option key={option.id} value={option.id}>{option.label}</option>
174
  ))}
175
  </select>
176
  </label>
 
229
  return `${round(b.width)}×${round(b.height)} px · ${selectedAnn.points.length} точек · ${selectedAnn.source}`;
230
  })()}
231
  </div>
232
+ {!!selectedAnn.xmlMissingVisuals?.length && (
233
+ <div className="inspector-geo">
234
+ XML fallback: {selectedAnn.xmlMissingVisuals.join(', ')}
235
+ </div>
236
+ )}
237
  </div>
238
  ) : (
239
  <div className="inspector-empty">Выберите элемент для редактирования</div>
 
265
  className={`ann-item${ann.id === selectedId ? ' active' : ''}`}
266
  onClick={() => dispatch({ type: A.SELECT_ANNOTATION, payload: ann.id })}
267
  >
268
+ <span className="ann-num" style={{ color: annotationCategoryColor(ann) }}>
269
+ {annotationLabel(ann)}
270
  </span>
271
  <div className="ann-body">
272
+ <div className="ann-title">
273
+ {annotationCategoryLabel(ann)}
274
+ {ann.bindingUid && <span className="ann-uid">UID {ann.bindingUid}</span>}
275
+ {xmlStatusLabel(ann) && (
276
+ <span className={`ann-xml-status ${ann.xmlNeedsAttention ? 'warn' : 'ok'}`}>
277
+ {ann.xmlNeedsAttention ? 'проверить' : 'xml'}
278
+ </span>
279
+ )}
280
+ </div>
281
  <div className="ann-meta">
282
  {ann.note && <span>{ann.note.slice(0, 30)}</span>}
283
  {ann.contextPath && <span>{ann.contextPath.split('\\').pop()}</span>}
web-new/src/components/SvgWorkspace.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
  import { useStore, useDispatch } from '../store/context';
3
  import * as A from '../store/actions';
4
  import { TOOLS, MODES, MIN_RECT_SIZE, LASSO_STEP, MOVE_THRESHOLD } from '../constants';
@@ -15,14 +15,31 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
15
  const MIN_ZOOM = 0.1;
16
  const MAX_ZOOM = 5;
17
  const DETECTED_KIND_COLORS = Object.freeze({
18
- cell: '#3158ff',
19
- group: '#00d71c',
 
20
  arrow: '#ff4f87',
21
- equipment: '#f97316',
22
  indicator: '#22c55e',
 
 
 
 
23
  other: '#94a3b8',
24
  });
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  function annotationColor(ann) {
27
  if (ann.source === 'auto') {
28
  const detected = String(ann.detectedKind || '').trim().toLowerCase();
@@ -36,12 +53,572 @@ function categoryFill(category) {
36
  return CATEGORY_COLORS[category] || CATEGORY_COLORS.other;
37
  }
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  export default function SvgWorkspace() {
40
  const state = useStore();
41
  const dispatch = useDispatch();
42
  const {
43
  interactionRef, findAnnotationAtPoint, createAnnotation,
44
- updateAnnotationPoints, deleteSelected,
45
  } = useAnnotationTools();
46
 
47
  const wrapRef = useRef(null);
@@ -51,10 +628,69 @@ export default function SvgWorkspace() {
51
  const [tempRect, setTempRect] = useState(null);
52
  const [tempPoints, setTempPoints] = useState([]);
53
  const [pointerNow, setPointerNow] = useState(null);
 
 
 
 
54
  const isPanning = useRef(false);
55
  const panStart = useRef({ x: 0, y: 0 });
56
 
57
  const { imageWidth, imageHeight, imageDataUrl, tool, mode, annotations, selectedId } = state;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
  useEffect(() => {
60
  if (!imageWidth || !imageHeight || !wrapRef.current) return;
@@ -94,6 +730,26 @@ export default function SvgWorkspace() {
94
  });
95
  }, [dispatch]);
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  const handlePointerDown = useCallback((e) => {
98
  const point = getSvgPoint(e);
99
  const ir = interactionRef.current;
@@ -107,16 +763,40 @@ export default function SvgWorkspace() {
107
 
108
  if (e.button !== 0) return;
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  const handleEl = e.target.closest('[data-handle-index]');
111
  if (handleEl && tool === TOOLS.SELECT) {
112
  const idx = parseInt(handleEl.dataset.handleIndex, 10);
113
  const annId = handleEl.dataset.annotationId;
114
  const ann = annotations.find((a) => a.id === annId);
115
  if (ann) {
116
- dispatch({ type: A.PUSH_HISTORY });
117
  ir.dragVertexIndex = idx;
118
  ir.dragTarget = annId;
119
  ir.dragOriginalPoints = ann.points.map((p) => ({ ...p }));
 
 
 
120
  dispatch({ type: A.SET_MODE, payload: MODES.VERTEX });
121
  e.stopPropagation();
122
  return;
@@ -128,14 +808,16 @@ export default function SvgWorkspace() {
128
  if (hit) {
129
  dispatch({ type: A.SELECT_ANNOTATION, payload: hit.id });
130
  if (hit.id === selectedId) {
131
- dispatch({ type: A.PUSH_HISTORY });
132
  ir.dragTarget = hit.id;
133
  ir.dragOriginalPoints = hit.points.map((p) => ({ ...p }));
134
  ir.pointerStart = point;
135
  ir.dragCommitted = false;
 
 
136
  dispatch({ type: A.SET_MODE, payload: MODES.MOVE });
137
  }
138
  } else {
 
139
  dispatch({ type: A.SELECT_ANNOTATION, payload: null });
140
  }
141
  return;
@@ -174,7 +856,18 @@ export default function SvgWorkspace() {
174
  dispatch({ type: A.SET_MODE, payload: MODES.LASSO_DRAW });
175
  return;
176
  }
177
- }, [tool, mode, annotations, selectedId, getSvgPoint, dispatch, findAnnotationAtPoint, interactionRef]);
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  const handlePointerMove = useCallback((e) => {
180
  const point = getSvgPoint(e);
@@ -188,7 +881,9 @@ export default function SvgWorkspace() {
188
  return;
189
  }
190
 
191
- setPointerNow(point);
 
 
192
 
193
  if (mode === MODES.DRAW_RECT && ir.pointerStart) {
194
  setTempRect({
@@ -212,38 +907,75 @@ export default function SvgWorkspace() {
212
  return;
213
  }
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  if (mode === MODES.MOVE && ir.dragTarget && ir.pointerStart) {
216
  const dx = point.x - ir.pointerStart.x;
217
  const dy = point.y - ir.pointerStart.y;
218
  if (!ir.dragCommitted && Math.hypot(dx, dy) < MOVE_THRESHOLD) return;
 
 
 
 
219
  ir.dragCommitted = true;
220
  const moved = ir.dragOriginalPoints.map((p) => ({
221
  x: clamp(p.x + dx, 0, imageWidth),
222
  y: clamp(p.y + dy, 0, imageHeight),
223
  }));
224
- dispatch({
225
- type: A.UPDATE_ANNOTATION,
226
- payload: { id: ir.dragTarget, changes: { points: moved } },
227
- });
228
  return;
229
  }
230
 
231
  if (mode === MODES.VERTEX && ir.dragTarget != null) {
232
  const ann = annotations.find((a) => a.id === ir.dragTarget);
233
  if (ann) {
234
- const newPoints = ann.points.map((p, i) =>
 
 
 
 
 
 
235
  i === ir.dragVertexIndex
236
  ? { x: clamp(point.x, 0, imageWidth), y: clamp(point.y, 0, imageHeight) }
237
  : p
238
  );
239
- dispatch({
240
- type: A.UPDATE_ANNOTATION,
241
- payload: { id: ir.dragTarget, changes: { points: newPoints } },
242
- });
243
  }
244
  return;
245
  }
246
- }, [mode, getSvgPoint, imageWidth, imageHeight, dispatch, annotations, interactionRef]);
247
 
248
  const handlePointerUp = useCallback(() => {
249
  const ir = interactionRef.current;
@@ -284,20 +1016,55 @@ export default function SvgWorkspace() {
284
  }
285
 
286
  if (mode === MODES.MOVE) {
 
 
 
 
 
287
  ir.dragTarget = null;
288
  ir.dragOriginalPoints = null;
 
 
289
  ir.dragCommitted = false;
 
 
290
  dispatch({ type: A.SET_MODE, payload: MODES.IDLE });
291
  return;
292
  }
293
 
294
  if (mode === MODES.VERTEX) {
 
 
 
 
295
  ir.dragTarget = null;
296
  ir.dragVertexIndex = -1;
 
 
 
 
 
297
  dispatch({ type: A.SET_MODE, payload: MODES.IDLE });
298
  return;
299
  }
300
- }, [mode, tempRect, createAnnotation, dispatch, interactionRef]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
  const finishPolygon = useCallback(() => {
303
  const ir = interactionRef.current;
@@ -320,14 +1087,20 @@ export default function SvgWorkspace() {
320
  interactionRef.current.tempPoints = [];
321
  setTempPoints([]);
322
  setTempRect(null);
 
 
 
 
 
 
 
 
323
  dispatch({ type: A.SET_MODE, payload: MODES.IDLE });
324
  }
325
  }
326
  window.addEventListener('keydown', handleKey);
327
  return () => window.removeEventListener('keydown', handleKey);
328
- }, [mode, finishPolygon, dispatch, interactionRef]);
329
-
330
- const scaleInvariant = useCallback((size) => size / zoom, [zoom]);
331
 
332
  if (!imageDataUrl) {
333
  return (
@@ -343,18 +1116,6 @@ export default function SvgWorkspace() {
343
  const svgW = imageWidth * zoom;
344
  const svgH = imageHeight * zoom;
345
 
346
- const selectedAnn = annotations.find((a) => a.id === selectedId);
347
- const sorted = [...annotations].sort((a, b) => polygonArea(b.points) - polygonArea(a.points));
348
- const processingCounts = state.processing?.counts || {};
349
- const processingSummary = [
350
- `группы: ${Number(processingCounts.groups) || 0}`,
351
- `ячейки: ${Number(processingCounts.cells) || 0}`,
352
- `стрелки: ${Number(processingCounts.arrows) || 0}`,
353
- `оборудование: ${Number(processingCounts.equipment) || 0}`,
354
- `индикаторы: ${Number(processingCounts.indicators) || 0}`,
355
- `прочее: ${Number(processingCounts.other) || 0}`,
356
- ].join(', ');
357
-
358
  return (
359
  <div className="viewer-panel" style={{ display: 'flex', flexDirection: 'column' }}>
360
  <div
@@ -393,92 +1154,41 @@ export default function SvgWorkspace() {
393
  </defs>
394
  <image href={imageDataUrl} x="0" y="0" width={imageWidth} height={imageHeight} />
395
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  {/* Annotations layer */}
397
  <g id="annotationsLayer">
398
  {sorted.map((ann) => {
399
  const isSelected = ann.id === selectedId;
400
- const strokeColor = annotationColor(ann);
401
- const fillColor = strokeColor;
402
- const opacity = 0.3 + (ann.confidence || 0.5) * 0.7;
403
- const bounds = boundsOfPoints(ann.points);
404
- const fontSize = Math.max(10, 13 / zoom);
405
- const detectedKind = String(ann.detectedKind || '').trim().toLowerCase();
406
- const showIndexLabel = detectedKind !== 'group';
407
- const dashArray = detectedKind === 'group'
408
- ? `${8 / zoom} ${5 / zoom}`
409
- : detectedKind === 'arrow'
410
- ? `${4 / zoom} ${3 / zoom}`
411
- : 'none';
412
- const strokeWidth = isSelected
413
- ? 2.5 / zoom
414
- : detectedKind === 'group'
415
- ? 2.2 / zoom
416
- : 1.8 / zoom;
417
-
418
  return (
419
- <g key={ann.id} data-id={ann.id}>
420
- <path
421
- d={pathFromPoints(ann.points)}
422
- fill={isSelected ? `${fillColor}22` : 'none'}
423
- stroke={isSelected ? '#fff' : strokeColor}
424
- strokeWidth={strokeWidth}
425
- strokeDasharray={isSelected ? `${4 / zoom} ${3 / zoom}` : dashArray}
426
- opacity={opacity}
427
- />
428
- {/* Label */}
429
- {showIndexLabel && (
430
- <>
431
- <circle
432
- cx={bounds.x}
433
- cy={bounds.y}
434
- r={Math.max(8, 10 / zoom)}
435
- fill={fillColor}
436
- opacity="0.85"
437
- filter="url(#labelShadow)"
438
- />
439
- <text
440
- x={bounds.x}
441
- y={bounds.y}
442
- textAnchor="middle"
443
- dominantBaseline="central"
444
- fill="white"
445
- fontSize={fontSize}
446
- fontWeight="bold"
447
- stroke="rgba(0,0,0,0.4)"
448
- strokeWidth={0.8 / zoom}
449
- paintOrder="stroke"
450
- >
451
- {ann.number}
452
- </text>
453
- </>
454
- )}
455
- {/* Selection overlay */}
456
- {isSelected && (
457
- <>
458
- <rect
459
- x={bounds.x} y={bounds.y}
460
- width={bounds.width} height={bounds.height}
461
- fill="none"
462
- stroke="rgba(255,255,255,0.5)"
463
- strokeWidth={1 / zoom}
464
- strokeDasharray={`${4 / zoom} ${3 / zoom}`}
465
- />
466
- {ann.points.map((pt, i) => (
467
- <circle
468
- key={i}
469
- cx={pt.x} cy={pt.y}
470
- r={5 / zoom}
471
- fill="white"
472
- stroke="rgba(0,0,0,0.8)"
473
- strokeWidth={1.5 / zoom}
474
- data-handle-index={i}
475
- data-annotation-id={ann.id}
476
- style={{ cursor: 'move' }}
477
- />
478
- ))}
479
- </>
480
- )}
481
- </g>
482
  );
483
  })}
484
  </g>
 
1
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
  import { useStore, useDispatch } from '../store/context';
3
  import * as A from '../store/actions';
4
  import { TOOLS, MODES, MIN_RECT_SIZE, LASSO_STEP, MOVE_THRESHOLD } from '../constants';
 
15
  const MIN_ZOOM = 0.1;
16
  const MAX_ZOOM = 5;
17
  const DETECTED_KIND_COLORS = Object.freeze({
18
+ widget: '#3158ff',
19
+ form: '#00d71c',
20
+ shape: '#f97316',
21
  arrow: '#ff4f87',
 
22
  indicator: '#22c55e',
23
+ 'text-label': '#e879f9',
24
+ 'text-title': '#c026d3',
25
+ 'text-equipment': '#f97316',
26
+ 'text-parameter': '#06b6d4',
27
  other: '#94a3b8',
28
  });
29
 
30
+ const KIND_BADGE_LABELS = Object.freeze({
31
+ widget: 'В',
32
+ form: 'Ф',
33
+ shape: 'Ш',
34
+ arrow: 'С',
35
+ indicator: 'И',
36
+ 'text-label': 'Т',
37
+ 'text-title': 'Т',
38
+ 'text-equipment': 'Т',
39
+ 'text-parameter': 'Т',
40
+ other: '?',
41
+ });
42
+
43
  function annotationColor(ann) {
44
  if (ann.source === 'auto') {
45
  const detected = String(ann.detectedKind || '').trim().toLowerCase();
 
53
  return CATEGORY_COLORS[category] || CATEGORY_COLORS.other;
54
  }
55
 
56
+ function isNumberedAutoAnnotation(ann) {
57
+ const detectedKind = String(ann?.detectedKind || '').trim().toLowerCase();
58
+ const category = String(ann?.category || '').trim().toLowerCase();
59
+ return (
60
+ detectedKind === 'widget'
61
+ || detectedKind === 'shape'
62
+ || detectedKind === 'indicator'
63
+ || category === 'widget'
64
+ || category === 'indicator'
65
+ );
66
+ }
67
+
68
+ function annotationLabel(ann) {
69
+ if (ann?.source === 'auto' && !isNumberedAutoAnnotation(ann)) return '';
70
+ const displayNumber = Number.parseInt(String(ann?.displayNumber ?? ''), 10);
71
+ if (Number.isFinite(displayNumber) && displayNumber > 0) return String(displayNumber);
72
+ const bindingUid = String(ann?.bindingUid || '').trim();
73
+ if (bindingUid) return bindingUid;
74
+ if (ann?.source === 'auto') return '';
75
+ const localNumber = Number.parseInt(String(ann?.number ?? ''), 10);
76
+ if (Number.isFinite(localNumber) && localNumber > 0) return String(localNumber);
77
+ return '';
78
+ }
79
+
80
+ function annotationXmlTone(ann) {
81
+ if (ann?.xmlNeedsAttention) return '#f59e0b';
82
+ if (ann?.xmlExported) return '#22c55e';
83
+ return '';
84
+ }
85
+
86
+ function basicShapeName(ann) {
87
+ return String(ann?.basicShape || '').trim().toLowerCase();
88
+ }
89
+
90
+ function isLibraryPlacedAnnotation(ann) {
91
+ return Boolean(
92
+ String(ann?.widgetNameOverride || '').trim()
93
+ || String(ann?.staticShapeNameOverride || '').trim()
94
+ );
95
+ }
96
+
97
+ function annotationSourceCode(ann) {
98
+ if (ann?.source === 'auto') return 'A';
99
+ if (ann?.source === 'polygon') return 'P';
100
+ return 'M';
101
+ }
102
+
103
+ function annotationKindCode(ann) {
104
+ const detected = String(ann?.detectedKind || '').trim().toLowerCase();
105
+ return KIND_BADGE_LABELS[detected] || KIND_BADGE_LABELS.other;
106
+ }
107
+
108
+ function annotationBadgeText(ann) {
109
+ return `${annotationSourceCode(ann)}:${annotationKindCode(ann)}`;
110
+ }
111
+
112
+ function midpoint(a, b) {
113
+ return {
114
+ x: (Number(a?.x) + Number(b?.x)) / 2,
115
+ y: (Number(a?.y) + Number(b?.y)) / 2,
116
+ };
117
+ }
118
+
119
+ function rotatePointAround(point, center, angleDeg) {
120
+ const radians = (Number(angleDeg) * Math.PI) / 180;
121
+ const cos = Math.cos(radians);
122
+ const sin = Math.sin(radians);
123
+ const dx = point.x - center.x;
124
+ const dy = point.y - center.y;
125
+ return {
126
+ x: center.x + dx * cos - dy * sin,
127
+ y: center.y + dx * sin + dy * cos,
128
+ };
129
+ }
130
+
131
+ function rotatePointsAround(points, center, angleDeg) {
132
+ return (points || []).map((point) => rotatePointAround(point, center, angleDeg));
133
+ }
134
+
135
+ function rectFrame(points) {
136
+ if (!Array.isArray(points) || points.length !== 4) return null;
137
+ const [p0, p1, p2, p3] = points;
138
+ const edgeX = { x: p1.x - p0.x, y: p1.y - p0.y };
139
+ const edgeY = { x: p3.x - p0.x, y: p3.y - p0.y };
140
+ const width = Math.hypot(edgeX.x, edgeX.y);
141
+ const height = Math.hypot(edgeY.x, edgeY.y);
142
+ if (width < 1e-6 || height < 1e-6) return null;
143
+ return {
144
+ origin: { ...p0 },
145
+ center: midpoint(p0, p2),
146
+ edgeX,
147
+ edgeY,
148
+ width,
149
+ height,
150
+ axisX: { x: edgeX.x / width, y: edgeX.y / width },
151
+ axisY: { x: edgeY.x / height, y: edgeY.y / height },
152
+ rotation: (Math.atan2(edgeX.y, edgeX.x) * 180) / Math.PI,
153
+ };
154
+ }
155
+
156
+ function rectRotationFromPoints(points) {
157
+ const frame = rectFrame(points);
158
+ if (!frame) return 0;
159
+ let angle = frame.rotation % 360;
160
+ if (angle > 180) angle -= 360;
161
+ if (angle <= -180) angle += 360;
162
+ return round(angle, 2);
163
+ }
164
+
165
+ function frameMatrix(frame, sourceWidth = frame?.width, sourceHeight = frame?.height) {
166
+ if (!frame) return '';
167
+ const sx = Math.max(1e-6, Number(sourceWidth) || frame.width || 1);
168
+ const sy = Math.max(1e-6, Number(sourceHeight) || frame.height || 1);
169
+ return `matrix(${round(frame.edgeX.x / sx, 5)} ${round(frame.edgeX.y / sx, 5)} ${round(frame.edgeY.x / sy, 5)} ${round(frame.edgeY.y / sy, 5)} ${round(frame.origin.x, 5)} ${round(frame.origin.y, 5)})`;
170
+ }
171
+
172
+ function frameLocalToWorld(frame, localPoint) {
173
+ return {
174
+ x: frame.center.x + frame.axisX.x * localPoint.x + frame.axisY.x * localPoint.y,
175
+ y: frame.center.y + frame.axisX.y * localPoint.x + frame.axisY.y * localPoint.y,
176
+ };
177
+ }
178
+
179
+ function frameProjectPoint(frame, point) {
180
+ const dx = point.x - frame.center.x;
181
+ const dy = point.y - frame.center.y;
182
+ return {
183
+ x: dx * frame.axisX.x + dy * frame.axisX.y,
184
+ y: dx * frame.axisY.x + dy * frame.axisY.y,
185
+ };
186
+ }
187
+
188
+ function buildResizedRectPoints(points, handleIndex, point) {
189
+ const frame = rectFrame(points);
190
+ if (!frame || !Number.isFinite(handleIndex)) return points;
191
+ const halfWidth = frame.width / 2;
192
+ const halfHeight = frame.height / 2;
193
+ const localCorners = [
194
+ { x: -halfWidth, y: -halfHeight },
195
+ { x: halfWidth, y: -halfHeight },
196
+ { x: halfWidth, y: halfHeight },
197
+ { x: -halfWidth, y: halfHeight },
198
+ ];
199
+ const anchor = localCorners[(handleIndex + 2) % 4];
200
+ const current = localCorners[handleIndex];
201
+ const pointerLocal = frameProjectPoint(frame, point);
202
+ const signX = Math.sign(current.x - anchor.x) || 1;
203
+ const signY = Math.sign(current.y - anchor.y) || 1;
204
+ if (Math.abs(pointerLocal.x - anchor.x) < MIN_RECT_SIZE) {
205
+ pointerLocal.x = anchor.x + signX * MIN_RECT_SIZE;
206
+ }
207
+ if (Math.abs(pointerLocal.y - anchor.y) < MIN_RECT_SIZE) {
208
+ pointerLocal.y = anchor.y + signY * MIN_RECT_SIZE;
209
+ }
210
+ const minX = Math.min(anchor.x, pointerLocal.x);
211
+ const maxX = Math.max(anchor.x, pointerLocal.x);
212
+ const minY = Math.min(anchor.y, pointerLocal.y);
213
+ const maxY = Math.max(anchor.y, pointerLocal.y);
214
+ return [
215
+ { x: minX, y: minY },
216
+ { x: maxX, y: minY },
217
+ { x: maxX, y: maxY },
218
+ { x: minX, y: maxY },
219
+ ].map((local) => frameLocalToWorld(frame, local));
220
+ }
221
+
222
+ function rotateControl(points, zoom) {
223
+ if (!Array.isArray(points) || points.length !== 4) return null;
224
+ const topMid = midpoint(points[0], points[1]);
225
+ const center = midpoint(points[0], points[2]);
226
+ const vx = topMid.x - center.x;
227
+ const vy = topMid.y - center.y;
228
+ const length = Math.max(1e-6, Math.hypot(vx, vy));
229
+ const offset = 28 / zoom;
230
+ return {
231
+ topMid,
232
+ handle: {
233
+ x: topMid.x + (vx / length) * offset,
234
+ y: topMid.y + (vy / length) * offset,
235
+ },
236
+ };
237
+ }
238
+
239
+ function isTransformableAnnotation(ann, points) {
240
+ return ann?.source === 'manual' && ann?.type === 'rect' && Array.isArray(points) && points.length === 4;
241
+ }
242
+
243
+ const AnnotationNode = memo(function AnnotationNode({
244
+ ann,
245
+ points,
246
+ isSelected,
247
+ zoom,
248
+ }) {
249
+ const strokeColor = annotationColor(ann);
250
+ const fillColor = strokeColor;
251
+ const opacity = 0.3 + (ann.confidence || 0.5) * 0.7;
252
+ const fontSize = Math.max(10, 13 / zoom);
253
+ const detectedKind = String(ann.detectedKind || '').trim().toLowerCase();
254
+ const xmlTone = annotationXmlTone(ann);
255
+ const showIndexLabel = detectedKind !== 'form' && detectedKind !== 'arrow';
256
+ const labelText = annotationLabel(ann);
257
+ const showLibraryPreview = ann?.source === 'manual' && isLibraryPlacedAnnotation(ann);
258
+ const annotationBoundsBox = boundsOfPoints(points);
259
+ const libraryTitle = String(
260
+ ann?.note
261
+ || ann?.widgetNameOverride
262
+ || ann?.staticShapeNameOverride
263
+ || ''
264
+ ).trim();
265
+ const dashArray = detectedKind === 'form'
266
+ ? `${8 / zoom} ${5 / zoom}`
267
+ : detectedKind === 'arrow'
268
+ ? `${4 / zoom} ${3 / zoom}`
269
+ : 'none';
270
+ const isManual = ann.source === 'manual';
271
+ const isAuto = ann.source === 'auto';
272
+ const frame = rectFrame(points);
273
+ const baseShape = basicShapeName(ann);
274
+ const transformable = isSelected && isTransformableAnnotation(ann, points);
275
+ const rotateHandle = transformable ? rotateControl(points, zoom) : null;
276
+ const strokeWidth = isSelected
277
+ ? 2.5 / zoom
278
+ : isManual
279
+ ? 2.8 / zoom
280
+ : detectedKind === 'form'
281
+ ? 2.2 / zoom
282
+ : 1.8 / zoom;
283
+ const haloWidth = isSelected
284
+ ? 7 / zoom
285
+ : isManual
286
+ ? 6 / zoom
287
+ : 4.5 / zoom;
288
+ const baseFillOpacity = isSelected
289
+ ? 0.28
290
+ : isManual
291
+ ? 0.24
292
+ : detectedKind === 'arrow'
293
+ ? 0.0
294
+ : detectedKind === 'form'
295
+ ? 0.12
296
+ : 0.18;
297
+ const shapeFill = detectedKind === 'arrow' ? 'none' : `${fillColor}${Math.round(baseFillOpacity * 255).toString(16).padStart(2, '0')}`;
298
+ const sourceBadgeText = annotationBadgeText(ann);
299
+ const badgeWidth = Math.max(18, sourceBadgeText.length * (6.6 / zoom) + 8 / zoom);
300
+ const badgeHeight = Math.max(11, 13 / zoom);
301
+ const badgeX = annotationBoundsBox.x + 2 / zoom;
302
+ const badgeY = annotationBoundsBox.y + 2 / zoom;
303
+ const badgeFill = isManual ? 'rgba(15,23,42,0.88)' : 'rgba(15,23,42,0.72)';
304
+ const badgeStroke = isManual ? 'rgba(255,255,255,0.9)' : `${strokeColor}cc`;
305
+ const hasIntrinsicShape = Boolean(frame && (String(ann?.svgPath || '').trim() || baseShape));
306
+ const vectorMatrix = frame ? frameMatrix(
307
+ frame,
308
+ Number(ann?.svgWidth) || frame.width,
309
+ Number(ann?.svgHeight) || frame.height,
310
+ ) : '';
311
+ const shapeMatrix = frame ? frameMatrix(frame, frame.width, frame.height) : '';
312
+ const shapeStroke = isSelected ? '#fff' : strokeColor;
313
+
314
+ return (
315
+ <g data-id={ann.id}>
316
+ {!hasIntrinsicShape && (detectedKind === 'arrow' ? (
317
+ <path
318
+ d={polylineFromPoints(points)}
319
+ fill="none"
320
+ stroke={`${strokeColor}${isManual ? '55' : '33'}`}
321
+ strokeWidth={haloWidth}
322
+ strokeLinecap="round"
323
+ strokeLinejoin="round"
324
+ opacity={0.95}
325
+ />
326
+ ) : (
327
+ <path
328
+ d={pathFromPoints(points)}
329
+ fill={shapeFill}
330
+ stroke={`${strokeColor}${isManual ? '66' : '40'}`}
331
+ strokeWidth={haloWidth}
332
+ strokeLinejoin="round"
333
+ opacity={0.9}
334
+ />
335
+ ))}
336
+ {xmlTone && (
337
+ <path
338
+ d={pathFromPoints(points)}
339
+ fill="none"
340
+ stroke={xmlTone}
341
+ strokeWidth={(ann?.xmlNeedsAttention ? 3 : 2) / zoom}
342
+ strokeDasharray={ann?.xmlNeedsAttention ? `${6 / zoom} ${4 / zoom}` : `${3 / zoom} ${3 / zoom}`}
343
+ opacity={0.95}
344
+ />
345
+ )}
346
+ {(() => {
347
+ const common = {
348
+ stroke: shapeStroke,
349
+ strokeWidth,
350
+ opacity,
351
+ };
352
+ if (frame && String(ann?.svgPath || '').trim()) {
353
+ return (
354
+ <path
355
+ d={ann.svgPath}
356
+ transform={vectorMatrix}
357
+ fill="none"
358
+ stroke={shapeStroke}
359
+ strokeWidth={Math.max(strokeWidth, 2.2 / zoom)}
360
+ strokeLinejoin="round"
361
+ strokeLinecap="round"
362
+ opacity={1}
363
+ />
364
+ );
365
+ }
366
+ if (frame && baseShape === 'ellipse') {
367
+ return (
368
+ <ellipse
369
+ cx={frame.width / 2}
370
+ cy={frame.height / 2}
371
+ rx={Math.max(1, frame.width / 2)}
372
+ ry={Math.max(1, frame.height / 2)}
373
+ fill={isSelected ? `${fillColor}22` : 'none'}
374
+ transform={shapeMatrix}
375
+ {...common}
376
+ />
377
+ );
378
+ }
379
+ if (frame && (baseShape === 'line' || baseShape === 'arrow')) {
380
+ const x1 = 0;
381
+ const y1 = frame.height;
382
+ const x2 = frame.width;
383
+ const y2 = 0;
384
+ return (
385
+ <>
386
+ <line x1={x1} y1={y1} x2={x2} y2={y2} fill="none" transform={shapeMatrix} {...common} />
387
+ {baseShape === 'arrow' && (
388
+ <polyline
389
+ points={`${x2 - (8 / zoom)},${y2 + (2 / zoom)} ${x2},${y2} ${x2 - (2 / zoom)},${y2 + (8 / zoom)}`}
390
+ fill="none"
391
+ stroke={shapeStroke}
392
+ strokeWidth={strokeWidth}
393
+ opacity={opacity}
394
+ transform={shapeMatrix}
395
+ />
396
+ )}
397
+ </>
398
+ );
399
+ }
400
+ if (frame && baseShape === 'table') {
401
+ const bw = Math.max(2, frame.width);
402
+ const bh = Math.max(2, frame.height);
403
+ return (
404
+ <>
405
+ <rect x="0" y="0" width={bw} height={bh} fill="none" transform={shapeMatrix} {...common} />
406
+ <line x1="0" y1={bh * 0.35} x2={bw} y2={bh * 0.35} fill="none" transform={shapeMatrix} {...common} />
407
+ <line x1={bw * 0.5} y1={bh * 0.35} x2={bw * 0.5} y2={bh} fill="none" transform={shapeMatrix} {...common} />
408
+ </>
409
+ );
410
+ }
411
+ if (frame && baseShape === 'text') {
412
+ return (
413
+ <text
414
+ x={frame.width / 2}
415
+ y={frame.height / 2}
416
+ transform={shapeMatrix}
417
+ textAnchor="middle"
418
+ dominantBaseline="central"
419
+ fill={shapeStroke}
420
+ fontSize={Math.max(10, 14 / zoom)}
421
+ fontWeight="600"
422
+ >
423
+ {libraryTitle || 'Текст'}
424
+ </text>
425
+ );
426
+ }
427
+ const manualFill = isManual ? `${fillColor}55` : shapeFill;
428
+ if (detectedKind === 'arrow') {
429
+ return (
430
+ <path
431
+ d={polylineFromPoints(points)}
432
+ fill="none"
433
+ stroke={shapeStroke}
434
+ strokeWidth={Math.max(strokeWidth, 2.2 / zoom)}
435
+ strokeDasharray={isSelected ? `${4 / zoom} ${3 / zoom}` : 'none'}
436
+ strokeLinecap="round"
437
+ strokeLinejoin="round"
438
+ opacity={1}
439
+ />
440
+ );
441
+ }
442
+ return (
443
+ <path
444
+ d={pathFromPoints(points)}
445
+ fill={isSelected ? `${fillColor}33` : (showLibraryPreview ? `${fillColor}38` : manualFill)}
446
+ stroke={shapeStroke}
447
+ strokeWidth={strokeWidth}
448
+ strokeDasharray={isSelected ? `${4 / zoom} ${3 / zoom}` : (isManual ? 'none' : dashArray)}
449
+ opacity={isManual ? 1 : opacity}
450
+ />
451
+ );
452
+ })()}
453
+ <g opacity={isSelected ? 1 : (isAuto ? 0.9 : 1)}>
454
+ <rect
455
+ x={badgeX}
456
+ y={badgeY}
457
+ rx={Math.max(2, 3 / zoom)}
458
+ ry={Math.max(2, 3 / zoom)}
459
+ width={badgeWidth}
460
+ height={badgeHeight}
461
+ fill={badgeFill}
462
+ stroke={badgeStroke}
463
+ strokeWidth={1 / zoom}
464
+ />
465
+ <text
466
+ x={badgeX + badgeWidth / 2}
467
+ y={badgeY + badgeHeight / 2}
468
+ textAnchor="middle"
469
+ dominantBaseline="central"
470
+ fill="white"
471
+ fontSize={Math.max(8, 9 / zoom)}
472
+ fontWeight="700"
473
+ letterSpacing="0.02em"
474
+ >
475
+ {sourceBadgeText}
476
+ </text>
477
+ </g>
478
+ {showLibraryPreview && !String(ann?.svgPath || '').trim() && baseShape !== 'text' && libraryTitle && (
479
+ <text
480
+ x={annotationBoundsBox.x + annotationBoundsBox.width / 2}
481
+ y={annotationBoundsBox.y + annotationBoundsBox.height / 2}
482
+ textAnchor="middle"
483
+ dominantBaseline="central"
484
+ fill={fillColor}
485
+ fontSize={Math.max(9, 11 / zoom)}
486
+ fontWeight="600"
487
+ stroke="rgba(15,23,42,0.55)"
488
+ strokeWidth={0.7 / zoom}
489
+ paintOrder="stroke"
490
+ >
491
+ {libraryTitle.slice(0, 20)}
492
+ </text>
493
+ )}
494
+ {showIndexLabel && labelText && (
495
+ <>
496
+ <circle
497
+ cx={annotationBoundsBox.x}
498
+ cy={annotationBoundsBox.y}
499
+ r={Math.max(8, 10 / zoom)}
500
+ fill={fillColor}
501
+ opacity="0.85"
502
+ filter="url(#labelShadow)"
503
+ />
504
+ <text
505
+ x={annotationBoundsBox.x}
506
+ y={annotationBoundsBox.y}
507
+ textAnchor="middle"
508
+ dominantBaseline="central"
509
+ fill="white"
510
+ fontSize={fontSize}
511
+ fontWeight="bold"
512
+ stroke="rgba(0,0,0,0.4)"
513
+ strokeWidth={0.8 / zoom}
514
+ paintOrder="stroke"
515
+ >
516
+ {labelText}
517
+ </text>
518
+ </>
519
+ )}
520
+ {ann?.xmlNeedsAttention && (
521
+ <circle
522
+ cx={annotationBoundsBox.x + annotationBoundsBox.width}
523
+ cy={annotationBoundsBox.y}
524
+ r={Math.max(4, 5 / zoom)}
525
+ fill="#f59e0b"
526
+ stroke="rgba(15,23,42,0.95)"
527
+ strokeWidth={1.4 / zoom}
528
+ />
529
+ )}
530
+ {transformable ? (
531
+ <>
532
+ <path
533
+ d={pathFromPoints(points)}
534
+ fill="none"
535
+ stroke="rgba(255,255,255,0.58)"
536
+ strokeWidth={1 / zoom}
537
+ strokeDasharray={`${4 / zoom} ${3 / zoom}`}
538
+ />
539
+ {points.map((pt, i) => (
540
+ <rect
541
+ key={i}
542
+ x={pt.x - (5.5 / zoom)}
543
+ y={pt.y - (5.5 / zoom)}
544
+ width={11 / zoom}
545
+ height={11 / zoom}
546
+ rx={2.2 / zoom}
547
+ fill="white"
548
+ stroke="rgba(0,0,0,0.8)"
549
+ strokeWidth={1.5 / zoom}
550
+ data-transform-handle={`resize-${i}`}
551
+ data-annotation-id={ann.id}
552
+ style={{ cursor: 'nwse-resize' }}
553
+ />
554
+ ))}
555
+ {rotateHandle && (
556
+ <>
557
+ <line
558
+ x1={rotateHandle.topMid.x}
559
+ y1={rotateHandle.topMid.y}
560
+ x2={rotateHandle.handle.x}
561
+ y2={rotateHandle.handle.y}
562
+ stroke="rgba(255,255,255,0.8)"
563
+ strokeWidth={1.2 / zoom}
564
+ />
565
+ <circle
566
+ cx={rotateHandle.handle.x}
567
+ cy={rotateHandle.handle.y}
568
+ r={6 / zoom}
569
+ fill="#0f172a"
570
+ stroke="white"
571
+ strokeWidth={1.5 / zoom}
572
+ data-transform-handle="rotate"
573
+ data-annotation-id={ann.id}
574
+ style={{ cursor: 'grab' }}
575
+ />
576
+ </>
577
+ )}
578
+ </>
579
+ ) : isSelected && (
580
+ <>
581
+ <rect
582
+ x={annotationBoundsBox.x}
583
+ y={annotationBoundsBox.y}
584
+ width={annotationBoundsBox.width}
585
+ height={annotationBoundsBox.height}
586
+ fill="none"
587
+ stroke="rgba(255,255,255,0.5)"
588
+ strokeWidth={1 / zoom}
589
+ strokeDasharray={`${4 / zoom} ${3 / zoom}`}
590
+ />
591
+ {points.map((pt, i) => (
592
+ <circle
593
+ key={i}
594
+ cx={pt.x}
595
+ cy={pt.y}
596
+ r={5 / zoom}
597
+ fill="white"
598
+ stroke="rgba(0,0,0,0.8)"
599
+ strokeWidth={1.5 / zoom}
600
+ data-handle-index={i}
601
+ data-annotation-id={ann.id}
602
+ style={{ cursor: 'move' }}
603
+ />
604
+ ))}
605
+ </>
606
+ )}
607
+ </g>
608
+ );
609
+ }, (prev, next) => (
610
+ prev.ann === next.ann
611
+ && prev.points === next.points
612
+ && prev.isSelected === next.isSelected
613
+ && prev.zoom === next.zoom
614
+ ));
615
+
616
  export default function SvgWorkspace() {
617
  const state = useStore();
618
  const dispatch = useDispatch();
619
  const {
620
  interactionRef, findAnnotationAtPoint, createAnnotation,
621
+ updateAnnotationPoints,
622
  } = useAnnotationTools();
623
 
624
  const wrapRef = useRef(null);
 
628
  const [tempRect, setTempRect] = useState(null);
629
  const [tempPoints, setTempPoints] = useState([]);
630
  const [pointerNow, setPointerNow] = useState(null);
631
+ const [draftPoints, setDraftPoints] = useState(null);
632
+ const draftPointsRef = useRef(null);
633
+ const draftFrameRef = useRef(0);
634
+ const pendingDraftPointsRef = useRef(null);
635
  const isPanning = useRef(false);
636
  const panStart = useRef({ x: 0, y: 0 });
637
 
638
  const { imageWidth, imageHeight, imageDataUrl, tool, mode, annotations, selectedId } = state;
639
+ const selectedAnn = annotations.find((a) => a.id === selectedId) || null;
640
+ const sorted = useMemo(
641
+ () => [...annotations].sort((a, b) => polygonArea(b.points) - polygonArea(a.points)),
642
+ [annotations],
643
+ );
644
+ const processingCounts = state.processing?.counts || {};
645
+ const processingSummary = [
646
+ `виджеты: ${Number(processingCounts.widgets) || Number(processingCounts.cells) || 0}`,
647
+ `формы: ${Number(processingCounts.forms) || Number(processingCounts.groups) || 0}`,
648
+ `стрелки: ${Number(processingCounts.arrows) || 0}`,
649
+ `шейпы: ${Number(processingCounts.shapes) || Number(processingCounts.equipment) || 0}`,
650
+ `индикаторы: ${Number(processingCounts.indicators) || 0}`,
651
+ `прочее: ${Number(processingCounts.other) || 0}`,
652
+ ].join(', ');
653
+
654
+ const setDraftPointsImmediate = useCallback((points) => {
655
+ const normalized = Array.isArray(points) ? points.map((p) => ({ ...p })) : null;
656
+ draftPointsRef.current = normalized;
657
+ pendingDraftPointsRef.current = null;
658
+ if (draftFrameRef.current) {
659
+ cancelAnimationFrame(draftFrameRef.current);
660
+ draftFrameRef.current = 0;
661
+ }
662
+ setDraftPoints(normalized);
663
+ }, []);
664
+
665
+ const scheduleDraftPoints = useCallback((points) => {
666
+ const normalized = Array.isArray(points) ? points.map((p) => ({ ...p })) : null;
667
+ draftPointsRef.current = normalized;
668
+ pendingDraftPointsRef.current = normalized;
669
+ if (draftFrameRef.current) return;
670
+ draftFrameRef.current = requestAnimationFrame(() => {
671
+ draftFrameRef.current = 0;
672
+ const pending = pendingDraftPointsRef.current;
673
+ pendingDraftPointsRef.current = null;
674
+ setDraftPoints(pending);
675
+ });
676
+ }, []);
677
+
678
+ const clearDraftPoints = useCallback(() => {
679
+ draftPointsRef.current = null;
680
+ pendingDraftPointsRef.current = null;
681
+ if (draftFrameRef.current) {
682
+ cancelAnimationFrame(draftFrameRef.current);
683
+ draftFrameRef.current = 0;
684
+ }
685
+ setDraftPoints(null);
686
+ }, []);
687
+
688
+ useEffect(() => () => {
689
+ if (draftFrameRef.current) {
690
+ cancelAnimationFrame(draftFrameRef.current);
691
+ draftFrameRef.current = 0;
692
+ }
693
+ }, []);
694
 
695
  useEffect(() => {
696
  if (!imageWidth || !imageHeight || !wrapRef.current) return;
 
730
  });
731
  }, [dispatch]);
732
 
733
+ const commitAnnotationGeometry = useCallback((annotation, nextPoints) => {
734
+ if (!annotation || !Array.isArray(nextPoints) || !nextPoints.length) return;
735
+ const clampedPoints = nextPoints.map((point) => ({
736
+ x: clamp(Number(point.x), 0, imageWidth),
737
+ y: clamp(Number(point.y), 0, imageHeight),
738
+ }));
739
+ dispatch({
740
+ type: A.UPDATE_ANNOTATION,
741
+ payload: {
742
+ id: annotation.id,
743
+ changes: {
744
+ points: clampedPoints,
745
+ rotation: annotation.type === 'rect' ? rectRotationFromPoints(clampedPoints) : 0,
746
+ reviewed: true,
747
+ reviewedAt: Date.now(),
748
+ },
749
+ },
750
+ });
751
+ }, [dispatch, imageWidth, imageHeight]);
752
+
753
  const handlePointerDown = useCallback((e) => {
754
  const point = getSvgPoint(e);
755
  const ir = interactionRef.current;
 
763
 
764
  if (e.button !== 0) return;
765
 
766
+ const transformHandleEl = e.target.closest('[data-transform-handle]');
767
+ if (transformHandleEl && tool === TOOLS.SELECT) {
768
+ const annId = transformHandleEl.dataset.annotationId;
769
+ const ann = annotations.find((item) => item.id === annId);
770
+ if (ann && isTransformableAnnotation(ann, ann.points)) {
771
+ ir.transformHandle = String(transformHandleEl.dataset.transformHandle || '');
772
+ ir.dragTarget = annId;
773
+ ir.dragOriginalPoints = ann.points.map((p) => ({ ...p }));
774
+ ir.dragOriginalRotation = rectRotationFromPoints(ann.points);
775
+ ir.pointerStart = point;
776
+ ir.dragCommitted = false;
777
+ ir.historyPushed = false;
778
+ setDraftPointsImmediate(ann.points);
779
+ dispatch({
780
+ type: A.SET_MODE,
781
+ payload: ir.transformHandle === 'rotate' ? MODES.ROTATE : MODES.RESIZE,
782
+ });
783
+ e.stopPropagation();
784
+ return;
785
+ }
786
+ }
787
+
788
  const handleEl = e.target.closest('[data-handle-index]');
789
  if (handleEl && tool === TOOLS.SELECT) {
790
  const idx = parseInt(handleEl.dataset.handleIndex, 10);
791
  const annId = handleEl.dataset.annotationId;
792
  const ann = annotations.find((a) => a.id === annId);
793
  if (ann) {
 
794
  ir.dragVertexIndex = idx;
795
  ir.dragTarget = annId;
796
  ir.dragOriginalPoints = ann.points.map((p) => ({ ...p }));
797
+ ir.dragCommitted = false;
798
+ ir.historyPushed = false;
799
+ setDraftPointsImmediate(ann.points);
800
  dispatch({ type: A.SET_MODE, payload: MODES.VERTEX });
801
  e.stopPropagation();
802
  return;
 
808
  if (hit) {
809
  dispatch({ type: A.SELECT_ANNOTATION, payload: hit.id });
810
  if (hit.id === selectedId) {
 
811
  ir.dragTarget = hit.id;
812
  ir.dragOriginalPoints = hit.points.map((p) => ({ ...p }));
813
  ir.pointerStart = point;
814
  ir.dragCommitted = false;
815
+ ir.historyPushed = false;
816
+ setDraftPointsImmediate(hit.points);
817
  dispatch({ type: A.SET_MODE, payload: MODES.MOVE });
818
  }
819
  } else {
820
+ clearDraftPoints();
821
  dispatch({ type: A.SELECT_ANNOTATION, payload: null });
822
  }
823
  return;
 
856
  dispatch({ type: A.SET_MODE, payload: MODES.LASSO_DRAW });
857
  return;
858
  }
859
+ }, [
860
+ tool,
861
+ mode,
862
+ annotations,
863
+ selectedId,
864
+ getSvgPoint,
865
+ dispatch,
866
+ findAnnotationAtPoint,
867
+ interactionRef,
868
+ setDraftPointsImmediate,
869
+ clearDraftPoints,
870
+ ]);
871
 
872
  const handlePointerMove = useCallback((e) => {
873
  const point = getSvgPoint(e);
 
881
  return;
882
  }
883
 
884
+ if (mode === MODES.POLYGON_DRAW) {
885
+ setPointerNow(point);
886
+ }
887
 
888
  if (mode === MODES.DRAW_RECT && ir.pointerStart) {
889
  setTempRect({
 
907
  return;
908
  }
909
 
910
+ if (mode === MODES.RESIZE && ir.dragTarget && ir.transformHandle) {
911
+ const handleIndex = Number.parseInt(String(ir.transformHandle).replace('resize-', ''), 10);
912
+ if (!Number.isFinite(handleIndex)) return;
913
+ if (!ir.historyPushed) {
914
+ dispatch({ type: A.PUSH_HISTORY });
915
+ ir.historyPushed = true;
916
+ }
917
+ ir.dragCommitted = true;
918
+ const resized = buildResizedRectPoints(ir.dragOriginalPoints, handleIndex, point).map((pt) => ({
919
+ x: clamp(pt.x, 0, imageWidth),
920
+ y: clamp(pt.y, 0, imageHeight),
921
+ }));
922
+ scheduleDraftPoints(resized);
923
+ return;
924
+ }
925
+
926
+ if (mode === MODES.ROTATE && ir.dragTarget && ir.pointerStart) {
927
+ const originalPoints = ir.dragOriginalPoints;
928
+ if (!Array.isArray(originalPoints) || originalPoints.length !== 4) return;
929
+ const center = midpoint(originalPoints[0], originalPoints[2]);
930
+ const startAngle = Math.atan2(ir.pointerStart.y - center.y, ir.pointerStart.x - center.x);
931
+ const currentAngle = Math.atan2(point.y - center.y, point.x - center.x);
932
+ const deltaDeg = ((currentAngle - startAngle) * 180) / Math.PI;
933
+ if (!ir.dragCommitted && Math.abs(deltaDeg) < 1.2) return;
934
+ if (!ir.historyPushed) {
935
+ dispatch({ type: A.PUSH_HISTORY });
936
+ ir.historyPushed = true;
937
+ }
938
+ ir.dragCommitted = true;
939
+ scheduleDraftPoints(rotatePointsAround(originalPoints, center, deltaDeg));
940
+ return;
941
+ }
942
+
943
  if (mode === MODES.MOVE && ir.dragTarget && ir.pointerStart) {
944
  const dx = point.x - ir.pointerStart.x;
945
  const dy = point.y - ir.pointerStart.y;
946
  if (!ir.dragCommitted && Math.hypot(dx, dy) < MOVE_THRESHOLD) return;
947
+ if (!ir.historyPushed) {
948
+ dispatch({ type: A.PUSH_HISTORY });
949
+ ir.historyPushed = true;
950
+ }
951
  ir.dragCommitted = true;
952
  const moved = ir.dragOriginalPoints.map((p) => ({
953
  x: clamp(p.x + dx, 0, imageWidth),
954
  y: clamp(p.y + dy, 0, imageHeight),
955
  }));
956
+ scheduleDraftPoints(moved);
 
 
 
957
  return;
958
  }
959
 
960
  if (mode === MODES.VERTEX && ir.dragTarget != null) {
961
  const ann = annotations.find((a) => a.id === ir.dragTarget);
962
  if (ann) {
963
+ if (!ir.historyPushed) {
964
+ dispatch({ type: A.PUSH_HISTORY });
965
+ ir.historyPushed = true;
966
+ }
967
+ ir.dragCommitted = true;
968
+ const sourcePoints = Array.isArray(draftPoints) ? draftPoints : ann.points;
969
+ const newPoints = sourcePoints.map((p, i) =>
970
  i === ir.dragVertexIndex
971
  ? { x: clamp(point.x, 0, imageWidth), y: clamp(point.y, 0, imageHeight) }
972
  : p
973
  );
974
+ scheduleDraftPoints(newPoints);
 
 
 
975
  }
976
  return;
977
  }
978
+ }, [mode, getSvgPoint, imageWidth, imageHeight, dispatch, annotations, interactionRef, draftPoints, scheduleDraftPoints]);
979
 
980
  const handlePointerUp = useCallback(() => {
981
  const ir = interactionRef.current;
 
1016
  }
1017
 
1018
  if (mode === MODES.MOVE) {
1019
+ const committedPoints = Array.isArray(draftPointsRef.current) ? draftPointsRef.current : draftPoints;
1020
+ const annotation = annotations.find((item) => item.id === ir.dragTarget);
1021
+ if (ir.dragCommitted && annotation && Array.isArray(committedPoints)) {
1022
+ commitAnnotationGeometry(annotation, committedPoints);
1023
+ }
1024
  ir.dragTarget = null;
1025
  ir.dragOriginalPoints = null;
1026
+ ir.pointerStart = null;
1027
+ ir.transformHandle = '';
1028
  ir.dragCommitted = false;
1029
+ ir.historyPushed = false;
1030
+ clearDraftPoints();
1031
  dispatch({ type: A.SET_MODE, payload: MODES.IDLE });
1032
  return;
1033
  }
1034
 
1035
  if (mode === MODES.VERTEX) {
1036
+ const committedPoints = Array.isArray(draftPointsRef.current) ? draftPointsRef.current : draftPoints;
1037
+ if (ir.dragCommitted && ir.dragTarget && Array.isArray(committedPoints)) {
1038
+ updateAnnotationPoints(ir.dragTarget, committedPoints);
1039
+ }
1040
  ir.dragTarget = null;
1041
  ir.dragVertexIndex = -1;
1042
+ ir.pointerStart = null;
1043
+ ir.transformHandle = '';
1044
+ ir.dragCommitted = false;
1045
+ ir.historyPushed = false;
1046
+ clearDraftPoints();
1047
  dispatch({ type: A.SET_MODE, payload: MODES.IDLE });
1048
  return;
1049
  }
1050
+
1051
+ if (mode === MODES.RESIZE || mode === MODES.ROTATE) {
1052
+ const committedPoints = Array.isArray(draftPointsRef.current) ? draftPointsRef.current : draftPoints;
1053
+ const annotation = annotations.find((item) => item.id === ir.dragTarget);
1054
+ if (ir.dragCommitted && annotation && Array.isArray(committedPoints)) {
1055
+ commitAnnotationGeometry(annotation, committedPoints);
1056
+ }
1057
+ ir.dragTarget = null;
1058
+ ir.dragOriginalPoints = null;
1059
+ ir.pointerStart = null;
1060
+ ir.transformHandle = '';
1061
+ ir.dragCommitted = false;
1062
+ ir.historyPushed = false;
1063
+ clearDraftPoints();
1064
+ dispatch({ type: A.SET_MODE, payload: MODES.IDLE });
1065
+ return;
1066
+ }
1067
+ }, [mode, tempRect, createAnnotation, dispatch, interactionRef, draftPoints, annotations, updateAnnotationPoints, clearDraftPoints, commitAnnotationGeometry]);
1068
 
1069
  const finishPolygon = useCallback(() => {
1070
  const ir = interactionRef.current;
 
1087
  interactionRef.current.tempPoints = [];
1088
  setTempPoints([]);
1089
  setTempRect(null);
1090
+ clearDraftPoints();
1091
+ interactionRef.current.dragTarget = null;
1092
+ interactionRef.current.dragOriginalPoints = null;
1093
+ interactionRef.current.dragVertexIndex = -1;
1094
+ interactionRef.current.pointerStart = null;
1095
+ interactionRef.current.transformHandle = '';
1096
+ interactionRef.current.dragCommitted = false;
1097
+ interactionRef.current.historyPushed = false;
1098
  dispatch({ type: A.SET_MODE, payload: MODES.IDLE });
1099
  }
1100
  }
1101
  window.addEventListener('keydown', handleKey);
1102
  return () => window.removeEventListener('keydown', handleKey);
1103
+ }, [mode, finishPolygon, dispatch, interactionRef, clearDraftPoints]);
 
 
1104
 
1105
  if (!imageDataUrl) {
1106
  return (
 
1116
  const svgW = imageWidth * zoom;
1117
  const svgH = imageHeight * zoom;
1118
 
 
 
 
 
 
 
 
 
 
 
 
 
1119
  return (
1120
  <div className="viewer-panel" style={{ display: 'flex', flexDirection: 'column' }}>
1121
  <div
 
1154
  </defs>
1155
  <image href={imageDataUrl} x="0" y="0" width={imageWidth} height={imageHeight} />
1156
 
1157
+ <g id="overlayLegend" transform={`translate(${12 / zoom}, ${12 / zoom})`}>
1158
+ <rect
1159
+ x="0"
1160
+ y="0"
1161
+ width={170 / zoom}
1162
+ height={58 / zoom}
1163
+ rx={8 / zoom}
1164
+ fill="rgba(15,23,42,0.78)"
1165
+ stroke="rgba(255,255,255,0.22)"
1166
+ strokeWidth={1 / zoom}
1167
+ />
1168
+ <text x={10 / zoom} y={16 / zoom} fill="white" fontSize={10 / zoom} fontWeight="700">
1169
+ Объекты на схеме
1170
+ </text>
1171
+ <text x={10 / zoom} y={31 / zoom} fill="rgba(255,255,255,0.92)" fontSize={9 / zoom}>
1172
+ A:* распознано автоматически
1173
+ </text>
1174
+ <text x={10 / zoom} y={45 / zoom} fill="rgba(255,255,255,0.92)" fontSize={9 / zoom}>
1175
+ M:* добавлено пользователем
1176
+ </text>
1177
+ </g>
1178
+
1179
  {/* Annotations layer */}
1180
  <g id="annotationsLayer">
1181
  {sorted.map((ann) => {
1182
  const isSelected = ann.id === selectedId;
1183
+ const points = isSelected && Array.isArray(draftPoints) ? draftPoints : ann.points;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1184
  return (
1185
+ <AnnotationNode
1186
+ key={ann.id}
1187
+ ann={ann}
1188
+ points={points}
1189
+ isSelected={isSelected}
1190
+ zoom={zoom}
1191
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1192
  );
1193
  })}
1194
  </g>
web-new/src/constants.js CHANGED
@@ -1,31 +1,41 @@
1
  export const ALLOWED_CATEGORIES = Object.freeze([
2
- 'table', 'equipment', 'indicator', 'pipe', 'value', 'other',
 
 
 
 
 
 
 
3
  ]);
4
 
5
  export const CATEGORY_COLORS = Object.freeze({
6
- table: '#60a5fa',
7
- equipment: '#f97316',
8
- indicator: '#22c55e',
9
- pipe: '#a78bfa',
10
- value: '#eab308',
 
 
 
11
  other: '#94a3b8',
12
  });
13
 
14
  export const CATEGORY_LABELS = Object.freeze({
15
- table: 'Таблица',
16
- equipment: 'Оборудование',
17
- indicator: 'Индикатор',
18
- pipe: 'Трубопровод',
19
- value: 'Значение',
 
 
 
20
  other: 'Другое',
21
  });
22
 
23
  export const CATEGORY_KEYS = Object.freeze({
24
- 1: 'table',
25
- 2: 'equipment',
26
- 3: 'indicator',
27
- 5: 'value',
28
- 6: 'other',
29
  });
30
 
31
  export const SOURCE_COLORS = Object.freeze({
@@ -57,6 +67,8 @@ export const MODES = Object.freeze({
57
  LASSO_DRAW: 'lasso-draw',
58
  MOVE: 'move',
59
  VERTEX: 'vertex',
 
 
60
  TRIM_CUT: 'trim-cut',
61
  SPLIT_CUT: 'split-cut',
62
  TRACE_PICK: 'trace-pick',
 
1
  export const ALLOWED_CATEGORIES = Object.freeze([
2
+ 'widget', 'static',
3
+ ]);
4
+
5
+ export const CATEGORY_SELECT_OPTIONS = Object.freeze([
6
+ { id: 'widget', label: 'Виджет', category: 'widget', detectedKind: 'widget', color: '#3158ff' },
7
+ { id: 'static-shape', label: 'Шейп', category: 'static', detectedKind: 'shape', color: '#f97316' },
8
+ { id: 'static-arrow', label: 'Стрелка процесса', category: 'static', detectedKind: 'arrow', color: '#ff4f87' },
9
+ { id: 'static-form', label: 'Статическая форма', category: 'static', detectedKind: 'form', color: '#16a34a' },
10
  ]);
11
 
12
  export const CATEGORY_COLORS = Object.freeze({
13
+ widget: '#3158ff',
14
+ static: '#16a34a',
15
+ table: '#16a34a',
16
+ equipment: '#16a34a',
17
+ indicator: '#3158ff',
18
+ pipe: '#16a34a',
19
+ value: '#3158ff',
20
+ text: '#16a34a',
21
  other: '#94a3b8',
22
  });
23
 
24
  export const CATEGORY_LABELS = Object.freeze({
25
+ widget: 'Виджет',
26
+ static: 'Статика',
27
+ table: 'Статика',
28
+ equipment: 'Статика',
29
+ indicator: 'Виджет',
30
+ pipe: 'Статика',
31
+ value: 'Виджет',
32
+ text: 'Статика',
33
  other: 'Другое',
34
  });
35
 
36
  export const CATEGORY_KEYS = Object.freeze({
37
+ 1: 'widget',
38
+ 2: 'static',
 
 
 
39
  });
40
 
41
  export const SOURCE_COLORS = Object.freeze({
 
67
  LASSO_DRAW: 'lasso-draw',
68
  MOVE: 'move',
69
  VERTEX: 'vertex',
70
+ RESIZE: 'resize',
71
+ ROTATE: 'rotate',
72
  TRIM_CUT: 'trim-cut',
73
  SPLIT_CUT: 'split-cut',
74
  TRACE_PICK: 'trace-pick',
web-new/src/hooks/useAnnotationTools.js CHANGED
@@ -51,6 +51,7 @@ function buildAnnotation(points, imageWidth, imageHeight, annotations, source =
51
  return {
52
  id: uid('ann'),
53
  number: num,
 
54
  bindingUid: '',
55
  type,
56
  points: cleaned,
@@ -63,6 +64,11 @@ function buildAnnotation(points, imageWidth, imageHeight, annotations, source =
63
  category: 'other',
64
  widgetNameOverride: '',
65
  staticShapeNameOverride: '',
 
 
 
 
 
66
  createdAt: Date.now(),
67
  reviewed,
68
  reviewedAt: reviewed ? Date.now() : 0,
@@ -78,7 +84,10 @@ export function useAnnotationTools() {
78
  dragTarget: null,
79
  dragOriginalPoints: null,
80
  dragVertexIndex: -1,
 
 
81
  dragCommitted: false,
 
82
  tempRect: null,
83
  tempPoints: [],
84
  isPanning: false,
@@ -101,7 +110,7 @@ export function useAnnotationTools() {
101
 
102
  const area = Math.max(1, polygonArea(ann.points));
103
  const boundsArea = Math.max(1, bounds.width * bounds.height);
104
- const inside = ann.type === 'rect'
105
  ? pointInRect(point, bounds)
106
  : (pointInRect(point, bounds) && pointInPolygon(point, ann.points));
107
  const edgeDist = pointToPolygonEdgeDistance(point, ann.points);
@@ -164,7 +173,16 @@ export function useAnnotationTools() {
164
  }
165
  dispatch({
166
  type: A.UPDATE_ANNOTATION,
167
- payload: { id, changes: { points: next, type, reviewed: true, reviewedAt: Date.now() } },
 
 
 
 
 
 
 
 
 
168
  });
169
  }, [dispatch, state.annotations, state.imageWidth, state.imageHeight]);
170
 
@@ -173,8 +191,30 @@ export function useAnnotationTools() {
173
  if (!ann) return;
174
  const offset = 15;
175
  const shifted = ann.points.map((p) => ({ x: p.x + offset, y: p.y + offset }));
176
- createAnnotation(shifted, 'manual', ann.confidence, ann.type);
177
- }, [state.annotations, state.selectedId, createAnnotation]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  const selectNext = useCallback((direction = 1) => {
180
  if (!state.annotations.length) return;
 
51
  return {
52
  id: uid('ann'),
53
  number: num,
54
+ displayNumber: null,
55
  bindingUid: '',
56
  type,
57
  points: cleaned,
 
64
  category: 'other',
65
  widgetNameOverride: '',
66
  staticShapeNameOverride: '',
67
+ basicShape: '',
68
+ svgPath: '',
69
+ svgWidth: 0,
70
+ svgHeight: 0,
71
+ rotation: 0,
72
  createdAt: Date.now(),
73
  reviewed,
74
  reviewedAt: reviewed ? Date.now() : 0,
 
84
  dragTarget: null,
85
  dragOriginalPoints: null,
86
  dragVertexIndex: -1,
87
+ dragOriginalRotation: 0,
88
+ transformHandle: '',
89
  dragCommitted: false,
90
+ historyPushed: false,
91
  tempRect: null,
92
  tempPoints: [],
93
  isPanning: false,
 
110
 
111
  const area = Math.max(1, polygonArea(ann.points));
112
  const boundsArea = Math.max(1, bounds.width * bounds.height);
113
+ const inside = ann.type === 'rect' && isLikelyRect(ann.points)
114
  ? pointInRect(point, bounds)
115
  : (pointInRect(point, bounds) && pointInPolygon(point, ann.points));
116
  const edgeDist = pointToPolygonEdgeDistance(point, ann.points);
 
173
  }
174
  dispatch({
175
  type: A.UPDATE_ANNOTATION,
176
+ payload: {
177
+ id,
178
+ changes: {
179
+ points: next,
180
+ type,
181
+ rotation: type === 'rect' ? (ann.rotation || 0) : 0,
182
+ reviewed: true,
183
+ reviewedAt: Date.now(),
184
+ },
185
+ },
186
  });
187
  }, [dispatch, state.annotations, state.imageWidth, state.imageHeight]);
188
 
 
191
  if (!ann) return;
192
  const offset = 15;
193
  const shifted = ann.points.map((p) => ({ x: p.x + offset, y: p.y + offset }));
194
+ const num = nextAvailableNumber(state.annotations);
195
+ dispatch({ type: A.PUSH_HISTORY });
196
+ dispatch({
197
+ type: A.ADD_ANNOTATION,
198
+ payload: {
199
+ ...ann,
200
+ id: uid('ann'),
201
+ number: num,
202
+ displayNumber: null,
203
+ bindingUid: '',
204
+ source: 'manual',
205
+ points: shifted,
206
+ createdAt: Date.now(),
207
+ reviewed: true,
208
+ reviewedAt: Date.now(),
209
+ xmlStatus: '',
210
+ xmlMatched: false,
211
+ xmlExported: false,
212
+ xmlNeedsAttention: false,
213
+ xmlMissingVisuals: [],
214
+ xmlResolvedVisuals: [],
215
+ },
216
+ });
217
+ }, [dispatch, state.annotations, state.selectedId]);
218
 
219
  const selectNext = useCallback((direction = 1) => {
220
  if (!state.annotations.length) return;
web-new/src/store/actions.js CHANGED
@@ -29,3 +29,4 @@ export const SET_MNEMO_TITLE = 'SET_MNEMO_TITLE';
29
  export const SET_LEGEND_ITEMS = 'SET_LEGEND_ITEMS';
30
 
31
  export const IMPORT_PROJECT = 'IMPORT_PROJECT';
 
 
29
  export const SET_LEGEND_ITEMS = 'SET_LEGEND_ITEMS';
30
 
31
  export const IMPORT_PROJECT = 'IMPORT_PROJECT';
32
+ export const SET_ELEMENT_LIBRARY = 'SET_ELEMENT_LIBRARY';
web-new/src/store/reducer.js CHANGED
@@ -29,6 +29,7 @@ export function createInitialState() {
29
  ocrData: null,
30
 
31
  processing: {
 
32
  ready: false,
33
  width: 0,
34
  height: 0,
@@ -36,10 +37,17 @@ export function createInitialState() {
36
  components: [],
37
  counts: {},
38
  summary: '',
39
- detector: 'src-colab-hybrid-v2',
40
  lastAnalyzedAt: '',
41
  },
42
 
 
 
 
 
 
 
 
43
  widgetExport: {
44
  sourceXmlName: '',
45
  sourceXmlText: '',
@@ -60,6 +68,7 @@ export function createInitialState() {
60
  exportedCount: 0,
61
  unmatchedNumbers: [],
62
  missingVisuals: [],
 
63
  generatedXmlText: '',
64
  lastGeneratedAt: '',
65
  },
@@ -71,11 +80,23 @@ function snapshotAnnotations(state) {
71
  annotations: state.annotations.map((a) => ({
72
  id: a.id, number: a.number, type: a.type,
73
  points: a.points, source: a.source, confidence: a.confidence,
 
74
  bindingUid: a.bindingUid,
75
  detectedKind: a.detectedKind,
76
  note: a.note, category: a.category, contextPath: a.contextPath,
77
  widgetNameOverride: a.widgetNameOverride,
78
  staticShapeNameOverride: a.staticShapeNameOverride,
 
 
 
 
 
 
 
 
 
 
 
79
  allowDuplicate: a.allowDuplicate,
80
  reviewed: a.reviewed, reviewedAt: a.reviewedAt,
81
  createdAt: a.createdAt,
@@ -102,10 +123,12 @@ export function reducer(state, action) {
102
  ocrData: null,
103
  processing: {
104
  ...state.processing,
 
105
  ready: false,
106
  components: [],
107
  counts: {},
108
  summary: '',
 
109
  lastAnalyzedAt: '',
110
  },
111
  };
@@ -247,6 +270,17 @@ export function reducer(state, action) {
247
  widgetExport: { ...state.widgetExport, legendItemsText: action.payload },
248
  };
249
 
 
 
 
 
 
 
 
 
 
 
 
250
  case A.IMPORT_PROJECT: {
251
  const p = action.payload;
252
  return {
 
29
  ocrData: null,
30
 
31
  processing: {
32
+ pending: false,
33
  ready: false,
34
  width: 0,
35
  height: 0,
 
37
  components: [],
38
  counts: {},
39
  summary: '',
40
+ detector: '',
41
  lastAnalyzedAt: '',
42
  },
43
 
44
+ elementLibrary: {
45
+ statics: [],
46
+ widgets: [],
47
+ basics: [],
48
+ loaded: false,
49
+ },
50
+
51
  widgetExport: {
52
  sourceXmlName: '',
53
  sourceXmlText: '',
 
68
  exportedCount: 0,
69
  unmatchedNumbers: [],
70
  missingVisuals: [],
71
+ annotationResults: [],
72
  generatedXmlText: '',
73
  lastGeneratedAt: '',
74
  },
 
80
  annotations: state.annotations.map((a) => ({
81
  id: a.id, number: a.number, type: a.type,
82
  points: a.points, source: a.source, confidence: a.confidence,
83
+ displayNumber: a.displayNumber,
84
  bindingUid: a.bindingUid,
85
  detectedKind: a.detectedKind,
86
  note: a.note, category: a.category, contextPath: a.contextPath,
87
  widgetNameOverride: a.widgetNameOverride,
88
  staticShapeNameOverride: a.staticShapeNameOverride,
89
+ basicShape: a.basicShape,
90
+ svgPath: a.svgPath,
91
+ svgWidth: a.svgWidth,
92
+ svgHeight: a.svgHeight,
93
+ rotation: a.rotation,
94
+ xmlStatus: a.xmlStatus,
95
+ xmlMatched: a.xmlMatched,
96
+ xmlExported: a.xmlExported,
97
+ xmlNeedsAttention: a.xmlNeedsAttention,
98
+ xmlMissingVisuals: a.xmlMissingVisuals,
99
+ xmlResolvedVisuals: a.xmlResolvedVisuals,
100
  allowDuplicate: a.allowDuplicate,
101
  reviewed: a.reviewed, reviewedAt: a.reviewedAt,
102
  createdAt: a.createdAt,
 
123
  ocrData: null,
124
  processing: {
125
  ...state.processing,
126
+ pending: false,
127
  ready: false,
128
  components: [],
129
  counts: {},
130
  summary: '',
131
+ detector: '',
132
  lastAnalyzedAt: '',
133
  },
134
  };
 
270
  widgetExport: { ...state.widgetExport, legendItemsText: action.payload },
271
  };
272
 
273
+ case A.SET_ELEMENT_LIBRARY:
274
+ return {
275
+ ...state,
276
+ elementLibrary: {
277
+ statics: action.payload.statics || [],
278
+ widgets: action.payload.widgets || [],
279
+ basics: action.payload.basics || [],
280
+ loaded: true,
281
+ },
282
+ };
283
+
284
  case A.IMPORT_PROJECT: {
285
  const p = action.payload;
286
  return {
web-new/src/utils/xmlBuilder.js CHANGED
@@ -172,6 +172,46 @@ export function buildStaticShapeCell(doc, { cellId, shapeName, x, y, width, heig
172
  return cell;
173
  }
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  function indentXml(xmlStr) {
176
  let result = '';
177
  let indent = 0;
@@ -223,6 +263,8 @@ export function generateMnemoXml({ annotations, bindingLookup = {}, title = '',
223
  const widgetName = String(ann.widgetNameOverride || ann.widget || '').trim();
224
  const omPath = String(ann.contextPath || ann.omPath || '').trim();
225
  const shapeName = String(ann.staticShapeNameOverride || '').trim();
 
 
226
 
227
  const bounds = ann.bounds || {};
228
  const x = bounds.x ?? 0;
@@ -230,6 +272,23 @@ export function generateMnemoXml({ annotations, bindingLookup = {}, title = '',
230
  const w = bounds.width ?? 24;
231
  const h = bounds.height ?? 24;
232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  if (shapeName && w > 0 && h > 0) {
234
  root.appendChild(buildStaticShapeCell(doc, {
235
  cellId: nextId++, shapeName, x, y, width: w, height: h,
 
172
  return cell;
173
  }
174
 
175
+ export function buildTextLabelCell(doc, { cellId, text, x, y, width, height, fontSize, align }) {
176
+ const fs = fontSize || Math.max(8, Math.min(24, Math.round(height * 0.65)));
177
+ const style = `text;html=1;strokeColor=none;fillColor=none;align=${align || 'center'};`
178
+ + `verticalAlign=middle;whiteSpace=wrap;rounded=0;fontFamily=Helvetica;`
179
+ + `fontSize=${fs};fontColor=#383838;`;
180
+ const cell = doc.createElement('mxCell');
181
+ setAttrs(cell, { id: cellId, value: text || '', style, parent: '1', vertex: '1' });
182
+ addGeometry(cell, doc, { x, y, width, height });
183
+ return cell;
184
+ }
185
+
186
+ export function buildArrowEdgeCell(doc, { cellId, points }) {
187
+ if (!points || points.length < 2) return null;
188
+ const style = 'endArrow=classic;html=1;strokeWidth=2;strokeColor=#696969;fontColor=#383838;';
189
+ const cell = doc.createElement('mxCell');
190
+ setAttrs(cell, { id: cellId, value: '', style, parent: '1', edge: '1' });
191
+ const geom = doc.createElement('mxGeometry');
192
+ geom.setAttribute('relative', '1');
193
+ geom.setAttribute('as', 'geometry');
194
+ const src = doc.createElement('mxPoint');
195
+ setAttrs(src, { x: String(points[0].x), y: String(points[0].y), as: 'sourcePoint' });
196
+ geom.appendChild(src);
197
+ const tgt = doc.createElement('mxPoint');
198
+ const last = points[points.length - 1];
199
+ setAttrs(tgt, { x: String(last.x), y: String(last.y), as: 'targetPoint' });
200
+ geom.appendChild(tgt);
201
+ if (points.length > 2) {
202
+ const arr = doc.createElement('Array');
203
+ arr.setAttribute('as', 'points');
204
+ for (const pt of points.slice(1, -1)) {
205
+ const mp = doc.createElement('mxPoint');
206
+ setAttrs(mp, { x: String(pt.x), y: String(pt.y) });
207
+ arr.appendChild(mp);
208
+ }
209
+ geom.appendChild(arr);
210
+ }
211
+ cell.appendChild(geom);
212
+ return cell;
213
+ }
214
+
215
  function indentXml(xmlStr) {
216
  let result = '';
217
  let indent = 0;
 
263
  const widgetName = String(ann.widgetNameOverride || ann.widget || '').trim();
264
  const omPath = String(ann.contextPath || ann.omPath || '').trim();
265
  const shapeName = String(ann.staticShapeNameOverride || '').trim();
266
+ const category = String(ann.category || '').trim().toLowerCase();
267
+ const detectedKind = String(ann.detectedKind || '').trim().toLowerCase();
268
 
269
  const bounds = ann.bounds || {};
270
  const x = bounds.x ?? 0;
 
272
  const w = bounds.width ?? 24;
273
  const h = bounds.height ?? 24;
274
 
275
+ if (category === 'text' || detectedKind.startsWith('text-')) {
276
+ const label = String(ann.note || ann.label || '').trim();
277
+ if (label) {
278
+ root.appendChild(buildTextLabelCell(doc, {
279
+ cellId: nextId++, text: label, x, y, width: w, height: h,
280
+ align: detectedKind === 'text-equipment' || detectedKind === 'text-parameter' ? 'left' : 'center',
281
+ }));
282
+ }
283
+ continue;
284
+ }
285
+
286
+ if ((detectedKind === 'arrow' || category === 'pipe' || category === 'static') && !uid && Array.isArray(ann.points) && detectedKind === 'arrow') {
287
+ const edgeCell = buildArrowEdgeCell(doc, { cellId: nextId++, points: ann.points });
288
+ if (edgeCell) root.appendChild(edgeCell);
289
+ continue;
290
+ }
291
+
292
  if (shapeName && w > 0 && h > 0) {
293
  root.appendChild(buildStaticShapeCell(doc, {
294
  cellId: nextId++, shapeName, x, y, width: w, height: h,