mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-12 15:14:12 +02:00
Merge remote-tracking branch 'upstream/master' into wiring-manager
Conflicts resolved in 24 files. Notable decisions: - ico/icon-themes.qrc: upstream's icon folder restructure kept; the 36 cable icon entries re-added under the new qet/qet-dark path scheme (XML valid, every referenced file exists). - Toolbar system: upstream's DiagramToolbarSettings now owns the toolbar filling. The cable tool is registered as diagrameditor.add_cable (generic group loop) and listed on the "diagram" toolbar right after the auto-break conductor action, separated by a divider, exactly like before the merge. - Paste: the cable lines of a copy are handed to PasteNumberingImport::push() as extra commands, so paste + cable lines + the numbering question stay ONE undo step on both paste paths (DiagramView::paste and DiagramEventAddPaste::commit). - PDF links: upstream's deterministic link sorting kept together with the cable cross-reference injector (injectLinkTo -> links list). - Element renumbering: upstream's ElementAutoNumSchemeCommand::renumber replaces the old formula-based body of QETProject::renumberElementsBySchemeTitle; the cable numbering rule API (cableAutoNum, axis priority, rule name) is kept in full. - Auto-numbering pages: upstream's scheme management kept, the Câbles tab added as index 4 (changeToCableTab), both entry points changeToCable() and changeToElement() kept side by side. - SelectAutonumW: dirty tracking and the contextEdited signal moved into upstream's insertPartRow() row API, so single-rule mode, the rule-removable button and "confirm without change leaves the project alone" keep working. - xrefpropertieswidget: upstream's offset clamping kept; the cable branch (Texte field, Police button, hidden display options) kept; the stack-overlapping checkbox is hidden for cables. Verified after the merge: build green (515/515, no errors), ctest 85/86 (only the pre-existing headless tst_menubarkeyboard failure), offscreen smoke exit 124.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Copyright 2006 The QElectroTech Team
|
||||
# Copyright 2006-2026 The QElectroTech Team
|
||||
# This file is part of QElectroTech.
|
||||
#
|
||||
# QElectroTech is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2006 The QElectroTech Team
|
||||
# Copyright 2006-2026 The QElectroTech Team
|
||||
# This file is part of QElectroTech.
|
||||
#
|
||||
# QElectroTech is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2006 The QElectroTech Team
|
||||
# Copyright 2006-2026 The QElectroTech Team
|
||||
# This file is part of QElectroTech.
|
||||
#
|
||||
# QElectroTech is free software: you can redistribute it and/or modify
|
||||
|
||||
+540
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright 2006 The QElectroTech Team
|
||||
# Copyright 2006-2026 The QElectroTech Team
|
||||
# This file is part of QElectroTech.
|
||||
#
|
||||
# QElectroTech is free software: you can redistribute it and/or modify
|
||||
@@ -66,6 +66,30 @@ include(../../cmake/fetch_pugixml.cmake)
|
||||
|
||||
enable_testing()
|
||||
|
||||
add_executable(tst_darkimagelayers tst_darkimagelayers.cpp
|
||||
${QET_DIR}/sources/palettegraphicsview.cpp ${QET_DIR}/sources/palettegraphicsview.h
|
||||
${QET_DIR}/sources/qetpalette.cpp)
|
||||
add_test(NAME tst_darkimagelayers COMMAND tst_darkimagelayers)
|
||||
target_include_directories(tst_darkimagelayers PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_darkimagelayers PRIVATE Qt::Test Qt::Widgets)
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||
add_test(NAME tst_attachedplainpaste
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_attached_plain_paste_probe.py
|
||||
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
|
||||
--source ${CMAKE_CURRENT_SOURCE_DIR}/attached_plain_paste_probe.cpp
|
||||
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
|
||||
set_tests_properties(tst_attachedplainpaste PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE)
|
||||
endif()
|
||||
if(Python3_Interpreter_FOUND AND CMAKE_GENERATOR STREQUAL "Ninja")
|
||||
add_test(NAME tst_darkimageintegration
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/run_image_dark_probe.py
|
||||
--build ${CMAKE_BINARY_DIR} --binary $<TARGET_FILE:qelectrotech>
|
||||
--source ${CMAKE_CURRENT_SOURCE_DIR}/image_dark_integration_probe.cpp
|
||||
--fixture ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/unlinked_contact_label.qet)
|
||||
set_tests_properties(tst_darkimageintegration PROPERTIES TIMEOUT 300 RUN_SERIAL TRUE)
|
||||
endif()
|
||||
|
||||
add_executable(${PROJECT_NAME} tst_mytest.cpp)
|
||||
add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME})
|
||||
|
||||
@@ -84,6 +108,13 @@ add_test(NAME tst_diagramsortkeys COMMAND tst_diagramsortkeys)
|
||||
target_include_directories(tst_diagramsortkeys PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_diagramsortkeys PRIVATE Qt::Test)
|
||||
|
||||
# positionorder.h is header-only: the position comparisons behind element
|
||||
# renumbering and terminal numbering, as strict weak orderings for std::sort.
|
||||
add_executable(tst_positionorder tst_positionorder.cpp)
|
||||
target_include_directories(tst_positionorder PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_positionorder PRIVATE Qt::Test Qt::Core)
|
||||
add_test(NAME tst_positionorder COMMAND tst_positionorder)
|
||||
|
||||
# bordercelllabels.h is header-only too.
|
||||
add_executable(tst_bordercelllabels tst_bordercelllabels.cpp)
|
||||
add_test(NAME tst_bordercelllabels COMMAND tst_bordercelllabels)
|
||||
@@ -104,6 +135,28 @@ add_test(NAME tst_textgrid COMMAND tst_textgrid)
|
||||
target_include_directories(tst_textgrid PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_textgrid PRIVATE Qt::Test)
|
||||
|
||||
# foliogrid.h is header-only: the folio grid step as read from the
|
||||
# settings, never 0 or negative, so no reader can divide by it or loop on it.
|
||||
add_executable(tst_foliogrid tst_foliogrid.cpp)
|
||||
target_include_directories(tst_foliogrid PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_foliogrid PRIVATE Qt::Test Qt::Core)
|
||||
add_test(NAME tst_foliogrid COMMAND tst_foliogrid)
|
||||
|
||||
# textlines.h is header-only: the lines of a text as they are laid out,
|
||||
# for the exports that write a text line by line.
|
||||
add_executable(tst_textlines tst_textlines.cpp)
|
||||
add_test(NAME tst_textlines COMMAND tst_textlines)
|
||||
set_tests_properties(tst_textlines PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
target_include_directories(tst_textlines PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_textlines PRIVATE Qt::Test Qt::Gui)
|
||||
|
||||
# elementviewgrid.h is header-only: the symbol editor's snap step at each
|
||||
# zoom (bugtracker #112), tested without an editor window.
|
||||
add_executable(tst_elementviewgrid tst_elementviewgrid.cpp)
|
||||
add_test(NAME tst_elementviewgrid COMMAND tst_elementviewgrid)
|
||||
target_include_directories(tst_elementviewgrid PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_elementviewgrid PRIVATE Qt::Test)
|
||||
|
||||
# alignment.h is header-only too (it builds on textgrid.h): the geometry
|
||||
# behind the align commands, tested without a scene.
|
||||
add_executable(tst_alignment tst_alignment.cpp)
|
||||
@@ -118,6 +171,13 @@ add_test(NAME tst_terminalnamecheck COMMAND tst_terminalnamecheck)
|
||||
target_include_directories(tst_terminalnamecheck PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_terminalnamecheck PRIVATE Qt::Test)
|
||||
|
||||
# symbolscale.h is header-only: which factors the element editor offers
|
||||
# for scaling an element without taking its terminals off the grid.
|
||||
add_executable(tst_symbolscale tst_symbolscale.cpp)
|
||||
add_test(NAME tst_symbolscale COMMAND tst_symbolscale)
|
||||
target_include_directories(tst_symbolscale PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_symbolscale PRIVATE Qt::Test)
|
||||
|
||||
# configprofile.h is header-only: which settings a saved configuration
|
||||
# profile carries, and how loading one replaces the live settings.
|
||||
add_executable(tst_configprofile tst_configprofile.cpp)
|
||||
@@ -132,6 +192,14 @@ add_test(NAME tst_itemgroups COMMAND tst_itemgroups)
|
||||
target_include_directories(tst_itemgroups PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_itemgroups PRIVATE Qt::Test Qt::Widgets Qt::Xml)
|
||||
|
||||
# shownkinds.cpp keeps View > Show (bugtracker #301) apart from the item
|
||||
# classes, so it is tested here on a plain scene of rectangles.
|
||||
add_executable(tst_shownkinds tst_shownkinds.cpp ${QET_DIR}/sources/shownkinds.cpp)
|
||||
add_test(NAME tst_shownkinds COMMAND tst_shownkinds)
|
||||
set_tests_properties(tst_shownkinds PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
target_include_directories(tst_shownkinds PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_shownkinds PRIVATE Qt::Test Qt::Widgets)
|
||||
|
||||
# textanchor.h is header-only: the point of a text chosen by its alignment,
|
||||
# shown as Position X/Y in the text properties (#1155).
|
||||
add_executable(tst_textanchor tst_textanchor.cpp)
|
||||
@@ -140,6 +208,18 @@ set_tests_properties(tst_textanchor PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offs
|
||||
target_include_directories(tst_textanchor PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_textanchor PRIVATE Qt::Test Qt::Widgets)
|
||||
|
||||
# TextResizeHandles: the corner handles that change the width of a text box,
|
||||
# on a plain QGraphicsTextItem with the "textWidth" property QET texts have.
|
||||
add_executable(
|
||||
tst_textresizehandles
|
||||
tst_textresizehandles.cpp
|
||||
${QET_DIR}/sources/QetGraphicsItemModeler/qetgraphicshandleritem.cpp
|
||||
${QET_DIR}/sources/QetGraphicsItemModeler/textresizehandles.cpp)
|
||||
add_test(NAME tst_textresizehandles COMMAND tst_textresizehandles)
|
||||
set_tests_properties(tst_textresizehandles PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
target_include_directories(tst_textresizehandles PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_textresizehandles PRIVATE Qt::Test Qt::Widgets)
|
||||
|
||||
# legacyelementtypes.h is header-only: the rewrite of element type names in
|
||||
# nomenclature queries saved before June 2022.
|
||||
add_executable(tst_legacyelementtypes tst_legacyelementtypes.cpp)
|
||||
@@ -229,6 +309,109 @@ set_tests_properties(tst_commandsearch PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=o
|
||||
target_include_directories(tst_commandsearch PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_commandsearch PRIVATE Qt::Test Qt::Widgets)
|
||||
|
||||
# ShortcutsConfigPage: icon and menu columns, category filter, finding a
|
||||
# command by pressing its key, and the copied list.
|
||||
add_executable(
|
||||
tst_shortcutspage
|
||||
tst_shortcutspage.cpp
|
||||
${QET_DIR}/sources/ui/configpage/shortcutsconfigpage.cpp
|
||||
${QET_DIR}/sources/ui/configpage/shortcutsconfigpage.h
|
||||
${QET_DIR}/sources/ui/configpage/configpage.h
|
||||
${QET_DIR}/sources/shortcutmanager.cpp
|
||||
${QET_DIR}/sources/qeticons.cpp
|
||||
${QET_DIR}/sources/qetpalette.cpp
|
||||
${QET_DIR}/sources/qetstyle.cpp)
|
||||
add_test(NAME tst_shortcutspage COMMAND tst_shortcutspage)
|
||||
set_tests_properties(tst_shortcutspage PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
target_include_directories(tst_shortcutspage PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_shortcutspage PRIVATE Qt::Test Qt::Widgets Qt::Svg)
|
||||
|
||||
# ToolbarSettings and its configuration page: defaults change nothing,
|
||||
# saved values reach every open window that took them, panels are left alone.
|
||||
add_executable(
|
||||
tst_toolbarsettings
|
||||
tst_toolbarsettings.cpp
|
||||
${QET_DIR}/sources/toolbarsettings.cpp
|
||||
${QET_DIR}/sources/toolbarsettings.h
|
||||
${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.cpp
|
||||
${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.h
|
||||
${QET_DIR}/sources/ui/configpage/configpage.h
|
||||
${QET_DIR}/sources/qeticons.cpp
|
||||
${QET_DIR}/sources/qetpalette.cpp
|
||||
${QET_DIR}/sources/qetstyle.cpp)
|
||||
add_test(NAME tst_toolbarsettings COMMAND tst_toolbarsettings)
|
||||
set_tests_properties(tst_toolbarsettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
target_include_directories(tst_toolbarsettings PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_toolbarsettings PRIVATE Qt::Test Qt::Widgets Qt::Svg)
|
||||
|
||||
# DiagramToolbarSettings and its configuration page: unchanged toolbars
|
||||
# store nothing and build as before, unknown ids and stray separators are
|
||||
# skipped, the user's own toolbars are kept, a widget is in one place only.
|
||||
add_executable(
|
||||
tst_diagramtoolbarsettings
|
||||
tst_diagramtoolbarsettings.cpp
|
||||
${QET_DIR}/sources/diagramtoolbarsettings.cpp
|
||||
${QET_DIR}/sources/diagramtoolbarsettings.h
|
||||
${QET_DIR}/sources/shortcutmanager.cpp
|
||||
${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.cpp
|
||||
${QET_DIR}/sources/ui/configpage/toolbarcommandsconfigpage.h
|
||||
${QET_DIR}/sources/ui/configpage/configpage.h
|
||||
${QET_DIR}/sources/qeticons.cpp
|
||||
${QET_DIR}/sources/qetpalette.cpp
|
||||
${QET_DIR}/sources/qetstyle.cpp)
|
||||
add_test(NAME tst_diagramtoolbarsettings COMMAND tst_diagramtoolbarsettings)
|
||||
set_tests_properties(tst_diagramtoolbarsettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
target_include_directories(tst_diagramtoolbarsettings PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_diagramtoolbarsettings PRIVATE Qt::Test Qt::Widgets Qt::Svg)
|
||||
|
||||
# GestureSettings, the gesture ring's 4 / 8 directions and empty slots, and
|
||||
# its configuration page: the ring follows the shortcut bar until the user
|
||||
# picks commands, then keeps each one in its direction.
|
||||
add_executable(
|
||||
tst_gesturesettings
|
||||
tst_gesturesettings.cpp
|
||||
${QET_DIR}/sources/gesturesettings.cpp
|
||||
${QET_DIR}/sources/gesturesettings.h
|
||||
${QET_DIR}/sources/shortcutbarsettings.cpp
|
||||
${QET_DIR}/sources/diagramgestureoverlay.cpp
|
||||
${QET_DIR}/sources/diagramgestureoverlay.h
|
||||
${QET_DIR}/sources/ui/configpage/gesturesconfigpage.cpp
|
||||
${QET_DIR}/sources/ui/configpage/gesturesconfigpage.h
|
||||
${QET_DIR}/sources/ui/configpage/configpage.h
|
||||
${QET_DIR}/sources/shortcutmanager.cpp
|
||||
${QET_DIR}/sources/qeticons.cpp
|
||||
${QET_DIR}/sources/qetpalette.cpp
|
||||
${QET_DIR}/sources/qetstyle.cpp)
|
||||
add_test(NAME tst_gesturesettings COMMAND tst_gesturesettings)
|
||||
set_tests_properties(tst_gesturesettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
target_include_directories(tst_gesturesettings PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_gesturesettings PRIVATE Qt::Test Qt::Widgets Qt::Svg Qt::Xml pugixml::pugixml)
|
||||
|
||||
# CustomizeDialog: the configuration pages as tabs of one window; OK
|
||||
# applies every page once, Cancel none.
|
||||
add_executable(
|
||||
tst_customizedialog
|
||||
tst_customizedialog.cpp
|
||||
${QET_DIR}/sources/ui/customizedialog.cpp
|
||||
${QET_DIR}/sources/ui/customizedialog.h
|
||||
${QET_DIR}/sources/ui/configpage/configpage.h)
|
||||
add_test(NAME tst_customizedialog COMMAND tst_customizedialog)
|
||||
set_tests_properties(tst_customizedialog PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
target_include_directories(tst_customizedialog PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_customizedialog PRIVATE Qt::Test Qt::Widgets)
|
||||
|
||||
# scriptheader.h is header-only: the // ==QETScript== block that turns a
|
||||
# stored script into a button; and ShortcutManager::unregisterAction(), which
|
||||
# takes a deleted script's command out of the lists again.
|
||||
add_executable(
|
||||
tst_scriptheader
|
||||
tst_scriptheader.cpp
|
||||
${QET_DIR}/sources/shortcutmanager.cpp)
|
||||
add_test(NAME tst_scriptheader COMMAND tst_scriptheader)
|
||||
set_tests_properties(tst_scriptheader PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
target_include_directories(tst_scriptheader PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_scriptheader PRIVATE Qt::Test Qt::Widgets)
|
||||
|
||||
# QETSql::execReadOnly() -- read-only enforcement for every project-database
|
||||
# query, including the ones a .qet file carries. Compiles sqlreadonly.cpp
|
||||
# alone against its own in-memory QSQLITE connection, so the security
|
||||
@@ -379,6 +562,18 @@ target_link_libraries(tst_conductorselfretrace PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_conductorselfretrace PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# A free text with a width keeps it when the project is saved again (the
|
||||
# attribute is written only when set) and is drawn wrapped to it.
|
||||
add_executable(
|
||||
tst_freetextwidth
|
||||
tst_freetextwidth.cpp)
|
||||
add_test(NAME tst_freetextwidth COMMAND tst_freetextwidth)
|
||||
set_tests_properties(tst_freetextwidth PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
|
||||
add_dependencies(tst_freetextwidth qelectrotech)
|
||||
target_link_libraries(tst_freetextwidth PRIVATE Qt::Test Qt::Gui Qt::Xml)
|
||||
target_compile_definitions(tst_freetextwidth PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# The wiring list export (--export-cables) keeps every wire drawn to a folio
|
||||
# report when a report has several, joins only one-to-one report pairs, and
|
||||
# prints the folio number rather than its template. fixtures/wiring_list_arrows.qet
|
||||
@@ -393,6 +588,113 @@ target_compile_definitions(tst_wiringlistexport PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# A label formula with ten or more sequential numbers gets the tenth value
|
||||
# for %sequ_10 (not the first one and a "0"): the real binary's
|
||||
# --export-bom on fixtures/ten_sequential_numbers.qet.
|
||||
add_executable(
|
||||
tst_tensequentialnumbers
|
||||
tst_tensequentialnumbers.cpp)
|
||||
add_test(NAME tst_tensequentialnumbers COMMAND tst_tensequentialnumbers)
|
||||
add_dependencies(tst_tensequentialnumbers qelectrotech)
|
||||
target_link_libraries(tst_tensequentialnumbers PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_tensequentialnumbers PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# --export-dxf writes a wrapped symbol text line by line, as it is drawn.
|
||||
add_executable(
|
||||
tst_dxfwrappedtext
|
||||
tst_dxfwrappedtext.cpp)
|
||||
add_test(NAME tst_dxfwrappedtext COMMAND tst_dxfwrappedtext)
|
||||
add_dependencies(tst_dxfwrappedtext qelectrotech)
|
||||
target_link_libraries(tst_dxfwrappedtext PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_dxfwrappedtext PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# --export-dxf on a folio with its title block at the right (issue #1339):
|
||||
# a circle and an arc stay one CIRCLE and one ARC, the title block is turned.
|
||||
add_executable(
|
||||
tst_dxfcurves
|
||||
tst_dxfcurves.cpp)
|
||||
add_test(NAME tst_dxfcurves COMMAND tst_dxfcurves)
|
||||
add_dependencies(tst_dxfcurves qelectrotech)
|
||||
target_link_libraries(tst_dxfcurves PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_dxfcurves PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# --export-dxf --dxf-blocks: a symbol placed twice is one BLOCK and two
|
||||
# INSERTs (issue #1339).
|
||||
add_executable(
|
||||
tst_dxfblocks
|
||||
tst_dxfblocks.cpp)
|
||||
add_test(NAME tst_dxfblocks COMMAND tst_dxfblocks)
|
||||
add_dependencies(tst_dxfblocks qelectrotech)
|
||||
target_link_libraries(tst_dxfblocks PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_dxfblocks PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# --export-pdf puts an A3 folio on an A3 page whichever way it is turned:
|
||||
# QPageSize matches standard sheets upright only, so a wide folio is matched
|
||||
# upright and turned. fixtures/pdf_page_a3.qet has one folio each way.
|
||||
add_executable(
|
||||
tst_pdfpagesize
|
||||
tst_pdfpagesize.cpp)
|
||||
add_test(NAME tst_pdfpagesize COMMAND tst_pdfpagesize)
|
||||
add_dependencies(tst_pdfpagesize qelectrotech)
|
||||
target_link_libraries(tst_pdfpagesize PRIVATE Qt::Test Qt::Gui)
|
||||
target_compile_definitions(tst_pdfpagesize PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# --export-pdf writes the cross-reference links in the same order in every
|
||||
# run, rather than in Element pointer order. Exports examples/industrial.qet
|
||||
# several times, in separate processes.
|
||||
add_executable(
|
||||
tst_pdflinkorder
|
||||
tst_pdflinkorder.cpp)
|
||||
add_test(NAME tst_pdflinkorder COMMAND tst_pdflinkorder)
|
||||
add_dependencies(tst_pdflinkorder qelectrotech)
|
||||
target_link_libraries(tst_pdflinkorder PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_pdflinkorder PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# One text per potential: the number goes on the same conductor in every
|
||||
# run when two conductors tie for longest. Exports
|
||||
# fixtures/one_text_per_potential_tie.qet through the real binary, several
|
||||
# times, since the order it used to depend on changes only between runs.
|
||||
add_executable(
|
||||
tst_potentialtextcarrier
|
||||
tst_potentialtextcarrier.cpp)
|
||||
add_test(NAME tst_potentialtextcarrier COMMAND tst_potentialtextcarrier)
|
||||
add_dependencies(tst_potentialtextcarrier qelectrotech)
|
||||
target_link_libraries(tst_potentialtextcarrier PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_potentialtextcarrier PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# Junction dots: a wire's corner that lies on another wire of the same
|
||||
# potential gets a dot, even when the two share no terminal (issue #1280).
|
||||
# Exports fixtures/junction_dot_shared_potential.qet through the real binary.
|
||||
add_executable(
|
||||
tst_junctiondots
|
||||
tst_junctiondots.cpp)
|
||||
add_test(NAME tst_junctiondots COMMAND tst_junctiondots)
|
||||
add_dependencies(tst_junctiondots qelectrotech)
|
||||
target_link_libraries(tst_junctiondots PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_junctiondots PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# With SOURCE_DATE_EPOCH set, --export-pdf writes the same bytes for the
|
||||
# same project in every run. Exports examples/741.qet several times, in
|
||||
# separate processes.
|
||||
add_executable(
|
||||
tst_pdfreproducible
|
||||
tst_pdfreproducible.cpp)
|
||||
add_test(NAME tst_pdfreproducible COMMAND tst_pdfreproducible)
|
||||
add_dependencies(tst_pdfreproducible qelectrotech)
|
||||
target_link_libraries(tst_pdfreproducible PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_pdfreproducible PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# Hops where wires cross (issue #436): the geometry in wirehops.cpp, and
|
||||
# through the real binary, that --resave keeps the project setting and
|
||||
# adds nothing to a project without it.
|
||||
@@ -407,6 +709,31 @@ target_link_libraries(tst_wirehops PRIVATE Qt::Test Qt::Gui)
|
||||
target_compile_definitions(tst_wirehops PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# Wires per terminal (discussion #1158): the rules in wiringrules.cpp, and
|
||||
# through the real binary, that --resave keeps the project setting, adds
|
||||
# none to a project without it, and that a wire past the limit is refused
|
||||
# unless the master switch is off.
|
||||
add_executable(
|
||||
tst_wiringrules
|
||||
tst_wiringrules.cpp
|
||||
${QET_DIR}/sources/wiringrules.cpp)
|
||||
add_test(NAME tst_wiringrules COMMAND tst_wiringrules)
|
||||
add_dependencies(tst_wiringrules qelectrotech)
|
||||
target_include_directories(tst_wiringrules PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_wiringrules PRIVATE Qt::Test Qt::Xml)
|
||||
target_compile_definitions(tst_wiringrules PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# The routing behind qet.routeConductor(): conductorrouter.cpp needs only
|
||||
# Qt Core, so the geometry is tested without a scene.
|
||||
add_executable(
|
||||
tst_conductorrouter
|
||||
tst_conductorrouter.cpp
|
||||
${QET_DIR}/sources/conductorrouter.cpp)
|
||||
add_test(NAME tst_conductorrouter COMMAND tst_conductorrouter)
|
||||
target_include_directories(tst_conductorrouter PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_conductorrouter PRIVATE Qt::Test)
|
||||
|
||||
# qet.conductorUuids() / qet.conductorEnds(): a script lists a folio's
|
||||
# conductors by uuid and finds each one's two ends. Runs a script through
|
||||
# the real binary's --run on fixtures/qet_bug_repro_resaved.qet, so only
|
||||
@@ -422,6 +749,28 @@ if(QET_HAS_SCRIPTING)
|
||||
target_compile_definitions(tst_scriptconductoruuid PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# qet.terminalPosition() and qet.conductorPath(), checked against each
|
||||
# other on a fixture's conductors.
|
||||
add_executable(
|
||||
tst_scriptlayoutreads
|
||||
tst_scriptlayoutreads.cpp)
|
||||
add_test(NAME tst_scriptlayoutreads COMMAND tst_scriptlayoutreads)
|
||||
add_dependencies(tst_scriptlayoutreads qelectrotech)
|
||||
target_link_libraries(tst_scriptlayoutreads PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_scriptlayoutreads PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# The project database keeps the width of a free text (independent_text),
|
||||
# read with qet.query() on fixtures/free_text_width.qet.
|
||||
add_executable(
|
||||
tst_scriptfreetextwidth
|
||||
tst_scriptfreetextwidth.cpp)
|
||||
add_test(NAME tst_scriptfreetextwidth COMMAND tst_scriptfreetextwidth)
|
||||
add_dependencies(tst_scriptfreetextwidth qelectrotech)
|
||||
target_link_libraries(tst_scriptfreetextwidth PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_scriptfreetextwidth PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# QET_SETTINGS_DIR moves the settings into an INI file there (#1178): a
|
||||
# script places a symbol only the folder's settings file can resolve.
|
||||
add_executable(
|
||||
@@ -434,6 +783,18 @@ if(QET_HAS_SCRIPTING)
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_ELEMENTS_DIR=\"${QET_DIR}/elements\"")
|
||||
|
||||
# qet.addElement() on a symbol that exists but cannot be read says so,
|
||||
# rather than reporting a collision with the copy already embedded.
|
||||
add_executable(
|
||||
tst_unreadableelement
|
||||
tst_unreadableelement.cpp)
|
||||
add_test(NAME tst_unreadableelement COMMAND tst_unreadableelement)
|
||||
add_dependencies(tst_unreadableelement qelectrotech)
|
||||
target_link_libraries(tst_unreadableelement PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_unreadableelement PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_ELEMENTS_DIR=\"${QET_DIR}/elements\"")
|
||||
|
||||
# The project database filled from the file as it is read holds exactly
|
||||
# what it holds filled from the built folios (QET_DATABASE_FROM_FOLIOS=1),
|
||||
# on every example saved once; an older file falls back, saying why.
|
||||
@@ -471,6 +832,68 @@ if(QET_HAS_SCRIPTING)
|
||||
target_compile_definitions(tst_scriptterminaluuid PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# Mirroring a symbol on its folio (#1335): its terminals swap sides and
|
||||
# face the other way, a second mirror puts it back, and the mirror is
|
||||
# kept by a save. Through the real binary's --run on perceuse.qet.
|
||||
add_executable(
|
||||
tst_scriptmirror
|
||||
tst_scriptmirror.cpp)
|
||||
add_test(NAME tst_scriptmirror COMMAND tst_scriptmirror)
|
||||
add_dependencies(tst_scriptmirror qelectrotech)
|
||||
target_link_libraries(tst_scriptmirror PRIVATE Qt::Test Qt::Xml)
|
||||
target_compile_definitions(tst_scriptmirror PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# A project can keep the texts of its turned symbols horizontal: the
|
||||
# DXF of a turned folio of tremie_vibrante.qet with the setting off and on.
|
||||
add_executable(
|
||||
tst_uprightsymboltexts
|
||||
tst_uprightsymboltexts.cpp)
|
||||
add_test(NAME tst_uprightsymboltexts COMMAND tst_uprightsymboltexts)
|
||||
add_dependencies(tst_uprightsymboltexts qelectrotech)
|
||||
target_link_libraries(tst_uprightsymboltexts PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_uprightsymboltexts PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# Undoing a crop of a picture restores its crop rectangle too, so a
|
||||
# project saved after the undo does not record the undone crop.
|
||||
add_executable(
|
||||
tst_imagecropundo
|
||||
tst_imagecropundo.cpp)
|
||||
add_test(NAME tst_imagecropundo COMMAND tst_imagecropundo)
|
||||
add_dependencies(tst_imagecropundo qelectrotech)
|
||||
target_link_libraries(tst_imagecropundo PRIVATE Qt::Test Qt::Gui)
|
||||
target_compile_definitions(tst_imagecropundo PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# The same symbol placed twice in a project whose collection does not
|
||||
# start with its "import" category (examples/lmdg.qet).
|
||||
add_executable(
|
||||
tst_importcategory
|
||||
tst_importcategory.cpp)
|
||||
add_test(NAME tst_importcategory COMMAND tst_importcategory)
|
||||
add_dependencies(tst_importcategory qelectrotech)
|
||||
target_link_libraries(tst_importcategory PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_importcategory PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\""
|
||||
"QET_ELEMENTS_DIR=\"${QET_DIR}/elements\"")
|
||||
|
||||
# qet.currentFolio() through --run: the first folio, or -1 with none; and
|
||||
# --run keeps one undo step per call (only an editor run is grouped).
|
||||
add_executable(
|
||||
tst_scriptcurrentfolio
|
||||
tst_scriptcurrentfolio.cpp)
|
||||
add_test(NAME tst_scriptcurrentfolio COMMAND tst_scriptcurrentfolio)
|
||||
add_dependencies(tst_scriptcurrentfolio qelectrotech)
|
||||
target_link_libraries(tst_scriptcurrentfolio PRIVATE Qt::Test)
|
||||
target_compile_definitions(tst_scriptcurrentfolio PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
endif()
|
||||
|
||||
# A symbol saved without a uuid gets the same one on every load, and
|
||||
@@ -499,6 +922,35 @@ target_compile_definitions(tst_derivedwireuuid PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# Element numbering schemes carry an id and elements follow them by it:
|
||||
# a legacy file gets stable ids on --resave; through --run, rename, edit,
|
||||
# refused removal and undo/redo keep the elements following the scheme.
|
||||
if(QET_HAS_SCRIPTING)
|
||||
add_executable(
|
||||
tst_elementautonumids
|
||||
tst_elementautonumids.cpp)
|
||||
add_test(NAME tst_elementautonumids COMMAND tst_elementautonumids)
|
||||
add_dependencies(tst_elementautonumids qelectrotech)
|
||||
target_link_libraries(tst_elementautonumids PRIVATE Qt::Test Qt::Xml)
|
||||
target_compile_definitions(tst_elementautonumids PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
endif()
|
||||
|
||||
# A picture whose saved crop does not match the picture shown (an undone
|
||||
# crop, saved before #1310) loads without that crop; a valid crop is kept.
|
||||
if(QET_HAS_SCRIPTING)
|
||||
add_executable(
|
||||
tst_imagestalecrop
|
||||
tst_imagestalecrop.cpp)
|
||||
add_test(NAME tst_imagestalecrop COMMAND tst_imagestalecrop)
|
||||
add_dependencies(tst_imagestalecrop qelectrotech)
|
||||
target_link_libraries(tst_imagestalecrop PRIVATE Qt::Test Qt::Gui)
|
||||
target_compile_definitions(tst_imagestalecrop PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
endif()
|
||||
|
||||
# Saving a project that was just saved changes nothing: runs the real
|
||||
# binary's --resave twice on every project in examples/, and on one whose
|
||||
# title block holds a single-space value (#973).
|
||||
@@ -513,6 +965,18 @@ target_compile_definitions(tst_resaveunchanged PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# A folio's "freeze new elements/conductors" flags, saved as the words
|
||||
# "true"/"false", are read back as words: the real binary's --resave on
|
||||
# fixtures/folio_freeze_flags.qet, one folio per combination.
|
||||
add_executable(
|
||||
tst_foliofreezeflags
|
||||
tst_foliofreezeflags.cpp)
|
||||
add_test(NAME tst_foliofreezeflags COMMAND tst_foliofreezeflags)
|
||||
add_dependencies(tst_foliofreezeflags qelectrotech)
|
||||
target_link_libraries(tst_foliofreezeflags PRIVATE Qt::Test Qt::Xml)
|
||||
target_compile_definitions(tst_foliofreezeflags PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# Replacing a symbol's definition in a project keeps its terminal uuids, so
|
||||
# the wires saved against them still load: TerminalUuids::keep() on its own,
|
||||
# then the real binary's --info on an example whose symbols were replaced.
|
||||
@@ -527,6 +991,19 @@ target_compile_definitions(tst_terminaluuids PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\""
|
||||
"QET_EXAMPLES_DIR=\"${QET_DIR}/examples\"")
|
||||
|
||||
# Sequential numbers saved the old way, as sequ_1/.../seqhf_1 attributes,
|
||||
# are all read: the real binary's --resave on
|
||||
# fixtures/legacy_sequential_attributes.qet, whose element and conductor
|
||||
# carry only seqhf_1.
|
||||
add_executable(
|
||||
tst_legacysequentialattributes
|
||||
tst_legacysequentialattributes.cpp)
|
||||
add_test(NAME tst_legacysequentialattributes COMMAND tst_legacysequentialattributes)
|
||||
add_dependencies(tst_legacysequentialattributes qelectrotech)
|
||||
target_link_libraries(tst_legacysequentialattributes PRIVATE Qt::Test Qt::Xml)
|
||||
target_compile_definitions(tst_legacysequentialattributes PRIVATE
|
||||
"QET_TEST_BINARY_PATH=\"$<TARGET_FILE:qelectrotech>\"")
|
||||
|
||||
# ConductorProperties::applyChanges() -- the Selection properties panel
|
||||
# writes only the fields the user edited.
|
||||
add_executable(
|
||||
@@ -540,6 +1017,34 @@ add_test(NAME tst_conductorapplychanges COMMAND tst_conductorapplychanges)
|
||||
target_include_directories(tst_conductorapplychanges PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_conductorapplychanges PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
|
||||
|
||||
# ConductorProperties::fromSettings() -- the default conductor width is a
|
||||
# decimal and comes back as saved (1.4 used to be read as 0).
|
||||
add_executable(
|
||||
tst_conductorsizesetting
|
||||
tst_conductorsizesetting.cpp
|
||||
${QET_DIR}/sources/conductorproperties.cpp
|
||||
${QET_DIR}/sources/qet.cpp
|
||||
${QET_DIR}/sources/qeticons.cpp
|
||||
${QET_DIR}/sources/shortcutmanager.cpp)
|
||||
add_test(NAME tst_conductorsizesetting COMMAND tst_conductorsizesetting)
|
||||
target_include_directories(tst_conductorsizesetting PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_conductorsizesetting PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
|
||||
|
||||
# ConductorMultiEdit -- editing several wires at once in the Selection
|
||||
# properties panel: order, which wires an edit reaches, one undo step for
|
||||
# all of them, and fields shown blank because the wires disagree.
|
||||
add_executable(
|
||||
tst_conductormultiedit
|
||||
tst_conductormultiedit.cpp
|
||||
${QET_DIR}/sources/conductorproperties.cpp
|
||||
${QET_DIR}/sources/QPropertyUndoCommand/qpropertyundocommand.cpp
|
||||
${QET_DIR}/sources/qet.cpp
|
||||
${QET_DIR}/sources/qeticons.cpp
|
||||
${QET_DIR}/sources/shortcutmanager.cpp)
|
||||
add_test(NAME tst_conductormultiedit COMMAND tst_conductormultiedit)
|
||||
target_include_directories(tst_conductormultiedit PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_conductormultiedit PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
|
||||
|
||||
# DiagramContext::fromXml() -- the two readers (QDom for projects, pugixml
|
||||
# for element definitions in the collection) give the same values: stray
|
||||
# spaces trimmed, accents kept.
|
||||
@@ -553,3 +1058,37 @@ add_executable(
|
||||
add_test(NAME tst_diagramcontext COMMAND tst_diagramcontext)
|
||||
target_include_directories(tst_diagramcontext PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_diagramcontext PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
|
||||
|
||||
# XRefProperties: an empty or unknown cross-reference position, as older
|
||||
# versions saved it, reads as the default instead of drawing the
|
||||
# cross-reference over the element's label (issue #1238).
|
||||
add_executable(
|
||||
tst_xrefpos
|
||||
tst_xrefpos.cpp
|
||||
${QET_DIR}/sources/properties/xrefproperties.cpp
|
||||
${QET_DIR}/sources/properties/propertiesinterface.cpp)
|
||||
add_test(NAME tst_xrefpos COMMAND tst_xrefpos)
|
||||
target_include_directories(tst_xrefpos PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_xrefpos PRIVATE Qt::Test Qt::Widgets Qt::Xml pugixml::pugixml)
|
||||
|
||||
# BorderProperties::fromXml() -- the project's <border> for new folios:
|
||||
# column width and row height are decimals and are read as such.
|
||||
add_executable(
|
||||
tst_borderpropertiesxml
|
||||
tst_borderpropertiesxml.cpp
|
||||
${QET_DIR}/sources/borderproperties.cpp)
|
||||
add_test(NAME tst_borderpropertiesxml COMMAND tst_borderpropertiesxml)
|
||||
target_include_directories(tst_borderpropertiesxml PRIVATE ${QET_DIR}/sources)
|
||||
target_link_libraries(tst_borderpropertiesxml PRIVATE Qt::Test Qt::Xml)
|
||||
|
||||
# Qt 6.4.2 writes the attributes of an element in an order that changes from
|
||||
# one process to the next, so a project saved twice by two runs of the binary
|
||||
# differs in bytes only (seen on projects without any numbering, too). A fixed
|
||||
# hash seed makes the order stable; it changes nothing for a newer Qt.
|
||||
if(Qt6_VERSION VERSION_LESS 6.5.0)
|
||||
foreach(qet_test tst_resaveunchanged tst_elementautonumids)
|
||||
if(TEST ${qet_test})
|
||||
set_tests_properties(${qet_test} PROPERTIES ENVIRONMENT "QT_HASH_SEED=0")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Real item/key-event regression, linked against the application's objects.
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QFontDatabase>
|
||||
#include <QGraphicsView>
|
||||
#include <QGlyphRun>
|
||||
#include <QKeyEvent>
|
||||
#include <QKeySequence>
|
||||
#include <QMimeData>
|
||||
#include <QRawFont>
|
||||
#include <QSettings>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTextBlock>
|
||||
#include <QTextCursor>
|
||||
#include <QTextLayout>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
#include "../../sources/diagram.h"
|
||||
#include "../../sources/qetproject.h"
|
||||
#include "../../sources/qetresult.h"
|
||||
#include "../../sources/qetmessagebox.h"
|
||||
#include "../../sources/qetgraphicsitem/dynamicelementtextitem.h"
|
||||
#include "../../sources/qetgraphicsitem/independenttextitem.h"
|
||||
|
||||
static void check(bool value, const char *message) { if (!value) throw std::runtime_error(message); }
|
||||
static void key(Diagram *scene, QGraphicsItem *item, int code, Qt::KeyboardModifiers modifiers) {
|
||||
QKeyEvent event(QEvent::KeyPress,code,modifiers); scene->sendEvent(item,&event);
|
||||
}
|
||||
static void redo(Diagram *scene, QGraphicsItem *item) {
|
||||
// Qt uses Ctrl+Y on Windows and Ctrl+Shift+Z on Linux.
|
||||
const QKeySequence shortcut(QKeySequence::Redo);
|
||||
check(shortcut.count()==1,"platform redo shortcut is a single key combination");
|
||||
const auto combination=shortcut[0];
|
||||
QKeyEvent event(QEvent::KeyPress,combination.key(),combination.keyboardModifiers());
|
||||
check(event.matches(QKeySequence::Redo),"generated key matches platform Redo");
|
||||
scene->sendEvent(item,&event);
|
||||
}
|
||||
static void clipboard(const QString &html, const QString &plain = QString(), bool withPlain = false) {
|
||||
auto *mime=new QMimeData; mime->setHtml(html); if(withPlain) mime->setText(plain); QApplication::clipboard()->setMimeData(mime);
|
||||
}
|
||||
static void select(QGraphicsTextItem *item,int start,int end) {
|
||||
auto cursor=item->textCursor(); cursor.setPosition(start); cursor.setPosition(end,QTextCursor::KeepAnchor); item->setTextCursor(cursor);
|
||||
}
|
||||
static void fieldStyle(QGraphicsTextItem *item,int start,int length,const QFont &field) {
|
||||
item->document()->documentLayout(); item->boundingRect();
|
||||
for(int i=start;i<start+length;++i) {
|
||||
if(item->toPlainText().at(i)=='\n') continue;
|
||||
QTextCursor cursor(item->document()); cursor.setPosition(i); cursor.setPosition(i+1,QTextCursor::KeepAnchor);
|
||||
const auto format=cursor.charFormat(); const QFont resolved=format.font().resolve(item->document()->defaultFont());
|
||||
check(resolved.family()==field.family()&&qAbs(resolved.pointSizeF()-field.pointSizeF())<0.001,"inserted character resolves to field font/size");
|
||||
check(!format.hasProperty(QTextFormat::ForegroundBrush)&&!format.isAnchor(),"clipboard colour/link discarded");
|
||||
const auto block=cursor.block(); const auto runs=block.layout()->glyphRuns(i-block.position(),1);
|
||||
check(!runs.isEmpty(),"inserted character laid out");
|
||||
for(const auto &run:runs) check(qAbs(run.rawFont().pixelSize()-QRawFont::fromFont(field).pixelSize())<0.01,"actual rendered font size follows field");
|
||||
}
|
||||
}
|
||||
int main(int argc,char **argv) {
|
||||
QApplication app(argc,argv); std::freopen(argv[2],"w",stdout);
|
||||
QTemporaryDir settings; QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||
QSettings::setPath(QSettings::IniFormat,QSettings::UserScope,settings.path()); QCoreApplication::setOrganizationName("QETPlainPasteRegression");
|
||||
QETProject::setBackupEnabled(false); QET::QetMessageBox::setNonInteractive(true);
|
||||
QFontDatabase::addApplicationFont(":/fonts/LiberationSans-Regular.ttf");
|
||||
try {
|
||||
const QString output=QString::fromLocal8Bit(argv[3]);
|
||||
QETProject project(QString::fromLocal8Bit(argv[1])); check(project.state()==QETProject::Ok,"fixture opens");
|
||||
auto *scene=project.diagrams().first(); auto *text=scene->elements().first()->dynamicTextItems().first();
|
||||
text->setTextFrom(DynamicElementTextItem::UserText);
|
||||
const QFont field("Liberation Sans",11); text->setFont(field);
|
||||
QGraphicsView view(scene); view.resize(400,300); view.show(); view.activateWindow(); QApplication::setActiveWindow(&view); view.setFocus(); QApplication::processEvents();
|
||||
auto edit=[&] { text->setTextInteractionFlags(Qt::TextEditorInteraction); text->setFocus(); scene->setFocus(); QApplication::processEvents(); check(text->hasFocus(),"real editing focus"); };
|
||||
auto paste=[&] { key(scene,text,Qt::Key_V,Qt::ControlModifier); };
|
||||
const QString rich="<p><a href='https://example.org'><span style='font-family:Courier New;font-size:40pt;color:#ff0000'><b><i>new</i></b></span></a></p><p>line</p>";
|
||||
text->setText("old"); edit(); select(text,0,3); clipboard(rich); paste();
|
||||
check(text->toPlainText()=="new\nline","HTML-only clipboard converts to plain with newlines"); fieldStyle(text,0,8,field);
|
||||
key(scene,text,Qt::Key_Z,Qt::ControlModifier); check(text->toPlainText()=="old","native undo is one paste step");
|
||||
redo(scene,text); check(text->toPlainText()=="new\nline","native redo"); fieldStyle(text,0,8,field);
|
||||
scene->clearFocus(); QApplication::processEvents(); check(text->text()=="new\nline","editing session updates stored user text");
|
||||
scene->undoStack().undo(); check(text->toPlainText()=="old","project undo"); scene->undoStack().redo(); check(text->toPlainText()=="new\nline","project redo");
|
||||
std::puts("PASS: HTML-only paste, newlines, field font/size, native and project undo/redo");
|
||||
text->setText("AB"); text->setHtml("<p style='font-family:Courier New;font-size:36pt'><b><i>AB</i></b></p>");
|
||||
edit(); select(text,2,2); clipboard("<span style='font-size:50pt'>ignored</span>","tail",true); paste();
|
||||
check(text->toPlainText()=="ABtail","clipboard plain representation takes precedence"); fieldStyle(text,2,4,field);
|
||||
QTextCursor prior(text->document()); prior.setPosition(0); prior.setPosition(1,QTextCursor::KeepAnchor); check(prior.charFormat().fontWeight()>=QFont::Bold,"existing preceding format untouched");
|
||||
select(text,1,4); clipboard("<span style='font-size:50pt'>Z</span>"); paste(); check(text->toPlainText()=="AZil","selection replaced in formatted document"); fieldStyle(text,1,1,field);
|
||||
key(scene,text,Qt::Key_Z,Qt::ControlModifier); check(text->toPlainText()=="ABtail","selection paste undo");
|
||||
redo(scene,text); check(text->toPlainText()=="AZil","selection paste redo");
|
||||
scene->clearFocus(); QApplication::processEvents();
|
||||
std::puts("PASS: paste after formatted text, plain/HTML precedence, selection replacement and undo/redo");
|
||||
text->setText("before"); edit(); select(text,0,6); clipboard("<b>ignored</b>","one\ntwo\nthree",true); paste(); fieldStyle(text,0,13,field); scene->clearFocus(); QApplication::processEvents();
|
||||
project.setFilePath(output+"/plain.qet"); check(project.write().isOk(),"save project");
|
||||
QDomDocument xml; QFile file(project.filePath()); check(file.open(QIODevice::ReadOnly),"read saved project"); check(bool(xml.setContent(file.readAll())),"parse saved project");
|
||||
bool found=false; const auto nodes=xml.elementsByTagName("dynamic_elmt_text");
|
||||
for(int i=0;i<nodes.size();++i) { auto node=nodes.at(i).toElement(); if(node.attribute("uuid")==text->uuid().toString()) { found=true; check(node.firstChildElement("html").isNull(),"no new HTML persistence"); check(node.firstChildElement("text").text()=="one\ntwo\nthree","existing plain XML text"); } }
|
||||
check(found,"attached text saved"); QETProject reopened(project.filePath()); check(reopened.state()==QETProject::Ok,"reopen project");
|
||||
DynamicElementTextItem *loaded=nullptr; for(auto *element:reopened.diagrams().first()->elements()) for(auto *item:element->dynamicTextItems()) if(item->uuid()==text->uuid()) loaded=item;
|
||||
check(loaded&&loaded->toPlainText()=="one\ntwo\nthree"&&loaded->font()==field,"plain text and field font survive reopen"); fieldStyle(loaded,0,13,field);
|
||||
std::puts("PASS: multiline clipboard, unchanged XML representation and save/reopen");
|
||||
QKeyEvent origin(QEvent::KeyPress,Qt::Key_V,Qt::ControlModifier|Qt::ShiftModifier); check(!origin.matches(QKeySequence::Paste),"Ctrl+Shift+V excluded from standard Paste");
|
||||
edit(); const QString before=text->toPlainText(); clipboard("<b>do not paste</b>"); scene->sendEvent(text,&origin); check(text->toPlainText()==before,"Ctrl+Shift+V not intercepted"); scene->clearFocus();
|
||||
auto *free=new IndependentTextItem; scene->addItem(free); free->setPlainText("free"); free->setTextInteractionFlags(Qt::TextEditorInteraction); free->setFocus(); scene->setFocus(); QApplication::processEvents(); select(free,0,4);
|
||||
clipboard("<p><b>bold</b> <i>italic</i> <span style='font-size:20pt'>large</span></p>"); key(scene,free,Qt::Key_V,Qt::ControlModifier);
|
||||
check(free->document()->find("bold").charFormat().fontWeight()>=QFont::Bold&&free->document()->find("italic").charFormat().fontItalic()&&free->document()->find("large").charFormat().fontPointSize()==20,"free-text rich paste unchanged");
|
||||
scene->clearFocus(); QApplication::processEvents(); project.setFilePath(output+"/free.qet"); check(project.write().isOk(),"save free text");
|
||||
QETProject freeReopened(project.filePath()); check(freeReopened.state()==QETProject::Ok,"reopen free text"); bool freeFound=false;
|
||||
for(auto *item:freeReopened.diagrams().first()->items()) if(auto *loadedFree=dynamic_cast<IndependentTextItem *>(item)) if(loadedFree->uuid()==free->uuid()) { freeFound=true; check(loadedFree->document()->find("bold").charFormat().fontWeight()>=QFont::Bold&&loadedFree->document()->find("italic").charFormat().fontItalic()&&loadedFree->document()->find("large").charFormat().fontPointSize()==20,"free-text formatting survives reopen"); }
|
||||
check(freeFound,"free text saved"); std::puts("PASS: Ctrl+Shift+V excluded; free-text rich paste and reopen unchanged");
|
||||
} catch(const std::exception &error) { std::printf("FAIL: %s\n",error.what()); return 1; }
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
<project title="F020" uuid="{484e3ed4-bbda-5334-a526-d2ceddc55f0a}" version="0.200.1">
|
||||
<properties>
|
||||
<property name="saveddate" show="1">10/2/26</property>
|
||||
<property name="saveddate-eu" show="1">02-10-2026</property>
|
||||
<property name="saveddate-us" show="1">2026-10-02</property>
|
||||
<property name="savedfilename" show="1">skeleton</property>
|
||||
<property name="savedfilepath" show="1">/tmp/qet-mcp-new-2fmbrnvi/skeleton.qet</property>
|
||||
<property name="savedtime" show="1">19:29</property>
|
||||
</properties>
|
||||
<usage enabled="true" time_spent="0"/>
|
||||
<newdiagrams>
|
||||
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<report label="%f-%l%c"/>
|
||||
<xrefs>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||
</xrefs>
|
||||
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||
<folio_autonums/>
|
||||
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||
<guides/>
|
||||
</newdiagrams>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{4bc342c7-122c-4846-9b05-5ee7d6dc677b}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<elements>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="X" type="embed://import/10_electric/10_allpole/130_terminals_terminal_strips/borne_2.elmt" uuid="{4c30414d-8678-47b8-b674-a1b87fe0347b}" x="200" y="200" z="10">
|
||||
<terminals>
|
||||
<terminal id="0" orientation="0" x="0" y="-6"/>
|
||||
<terminal id="1" orientation="1" x="6" y="0"/>
|
||||
<terminal id="2" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<elementInformations>
|
||||
<elementInformation name="machine_manufacturer_reference_auxiliary1" show="1">MMR-AUX1</elementInformation>
|
||||
<elementInformation name="machine_manufacturer_reference_auxiliary2" show="1">MMR-AUX2</elementInformation>
|
||||
<elementInformation name="machine_manufacturer_reference_auxiliary3" show="1">MMR-AUX3</elementInformation>
|
||||
<elementInformation name="machine_manufacturer_reference_auxiliary4" show="1">MMR-AUX4</elementInformation>
|
||||
<elementInformation name="manufacturer_reference_auxiliary1" show="1">MR-AUX1</elementInformation>
|
||||
<elementInformation name="manufacturer_reference_auxiliary2" show="1">MR-AUX2</elementInformation>
|
||||
<elementInformation name="manufacturer_reference_auxiliary3" show="1">MR-AUX3</elementInformation>
|
||||
<elementInformation name="manufacturer_reference_auxiliary4" show="1">MR-AUX4</elementInformation>
|
||||
</elementInformations>
|
||||
<properties>
|
||||
<element_type terminal_function="generic" terminal_led="false" terminal_type="generic"/>
|
||||
</properties>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{17dea734-db18-4b95-a27a-c4bb68473919}" x="3" y="-24.5">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,0,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="CompositeText" text_width="-1" uuid="{533b3793-9867-4a95-9ae9-bf945c8bb8d3}" x="30" y="10">
|
||||
<text>[%{machine_manufacturer_reference_auxiliary1}|MR-AUX1]</text>
|
||||
<composite_text>[%{machine_manufacturer_reference_auxiliary1}|%{manufacturer_reference_auxiliary1}]</composite_text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,0,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="CompositeText" text_width="-1" uuid="{2b1d4d47-e88f-4212-82b6-549b1565289d}" x="30" y="20">
|
||||
<text>[%{machine_manufacturer_reference_auxiliary2}|MR-AUX2]</text>
|
||||
<composite_text>[%{machine_manufacturer_reference_auxiliary2}|%{manufacturer_reference_auxiliary2}]</composite_text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,0,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="CompositeText" text_width="-1" uuid="{dc286aae-6c3e-4ee5-894c-205b06b538b0}" x="30" y="30">
|
||||
<text>[%{machine_manufacturer_reference_auxiliary3}|MR-AUX3]</text>
|
||||
<composite_text>[%{machine_manufacturer_reference_auxiliary3}|%{manufacturer_reference_auxiliary3}]</composite_text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,0,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="CompositeText" text_width="-1" uuid="{cf6e8c75-b728-46c1-b0d4-c93b84689ce6}" x="30" y="40">
|
||||
<text>[%{machine_manufacturer_reference_auxiliary4}|MR-AUX4]</text>
|
||||
<composite_text>[%{machine_manufacturer_reference_auxiliary4}|%{manufacturer_reference_auxiliary4}]</composite_text>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
</elements>
|
||||
</diagram>
|
||||
<collection>
|
||||
<category name="import">
|
||||
<names>
|
||||
<name lang="ca">Elements importats</name>
|
||||
<name lang="cs">Zavedené prvky</name>
|
||||
<name lang="da">Importerede elementer</name>
|
||||
<name lang="de">Importierte Elemente</name>
|
||||
<name lang="el">Εισηγμένα στοιχεία</name>
|
||||
<name lang="en">Imported elements</name>
|
||||
<name lang="es">Elementos importados</name>
|
||||
<name lang="fr">Éléments importés</name>
|
||||
<name lang="hr">Uvezeni elementi</name>
|
||||
<name lang="it">Elementi importati</name>
|
||||
<name lang="ja">インバートされた要素</name>
|
||||
<name lang="ko">전기 심볼 요소</name>
|
||||
<name lang="ko_KR">전기 심볼 요소</name>
|
||||
<name lang="nl">Elementen geïmporteerd</name>
|
||||
<name lang="nl_BE">Elementen geïmporteerd</name>
|
||||
<name lang="pl">Elementy importowane</name>
|
||||
<name lang="pt">Elementos importados</name>
|
||||
<name lang="pt_BR">Elementos importados</name>
|
||||
<name lang="ro">Elemente importate</name>
|
||||
<name lang="ru">Импортированные элементы</name>
|
||||
<name lang="sl">Uvoženi elementi</name>
|
||||
<name lang="sv">Importerade element</name>
|
||||
<name lang="tr">İthal öğeler</name>
|
||||
<name lang="uk">Імпортовані елементи</name>
|
||||
<name lang="zh">导入元件</name>
|
||||
</names>
|
||||
<category name="10_electric">
|
||||
<names>
|
||||
<name lang="ar">كهربائي</name>
|
||||
<name lang="ca">Elèctric</name>
|
||||
<name lang="cs">Elektrotechnika</name>
|
||||
<name lang="da">Elektrisk</name>
|
||||
<name lang="de">Elektrik</name>
|
||||
<name lang="el">Ηλεκτρικά</name>
|
||||
<name lang="en">Electric</name>
|
||||
<name lang="es">Eléctrica</name>
|
||||
<name lang="fi">Sähkö</name>
|
||||
<name lang="fr">Electrique</name>
|
||||
<name lang="hr">Električno</name>
|
||||
<name lang="hu">Elektromos</name>
|
||||
<name lang="it">Elettrica</name>
|
||||
<name lang="ja">電気</name>
|
||||
<name lang="ko">전기</name>
|
||||
<name lang="mn">Цахилгаан</name>
|
||||
<name lang="nb">Elektrisk</name>
|
||||
<name lang="nl">Elektrotechniek</name>
|
||||
<name lang="nl_BE">Elektrisch</name>
|
||||
<name lang="pl">Elektrotechnika</name>
|
||||
<name lang="pt">Elétrico</name>
|
||||
<name lang="pt_BR">Elétrica</name>
|
||||
<name lang="ro">Electric</name>
|
||||
<name lang="ru">Электротехника</name>
|
||||
<name lang="sk">Elektrické</name>
|
||||
<name lang="sl">Elektrika</name>
|
||||
<name lang="sr">Електрика</name>
|
||||
<name lang="sv">El</name>
|
||||
<name lang="tr">Elektrik</name>
|
||||
<name lang="uk">Електрика</name>
|
||||
<name lang="zh">10_电气</name>
|
||||
<name lang="zn">电气工程</name>
|
||||
</names>
|
||||
<category name="10_allpole">
|
||||
<names>
|
||||
<name lang="ar">متعدد الأقطاب</name>
|
||||
<name lang="ca">Multifilar</name>
|
||||
<name lang="cs">Vícežilový</name>
|
||||
<name lang="da">Flere ledere</name>
|
||||
<name lang="de">Allpolig</name>
|
||||
<name lang="el">Πολυγραμμικό</name>
|
||||
<name lang="en">All-pole</name>
|
||||
<name lang="es">Multifilar</name>
|
||||
<name lang="fi">Yleisnapa</name>
|
||||
<name lang="fr">Multifilaire</name>
|
||||
<name lang="hr">Univerzalni</name>
|
||||
<name lang="hu">Többvonalas szimbólumok</name>
|
||||
<name lang="it">Multifilare</name>
|
||||
<name lang="ja">複線</name>
|
||||
<name lang="ko">다-극</name>
|
||||
<name lang="mn">Бүх туйл</name>
|
||||
<name lang="nb">Alle poler</name>
|
||||
<name lang="nl">Veel polig</name>
|
||||
<name lang="nl_BE">All-pole</name>
|
||||
<name lang="pl">Schematy wieloliniowe</name>
|
||||
<name lang="pt">Multipolar</name>
|
||||
<name lang="pt_BR">Multifilar</name>
|
||||
<name lang="ro">Bipolar</name>
|
||||
<name lang="ru">Многополюсные</name>
|
||||
<name lang="sk">Všepólové</name>
|
||||
<name lang="sl">Vsi poli</name>
|
||||
<name lang="sr">Свe-полни</name>
|
||||
<name lang="sv">Allpolig</name>
|
||||
<name lang="tr">Çok kutuplu</name>
|
||||
<name lang="uk">Багатополюсні</name>
|
||||
<name lang="zh">10_全极</name>
|
||||
</names>
|
||||
<category name="130_terminals_terminal_strips">
|
||||
<names>
|
||||
<name lang="ar">طرفيات توصيل</name>
|
||||
<name lang="ca">Borns i regletes de borns</name>
|
||||
<name lang="cs">Svorky</name>
|
||||
<name lang="da">Klemmer og klemmerækker</name>
|
||||
<name lang="de">Klemmen und Klemmleisten</name>
|
||||
<name lang="el">Ακροδέκτες και σειρές ακροδεκτών</name>
|
||||
<name lang="en">Terminals and terminal strips</name>
|
||||
<name lang="es">Terminales</name>
|
||||
<name lang="fi">Rimaelittimet</name>
|
||||
<name lang="fr">Bornes et borniers</name>
|
||||
<name lang="hr">Stezaljke i redne stezaljke</name>
|
||||
<name lang="hu">Sorkapcsok</name>
|
||||
<name lang="it">Terminali</name>
|
||||
<name lang="ja">端子と端子台</name>
|
||||
<name lang="ko">단자 및 단자대</name>
|
||||
<name lang="mn">Клемм ба клеммын хавтан</name>
|
||||
<name lang="nb">Rekkeklemmer og klemlister</name>
|
||||
<name lang="nl">Klemmenlijst</name>
|
||||
<name lang="nl_BE">Klemmen en klemmenstroken</name>
|
||||
<name lang="pl">Złącza i zaciski</name>
|
||||
<name lang="pt">Terminais</name>
|
||||
<name lang="pt_BR">Bornes e borneiras</name>
|
||||
<name lang="ro">Borne și borniere</name>
|
||||
<name lang="ru">Клеммы и клеммники</name>
|
||||
<name lang="sk">Svorky a svorkovnice</name>
|
||||
<name lang="sl">Vrstne sponke in sponke</name>
|
||||
<name lang="sr">Клеме и редне клеме</name>
|
||||
<name lang="sv">Plintar och plintrader</name>
|
||||
<name lang="tr">Klemensler ve klemens şeritleri</name>
|
||||
<name lang="uk">Клеми та клемні колодки</name>
|
||||
<name lang="zh">接线端子与端子排</name>
|
||||
</names>
|
||||
<element name="borne_2.elmt">
|
||||
<definition height="40" hotspot_x="10" hotspot_y="20" link_type="terminal" type="element" version="0.100.0" width="30">
|
||||
<uuid uuid="{3f1985d3-104c-d0fe-e025-de0b19429826}"/>
|
||||
<names>
|
||||
<name lang="ar">طرف توصيل</name>
|
||||
<name lang="ca">Born de continuïtat</name>
|
||||
<name lang="cs">Spojovací svorka</name>
|
||||
<name lang="da">Klemme</name>
|
||||
<name lang="de">Klemme</name>
|
||||
<name lang="el">Ακροδέκτης</name>
|
||||
<name lang="en">Terminal block</name>
|
||||
<name lang="es">Terminal de unión</name>
|
||||
<name lang="fi">Liitinrima</name>
|
||||
<name lang="fr">Borne continuité</name>
|
||||
<name lang="hr">Stezaljka</name>
|
||||
<name lang="hu">Sorkapocs</name>
|
||||
<name lang="it">Terminale di unione</name>
|
||||
<name lang="ja">端子ブロック</name>
|
||||
<name lang="ko">단자 블록</name>
|
||||
<name lang="mn">Клеммын блок</name>
|
||||
<name lang="nb">Rekkeklemme</name>
|
||||
<name lang="nl">klem doorgang</name>
|
||||
<name lang="nl_BE">Klemmenblok</name>
|
||||
<name lang="pl">Zacisk</name>
|
||||
<name lang="pt">Terminal de combinação</name>
|
||||
<name lang="pt_BR">Borne de combinacao</name>
|
||||
<name lang="ro">Bloc de borne</name>
|
||||
<name lang="ru">Клеммное соединение</name>
|
||||
<name lang="sk">Svorkovnica</name>
|
||||
<name lang="sl">Vrstna sponka</name>
|
||||
<name lang="sr">Клемни блок</name>
|
||||
<name lang="sv">Plint</name>
|
||||
<name lang="tr">Klemens bloğu</name>
|
||||
<name lang="uk">Клемна колодка</name>
|
||||
<name lang="zh">2号接线端子</name>
|
||||
</names>
|
||||
<kindInformations>
|
||||
<kindInformation name="type">generic</kindInformation>
|
||||
<kindInformation name="function">generic</kindInformation>
|
||||
</kindInformations>
|
||||
<informations>Author: The QElectroTech team
|
||||
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
|
||||
<description>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="10" y2="3"/>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="-10" y2="-3"/>
|
||||
<ellipse antialias="true" height="5" style="line-style:normal;line-weight:normal;filling:none;color:black" width="5" x="-2.5" y="-2.5"/>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="10" x2="3" y1="0" y2="0"/>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="ElementInfo" text_width="-1" uuid="{8e248a1f-fe85-48a3-8940-9a66b8032f11}" x="3" y="-24.5" z="5">
|
||||
<text/>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_text>
|
||||
<terminal name="" orientation="n" type="Generic" uuid="{323e68e1-514f-4e03-8ece-f22744a2d325}" x="0" y="-10"/>
|
||||
<terminal name="" orientation="s" type="Generic" uuid="{c81262f0-711b-451d-a333-9039f4de25b6}" x="0" y="10"/>
|
||||
<terminal name="" orientation="e" type="Generic" uuid="{a33d1699-88d4-4071-a651-977d380e6e8c}" x="10" y="0"/>
|
||||
</description>
|
||||
</definition>
|
||||
</element>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</collection>
|
||||
</project>
|
||||
@@ -0,0 +1,79 @@
|
||||
<project title="DXF symbol blocks" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7b10}" version="0.200.1">
|
||||
<properties>
|
||||
<property name="saveddate" show="1">10/2/26</property>
|
||||
<property name="saveddate-eu" show="1">02-10-2026</property>
|
||||
<property name="saveddate-us" show="1">2026-10-02</property>
|
||||
<property name="savedfilename" show="1">dxf_blocks</property>
|
||||
<property name="savedtime" show="1">18:39</property>
|
||||
</properties>
|
||||
<usage enabled="true" time_spent="0"/>
|
||||
<newdiagrams>
|
||||
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<report label="%f-%l%c"/>
|
||||
<xrefs>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||
</xrefs>
|
||||
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||
<folio_autonums/>
|
||||
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||
<guides/>
|
||||
</newdiagrams>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a01}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<elements>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="" type="embed://import/curves.elmt" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a02}" x="300" y="300" z="10">
|
||||
<terminals/>
|
||||
<inputs/>
|
||||
<elementInformations>
|
||||
<elementInformation name="label" show="1">K1</elementInformation>
|
||||
<elementInformation name="manufacturer" show="1">Schneider</elementInformation>
|
||||
</elementInformations>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7b01}" x="0" y="25">
|
||||
<text>K1</text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="1" prefix="" type="embed://import/curves.elmt" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a03}" x="500" y="300" z="10">
|
||||
<terminals/>
|
||||
<inputs/>
|
||||
<elementInformations>
|
||||
<elementInformation name="label" show="1">K2</elementInformation>
|
||||
</elementInformations>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7b02}" x="0" y="25">
|
||||
<text>K2</text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
</elements>
|
||||
</diagram>
|
||||
<collection>
|
||||
<category name="import">
|
||||
<names>
|
||||
<name lang="en">Imported elements</name>
|
||||
</names>
|
||||
<element name="curves.elmt">
|
||||
<definition height="80" hotspot_x="10" hotspot_y="10" link_type="simple" type="element" version="0.100.0" width="120">
|
||||
<uuid uuid="{7d2b8e3f-0c65-4e30-9079-3f0d0f0a7a05}"/>
|
||||
<names>
|
||||
<name lang="en">Circle and arc</name>
|
||||
</names>
|
||||
<description>
|
||||
<ellipse antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="0" y="0" width="20" height="20"/>
|
||||
<arc antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="40" y="0" width="20" height="20" start="30" angle="120"/>
|
||||
</description>
|
||||
</definition>
|
||||
</element>
|
||||
</category>
|
||||
</collection>
|
||||
</project>
|
||||
@@ -0,0 +1,56 @@
|
||||
<project title="DXF curves and title block" uuid="{6c1a7d2e-9b54-4d2f-8f68-3f0d0f0a7b10}" version="0.200.1">
|
||||
<properties>
|
||||
<property name="saveddate" show="1">10/2/26</property>
|
||||
<property name="saveddate-eu" show="1">02-10-2026</property>
|
||||
<property name="saveddate-us" show="1">2026-10-02</property>
|
||||
<property name="savedfilename" show="1">dxf_curves</property>
|
||||
<property name="savedtime" show="1">18:39</property>
|
||||
</properties>
|
||||
<usage enabled="true" time_spent="0"/>
|
||||
<newdiagrams>
|
||||
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<report label="%f-%l%c"/>
|
||||
<xrefs>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||
</xrefs>
|
||||
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||
<folio_autonums/>
|
||||
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||
<guides/>
|
||||
</newdiagrams>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="right" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{6c1a7d2e-9b54-4d2f-8f68-3f0d0f0a7a01}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<elements>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="" type="embed://import/curves.elmt" uuid="{6c1a7d2e-9b54-4d2f-8f68-3f0d0f0a7a02}" x="300" y="300" z="10">
|
||||
<terminals/>
|
||||
<inputs/>
|
||||
<dynamic_texts/>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
</elements>
|
||||
</diagram>
|
||||
<collection>
|
||||
<category name="import">
|
||||
<names>
|
||||
<name lang="en">Imported elements</name>
|
||||
</names>
|
||||
<element name="curves.elmt">
|
||||
<definition height="80" hotspot_x="10" hotspot_y="10" link_type="simple" type="element" version="0.100.0" width="120">
|
||||
<uuid uuid="{6c1a7d2e-9b54-4d2f-8f68-3f0d0f0a7a05}"/>
|
||||
<names>
|
||||
<name lang="en">Circle and arc</name>
|
||||
</names>
|
||||
<description>
|
||||
<ellipse antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="0" y="0" width="20" height="20"/>
|
||||
<arc antialias="true" style="line-style:normal;line-weight:normal;filling:none;color:black" x="40" y="0" width="20" height="20" start="30" angle="120"/>
|
||||
</description>
|
||||
</definition>
|
||||
</element>
|
||||
</category>
|
||||
</collection>
|
||||
</project>
|
||||
@@ -0,0 +1,60 @@
|
||||
<project title="DXF wrapped texts" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b10}" version="0.200.1">
|
||||
<properties>
|
||||
<property name="saveddate" show="1">10/2/26</property>
|
||||
<property name="saveddate-eu" show="1">02-10-2026</property>
|
||||
<property name="saveddate-us" show="1">2026-10-02</property>
|
||||
<property name="savedfilename" show="1">dxf_wrapped_text</property>
|
||||
<property name="savedtime" show="1">18:39</property>
|
||||
</properties>
|
||||
<usage enabled="true" time_spent="0"/>
|
||||
<newdiagrams>
|
||||
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<report label="%f-%l%c"/>
|
||||
<xrefs>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||
</xrefs>
|
||||
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||
<folio_autonums/>
|
||||
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||
<guides/>
|
||||
</newdiagrams>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a01}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<elements>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="" type="embed://import/wrap.elmt" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a02}" x="300" y="300" z="10">
|
||||
<terminals/>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="UserText" text_width="70" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b01}" x="0" y="10"><text>WrapAlpha WrapBeta WrapGamma WrapDelta WrapEpsilon</text></dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b02}" x="0" y="60"><text>LineAlpha LineBeta LineGamma LineDelta LineEpsilon</text></dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
</elements>
|
||||
</diagram>
|
||||
<collection>
|
||||
<category name="import">
|
||||
<names>
|
||||
<name lang="en">Imported elements</name>
|
||||
</names>
|
||||
<element name="wrap.elmt">
|
||||
<definition height="80" hotspot_x="10" hotspot_y="10" link_type="simple" type="element" version="0.100.0" width="120">
|
||||
<uuid uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a05}"/>
|
||||
<names>
|
||||
<name lang="en">Wrapped symbol text</name>
|
||||
</names>
|
||||
<description>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="10" y1="0" y2="0"/>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" z="2" text_from="UserText" text_width="70" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b01}" x="0" y="10"><text>WrapAlpha WrapBeta WrapGamma WrapDelta WrapEpsilon</text></dynamic_text>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" z="3" text_from="UserText" text_width="-1" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7b02}" x="0" y="60"><text>LineAlpha LineBeta LineGamma LineDelta LineEpsilon</text></dynamic_text>
|
||||
</description>
|
||||
</definition>
|
||||
</element>
|
||||
</category>
|
||||
</collection>
|
||||
</project>
|
||||
@@ -0,0 +1,36 @@
|
||||
<project title="Folio freeze flags" uuid="{7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e00}" version="0.200.1">
|
||||
<properties>
|
||||
<property name="savedfilename" show="1">folio_freeze_flags</property>
|
||||
</properties>
|
||||
<usage enabled="true" time_spent="0"/>
|
||||
<newdiagrams>
|
||||
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<report label="%f-%l%c"/>
|
||||
<xrefs/>
|
||||
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||
<folio_autonums/>
|
||||
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||
<guides/>
|
||||
</newdiagrams>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="true" freezeNewElement="true" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="both frozen" uuid="{7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e01}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
</diagram>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="true" height="660" indexrev="" locmach="" order="2" plant="" rows="8" rowsize="80" title="elements frozen" uuid="{7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e02}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
</diagram>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="3" plant="" rows="8" rowsize="80" title="not frozen" uuid="{7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e03}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
</diagram>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" height="660" indexrev="" locmach="" order="4" plant="" rows="8" rowsize="80" title="no attributes" uuid="{7c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e04}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
</diagram>
|
||||
<collection>
|
||||
<category name="import">
|
||||
<names>
|
||||
<name lang="en">Imported elements</name>
|
||||
</names>
|
||||
</category>
|
||||
</collection>
|
||||
</project>
|
||||
@@ -0,0 +1,41 @@
|
||||
<project title="Free text widths" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a00}" version="0.200.1">
|
||||
<properties>
|
||||
<property name="saveddate" show="1">10/2/26</property>
|
||||
<property name="saveddate-eu" show="1">02-10-2026</property>
|
||||
<property name="saveddate-us" show="1">2026-10-02</property>
|
||||
<property name="savedfilename" show="1">free_text_width</property>
|
||||
<property name="savedtime" show="1">18:39</property>
|
||||
</properties>
|
||||
<usage enabled="true" time_spent="0"/>
|
||||
<newdiagrams>
|
||||
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<report label="%f-%l%c"/>
|
||||
<xrefs>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||
</xrefs>
|
||||
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||
<folio_autonums/>
|
||||
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||
<guides/>
|
||||
</newdiagrams>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a01}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<inputs>
|
||||
<input font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" rotation="0" text="FreeAlpha FreeBeta FreeGamma FreeDelta FreeEpsilon" text_width="70" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a03}" x="100" y="100"/>
|
||||
<input font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" rotation="0" text="OpenAlpha OpenBeta OpenGamma OpenDelta OpenEpsilon" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a04}" x="100" y="400"/>
|
||||
<input font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" rotation="0" text="<p align="center">CentAlpha CentBeta CentGamma</p>" text_width="120" uuid="{5b0f5e1c-8a43-4c1e-9e57-3f0d0f0a7a08}" x="400" y="100"/>
|
||||
</inputs>
|
||||
</diagram>
|
||||
<collection>
|
||||
<category name="import">
|
||||
<names>
|
||||
<name lang="en">Imported elements</name>
|
||||
</names>
|
||||
</category>
|
||||
</collection>
|
||||
</project>
|
||||
@@ -0,0 +1,354 @@
|
||||
<project title="i1280" uuid="{abc27554-b301-5651-8055-41e0ce4599cb}" version="0.200.1">
|
||||
<properties>
|
||||
<property name="saveddate" show="1">10/4/26</property>
|
||||
<property name="saveddate-eu" show="1">04-10-2026</property>
|
||||
<property name="saveddate-us" show="1">2026-10-04</property>
|
||||
<property name="savedfilename" show="1">skeleton</property>
|
||||
<property name="savedfilepath" show="1">/tmp/qet-mcp-new-jk9lz64u/skeleton.qet</property>
|
||||
<property name="savedtime" show="1">10:28</property>
|
||||
</properties>
|
||||
<usage enabled="true" time_spent="0"/>
|
||||
<newdiagrams>
|
||||
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<report label="%f-%l%c"/>
|
||||
<xrefs>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||
</xrefs>
|
||||
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||
<folio_autonums/>
|
||||
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||
<guides/>
|
||||
</newdiagrams>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{a94486ed-a490-4ae0-9698-b075035de54d}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<elements>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" x="100" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="0" orientation="0" x="0" y="-26"/>
|
||||
<terminal id="1" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{1b01d6c6-86f7-44e4-8e76-eba4b3a53dea}" x="-13" y="-2">
|
||||
<text>14</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{65cedcc2-1a82-4168-858c-8751bc1561d9}" x="-13" y="-34">
|
||||
<text>11</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{91920aeb-94ef-423e-a282-206489f04e5e}" x="1" y="-22">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" x="200" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="2" orientation="0" x="0" y="-26"/>
|
||||
<terminal id="3" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{827f6a67-cf32-448a-b33a-e52fbbb9cee7}" x="-13" y="-2">
|
||||
<text>14</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{8fe1bb6c-bd54-429f-b8b9-2b1ca789ed2c}" x="-13" y="-34">
|
||||
<text>11</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{15fa6b73-1702-4e17-b821-3bd12f81e7ba}" x="1" y="-22">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{b6bceef1-5f5c-4119-bc0c-4ea629eb0baa}" x="300" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="4" orientation="0" x="0" y="-26"/>
|
||||
<terminal id="5" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{93d83639-c2c1-4af5-9048-c0a5e0f241b8}" x="-13" y="-2">
|
||||
<text>14</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{c14602f0-24e8-463c-a183-3dac7f78e009}" x="-13" y="-34">
|
||||
<text>11</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{662e7f9d-9f6d-40b4-b517-7b584f6b3604}" x="1" y="-22">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{5d8752c5-1f02-4ef1-a24f-c7844ede38c2}" x="400" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="6" orientation="0" x="0" y="-26"/>
|
||||
<terminal id="7" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{3c604f1b-1498-4b2b-9f2d-0063e2bcb78c}" x="-13" y="-2">
|
||||
<text>14</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{dbb98cf7-9d66-4d99-91d9-2149077d6ba2}" x="-13" y="-34">
|
||||
<text>11</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{a8efc4c9-bfb3-4bde-a501-6b1bd5f93ae5}" x="1" y="-22">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
</elements>
|
||||
<conductors>
|
||||
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" element1_label="" element1_name="Normally open counter" element2="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{f5162c59-0c59-4e94-b6a6-4cf259a52d91}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
|
||||
<sequentialNumbers/>
|
||||
</conductor>
|
||||
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" element1_label="" element1_name="Normally open counter" element2="{5d8752c5-1f02-4ef1-a24f-c7844ede38c2}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{89cacce1-b12a-4ec3-aadf-d3f8feb7494d}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
|
||||
<sequentialNumbers/>
|
||||
</conductor>
|
||||
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" element1_label="" element1_name="Normally open counter" element2="{b6bceef1-5f5c-4119-bc0c-4ea629eb0baa}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{944a0069-bad6-4d92-91ad-7aedf3961c8a}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
|
||||
<sequentialNumbers/>
|
||||
</conductor>
|
||||
</conductors>
|
||||
</diagram>
|
||||
<collection>
|
||||
<category name="import">
|
||||
<names>
|
||||
<name lang="ca">Elements importats</name>
|
||||
<name lang="cs">Zavedené prvky</name>
|
||||
<name lang="da">Importerede elementer</name>
|
||||
<name lang="de">Importierte Elemente</name>
|
||||
<name lang="el">Εισηγμένα στοιχεία</name>
|
||||
<name lang="en">Imported elements</name>
|
||||
<name lang="es">Elementos importados</name>
|
||||
<name lang="fr">Éléments importés</name>
|
||||
<name lang="hr">Uvezeni elementi</name>
|
||||
<name lang="it">Elementi importati</name>
|
||||
<name lang="ja">インバートされた要素</name>
|
||||
<name lang="ko">전기 심볼 요소</name>
|
||||
<name lang="ko_KR">전기 심볼 요소</name>
|
||||
<name lang="nl">Elementen geïmporteerd</name>
|
||||
<name lang="nl_BE">Elementen geïmporteerd</name>
|
||||
<name lang="pl">Elementy importowane</name>
|
||||
<name lang="pt">Elementos importados</name>
|
||||
<name lang="pt_BR">Elementos importados</name>
|
||||
<name lang="ro">Elemente importate</name>
|
||||
<name lang="ru">Импортированные элементы</name>
|
||||
<name lang="sl">Uvoženi elementi</name>
|
||||
<name lang="sv">Importerade element</name>
|
||||
<name lang="tr">İthal öğeler</name>
|
||||
<name lang="uk">Імпортовані елементи</name>
|
||||
<name lang="zh">导入元件</name>
|
||||
</names>
|
||||
<category name="10_electric">
|
||||
<names>
|
||||
<name lang="ar">كهربائي</name>
|
||||
<name lang="ca">Elèctric</name>
|
||||
<name lang="cs">Elektrotechnika</name>
|
||||
<name lang="da">Elektrisk</name>
|
||||
<name lang="de">Elektrik</name>
|
||||
<name lang="el">Ηλεκτρικά</name>
|
||||
<name lang="en">Electric</name>
|
||||
<name lang="es">Eléctrica</name>
|
||||
<name lang="fi">Sähkö</name>
|
||||
<name lang="fr">Electrique</name>
|
||||
<name lang="hr">Električno</name>
|
||||
<name lang="hu">Elektromos</name>
|
||||
<name lang="it">Elettrica</name>
|
||||
<name lang="ja">電気</name>
|
||||
<name lang="ko">전기</name>
|
||||
<name lang="mn">Цахилгаан</name>
|
||||
<name lang="nb">Elektrisk</name>
|
||||
<name lang="nl">Elektrotechniek</name>
|
||||
<name lang="nl_BE">Elektrisch</name>
|
||||
<name lang="pl">Elektrotechnika</name>
|
||||
<name lang="pt">Elétrico</name>
|
||||
<name lang="pt_BR">Elétrica</name>
|
||||
<name lang="ro">Electric</name>
|
||||
<name lang="ru">Электротехника</name>
|
||||
<name lang="sk">Elektrické</name>
|
||||
<name lang="sl">Elektrika</name>
|
||||
<name lang="sr">Електрика</name>
|
||||
<name lang="sv">El</name>
|
||||
<name lang="tr">Elektrik</name>
|
||||
<name lang="uk">Електрика</name>
|
||||
<name lang="zh">10_电气</name>
|
||||
<name lang="zn">电气工程</name>
|
||||
</names>
|
||||
<category name="10_allpole">
|
||||
<names>
|
||||
<name lang="ar">متعدد الأقطاب</name>
|
||||
<name lang="ca">Multifilar</name>
|
||||
<name lang="cs">Vícežilový</name>
|
||||
<name lang="da">Flere ledere</name>
|
||||
<name lang="de">Allpolig</name>
|
||||
<name lang="el">Πολυγραμμικό</name>
|
||||
<name lang="en">All-pole</name>
|
||||
<name lang="es">Multifilar</name>
|
||||
<name lang="fi">Yleisnapa</name>
|
||||
<name lang="fr">Multifilaire</name>
|
||||
<name lang="hr">Univerzalni</name>
|
||||
<name lang="hu">Többvonalas szimbólumok</name>
|
||||
<name lang="it">Multifilare</name>
|
||||
<name lang="ja">複線</name>
|
||||
<name lang="ko">다-극</name>
|
||||
<name lang="mn">Бүх туйл</name>
|
||||
<name lang="nb">Alle poler</name>
|
||||
<name lang="nl">Veel polig</name>
|
||||
<name lang="nl_BE">All-pole</name>
|
||||
<name lang="pl">Schematy wieloliniowe</name>
|
||||
<name lang="pt">Multipolar</name>
|
||||
<name lang="pt_BR">Multifilar</name>
|
||||
<name lang="ro">Bipolar</name>
|
||||
<name lang="ru">Многополюсные</name>
|
||||
<name lang="sk">Všepólové</name>
|
||||
<name lang="sl">Vsi poli</name>
|
||||
<name lang="sr">Свe-полни</name>
|
||||
<name lang="sv">Allpolig</name>
|
||||
<name lang="tr">Çok kutuplu</name>
|
||||
<name lang="uk">Багатополюсні</name>
|
||||
<name lang="zh">10_全极</name>
|
||||
</names>
|
||||
<category name="310_relays_contactors_contacts">
|
||||
<names>
|
||||
<name lang="ar">المرحّلات والموصّلات وجهات الاتصال</name>
|
||||
<name lang="ca">Relés, contactors i contactes</name>
|
||||
<name lang="cs">Relé, stykače a kontakty</name>
|
||||
<name lang="da">Relæer, kontaktorer og kontakter</name>
|
||||
<name lang="de">Relais, Schütze und Kontakte</name>
|
||||
<name lang="el">Ηλεκτρονόμοι και επαφές</name>
|
||||
<name lang="en">Relays, contactors and contacts</name>
|
||||
<name lang="es">Relevadores, contactores y contactos</name>
|
||||
<name lang="fi">Releet, kontaktorit ja koskettimet</name>
|
||||
<name lang="fr">Relais, contacteurs et contacts</name>
|
||||
<name lang="hr">Releji, kontaktori i kontakti</name>
|
||||
<name lang="hu">Relék, kontaktorok és érintkezők</name>
|
||||
<name lang="it">Relè, contattori e contatti</name>
|
||||
<name lang="ja">継電器、接触器、接点</name>
|
||||
<name lang="ko">릴레이, 커넥터및 접점</name>
|
||||
<name lang="mn">Реле, контактор болон контакт</name>
|
||||
<name lang="nb">Reléer, kontaktorere og kontakter</name>
|
||||
<name lang="nl">Relais, contactoren en contacten</name>
|
||||
<name lang="nl_BE">Relais, contacteurs en contacten</name>
|
||||
<name lang="pl">Przekaźniki i styczniki</name>
|
||||
<name lang="pt">Relés, contatores e contactos</name>
|
||||
<name lang="pt_BR">Relés, Contatores e contatos</name>
|
||||
<name lang="ro">Relee, contactoare și contacte</name>
|
||||
<name lang="ru">Реле, пускатели и контакты</name>
|
||||
<name lang="sk">Relé, stykače a kontakty</name>
|
||||
<name lang="sl">Releji, kontaktorji in kontakti</name>
|
||||
<name lang="sr">Релеји, контактори и контакти</name>
|
||||
<name lang="sv">Reläer, kontaktorer och kontakter</name>
|
||||
<name lang="tr">Röleler, kontaktörler ve kontaklar</name>
|
||||
<name lang="uk">Реле, контактори та контакти</name>
|
||||
<name lang="zh">继电器、接触器与触点</name>
|
||||
</names>
|
||||
<category name="03_contacts">
|
||||
<names>
|
||||
<name lang="ar">جهات الاتصال</name>
|
||||
<name lang="ca">Contactes</name>
|
||||
<name lang="cs">Kontakty</name>
|
||||
<name lang="da">Kontaktpersoner</name>
|
||||
<name lang="de">Kontakte</name>
|
||||
<name lang="el">Επαφές</name>
|
||||
<name lang="en">Contacts</name>
|
||||
<name lang="es">Contactos</name>
|
||||
<name lang="fi">Kosketimet</name>
|
||||
<name lang="fr">Contacts</name>
|
||||
<name lang="hr">Kontakti</name>
|
||||
<name lang="hu">Kapcsolatok</name>
|
||||
<name lang="it">Contatti</name>
|
||||
<name lang="ja">連絡先</name>
|
||||
<name lang="ko">접점</name>
|
||||
<name lang="mn">Харилцагчид</name>
|
||||
<name lang="nb">Kontakter</name>
|
||||
<name lang="nl">Contacten</name>
|
||||
<name lang="nl_BE">Contacten</name>
|
||||
<name lang="pl">Łączność</name>
|
||||
<name lang="pt">Contatos</name>
|
||||
<name lang="pt_BR">Contatos</name>
|
||||
<name lang="ro">Contacte</name>
|
||||
<name lang="ru">Контакты</name>
|
||||
<name lang="sk">Kontakty</name>
|
||||
<name lang="sl">Stiki</name>
|
||||
<name lang="sr">Контакти</name>
|
||||
<name lang="sv">Kontakter</name>
|
||||
<name lang="tr">Kişiler</name>
|
||||
<name lang="uk">Контакти</name>
|
||||
<name lang="zh">触点</name>
|
||||
</names>
|
||||
<element name="act_comptage_no.elmt">
|
||||
<definition height="60" hotspot_x="22" hotspot_y="41" link_type="simple" type="element" version="0.100.0" width="30">
|
||||
<uuid uuid="{19fdfdc7-c1fe-4604-cb14-da967d1a5252}"/>
|
||||
<names>
|
||||
<name lang="ar">عدّاد مفتوح عادة</name>
|
||||
<name lang="ca">Taulell normalment obert</name>
|
||||
<name lang="cs">Kontakt čítače (NO)</name>
|
||||
<name lang="da">Normalt åben tæller</name>
|
||||
<name lang="de">Öffner Zähler betätigt</name>
|
||||
<name lang="el">Ανοικτή επαφή μετρητή</name>
|
||||
<name lang="en">Normally open counter</name>
|
||||
<name lang="es">Contador NA</name>
|
||||
<name lang="fi">Normaalisti avoin laskuri</name>
|
||||
<name lang="fr">Actionné par comptage NO</name>
|
||||
<name lang="hr">Brojač normalno otvoren</name>
|
||||
<name lang="hu">Alaphelyzetben nyitott számláló</name>
|
||||
<name lang="it">Contatto azionato da un contatore (NO)</name>
|
||||
<name lang="ja">常開カウンター</name>
|
||||
<name lang="ko">평상시 열림 카운터</name>
|
||||
<name lang="mn">Ихэвчлэн нээлттэй тоолуур</name>
|
||||
<name lang="nb">Normalt åpen teller</name>
|
||||
<name lang="nl">Teller contact (NO)</name>
|
||||
<name lang="nl_BE">Normaal open teller</name>
|
||||
<name lang="pl">Zestyk zwierny licznika</name>
|
||||
<name lang="pt">Contador normalmente aberto</name>
|
||||
<name lang="pt_BR">Contato NA acionado por contador</name>
|
||||
<name lang="ro">Contor normal deschis</name>
|
||||
<name lang="ru">Счётчик с нормально открытым контактом</name>
|
||||
<name lang="sk">Počítadlo s kontaktom NO</name>
|
||||
<name lang="sl">Števec normalno odprt</name>
|
||||
<name lang="sr">Бројач нормално отворен</name>
|
||||
<name lang="sv">Räknare med slutande kontakt</name>
|
||||
<name lang="tr">Normalde açık sayaç</name>
|
||||
<name lang="uk">Лічильник нормально розімкнений</name>
|
||||
<name lang="zh">计数驱动常开触点</name>
|
||||
</names>
|
||||
<informations>Author: The QElectroTech team
|
||||
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
|
||||
<description>
|
||||
<rect antialias="false" height="10" rx="0" ry="0" style="line-style:normal;line-weight:thin;filling:none;color:black" width="10" x="-20" y="-15"/>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:dashed;line-weight:thin;filling:none;color:black" x1="-10" x2="-3" y1="-10" y2="-10"/>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="UserText" text_width="-1" uuid="{a3321491-1dac-42e2-a9d1-1a43604116a9}" x="-13" y="-2" z="3">
|
||||
<text>14</text>
|
||||
</dynamic_text>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="UserText" text_width="-1" uuid="{a82a123f-f3cf-4dd7-93c0-44bbdd940429}" x="-13" y="-34" z="4">
|
||||
<text>11</text>
|
||||
</dynamic_text>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="ElementInfo" text_width="-1" uuid="{960d8065-c520-4d45-aa36-4bb600c0e6db}" x="1" y="-22" z="5">
|
||||
<text/>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_text>
|
||||
<polygon antialias="true" closed="false" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="-5" x2="0" x3="0" y1="-20" y2="0" y3="10"/>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="-30" y2="-20"/>
|
||||
<ellipse antialias="true" height="4" style="line-style:normal;line-weight:thin;filling:none;color:black" width="4" x="-17" y="-12"/>
|
||||
<terminal name="" orientation="s" type="Generic" uuid="{572018e1-9843-49dd-964f-836ff27b8240}" x="0" y="10"/>
|
||||
<terminal name="" orientation="n" type="Generic" uuid="{77224246-2278-498a-88e5-1ecd84e938e8}" x="0" y="-30"/>
|
||||
</description>
|
||||
</definition>
|
||||
</element>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</collection>
|
||||
</project>
|
||||
@@ -0,0 +1,351 @@
|
||||
<project title="Legacy sequential attributes - i1280" uuid="{abc27554-b301-5651-8055-41e0ce4599cb}" version="0.200.1">
|
||||
<properties>
|
||||
<property name="saveddate" show="1">10/4/26</property>
|
||||
<property name="saveddate-eu" show="1">04-10-2026</property>
|
||||
<property name="saveddate-us" show="1">2026-10-04</property>
|
||||
<property name="savedfilename" show="1">skeleton</property>
|
||||
<property name="savedtime" show="1">10:28</property>
|
||||
</properties>
|
||||
<usage enabled="true" time_spent="0"/>
|
||||
<newdiagrams>
|
||||
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<report label="%f-%l%c"/>
|
||||
<xrefs>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||
</xrefs>
|
||||
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||
<folio_autonums/>
|
||||
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||
<guides/>
|
||||
</newdiagrams>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{a94486ed-a490-4ae0-9698-b075035de54d}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<elements>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" seqhf_1="5" uuid="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" x="100" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="0" orientation="0" x="0" y="-26"/>
|
||||
<terminal id="1" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{1b01d6c6-86f7-44e4-8e76-eba4b3a53dea}" x="-13" y="-2">
|
||||
<text>14</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{65cedcc2-1a82-4168-858c-8751bc1561d9}" x="-13" y="-34">
|
||||
<text>11</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{91920aeb-94ef-423e-a282-206489f04e5e}" x="1" y="-22">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" sequ_1="3" uuid="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" x="200" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="2" orientation="0" x="0" y="-26"/>
|
||||
<terminal id="3" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{827f6a67-cf32-448a-b33a-e52fbbb9cee7}" x="-13" y="-2">
|
||||
<text>14</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{8fe1bb6c-bd54-429f-b8b9-2b1ca789ed2c}" x="-13" y="-34">
|
||||
<text>11</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{15fa6b73-1702-4e17-b821-3bd12f81e7ba}" x="1" y="-22">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{b6bceef1-5f5c-4119-bc0c-4ea629eb0baa}" x="300" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="4" orientation="0" x="0" y="-26"/>
|
||||
<terminal id="5" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{93d83639-c2c1-4af5-9048-c0a5e0f241b8}" x="-13" y="-2">
|
||||
<text>14</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{c14602f0-24e8-463c-a183-3dac7f78e009}" x="-13" y="-34">
|
||||
<text>11</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{662e7f9d-9f6d-40b4-b517-7b584f6b3604}" x="1" y="-22">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{5d8752c5-1f02-4ef1-a24f-c7844ede38c2}" x="400" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="6" orientation="0" x="0" y="-26"/>
|
||||
<terminal id="7" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{3c604f1b-1498-4b2b-9f2d-0063e2bcb78c}" x="-13" y="-2">
|
||||
<text>14</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{dbb98cf7-9d66-4d99-91d9-2149077d6ba2}" x="-13" y="-34">
|
||||
<text>11</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{a8efc4c9-bfb3-4bde-a501-6b1bd5f93ae5}" x="1" y="-22">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
</elements>
|
||||
<conductors>
|
||||
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" element1_label="" element1_name="Normally open counter" element2="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" seqhf_1="7" uuid="{f5162c59-0c59-4e94-b6a6-4cf259a52d91}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
|
||||
</conductor>
|
||||
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" element1_label="" element1_name="Normally open counter" element2="{5d8752c5-1f02-4ef1-a24f-c7844ede38c2}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" sequ_1="4" uuid="{89cacce1-b12a-4ec3-aadf-d3f8feb7494d}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
|
||||
</conductor>
|
||||
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" element1_label="" element1_name="Normally open counter" element2="{b6bceef1-5f5c-4119-bc0c-4ea629eb0baa}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{944a0069-bad6-4d92-91ad-7aedf3961c8a}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
|
||||
<sequentialNumbers/>
|
||||
</conductor>
|
||||
</conductors>
|
||||
</diagram>
|
||||
<collection>
|
||||
<category name="import">
|
||||
<names>
|
||||
<name lang="ca">Elements importats</name>
|
||||
<name lang="cs">Zavedené prvky</name>
|
||||
<name lang="da">Importerede elementer</name>
|
||||
<name lang="de">Importierte Elemente</name>
|
||||
<name lang="el">Εισηγμένα στοιχεία</name>
|
||||
<name lang="en">Imported elements</name>
|
||||
<name lang="es">Elementos importados</name>
|
||||
<name lang="fr">Éléments importés</name>
|
||||
<name lang="hr">Uvezeni elementi</name>
|
||||
<name lang="it">Elementi importati</name>
|
||||
<name lang="ja">インバートされた要素</name>
|
||||
<name lang="ko">전기 심볼 요소</name>
|
||||
<name lang="ko_KR">전기 심볼 요소</name>
|
||||
<name lang="nl">Elementen geïmporteerd</name>
|
||||
<name lang="nl_BE">Elementen geïmporteerd</name>
|
||||
<name lang="pl">Elementy importowane</name>
|
||||
<name lang="pt">Elementos importados</name>
|
||||
<name lang="pt_BR">Elementos importados</name>
|
||||
<name lang="ro">Elemente importate</name>
|
||||
<name lang="ru">Импортированные элементы</name>
|
||||
<name lang="sl">Uvoženi elementi</name>
|
||||
<name lang="sv">Importerade element</name>
|
||||
<name lang="tr">İthal öğeler</name>
|
||||
<name lang="uk">Імпортовані елементи</name>
|
||||
<name lang="zh">导入元件</name>
|
||||
</names>
|
||||
<category name="10_electric">
|
||||
<names>
|
||||
<name lang="ar">كهربائي</name>
|
||||
<name lang="ca">Elèctric</name>
|
||||
<name lang="cs">Elektrotechnika</name>
|
||||
<name lang="da">Elektrisk</name>
|
||||
<name lang="de">Elektrik</name>
|
||||
<name lang="el">Ηλεκτρικά</name>
|
||||
<name lang="en">Electric</name>
|
||||
<name lang="es">Eléctrica</name>
|
||||
<name lang="fi">Sähkö</name>
|
||||
<name lang="fr">Electrique</name>
|
||||
<name lang="hr">Električno</name>
|
||||
<name lang="hu">Elektromos</name>
|
||||
<name lang="it">Elettrica</name>
|
||||
<name lang="ja">電気</name>
|
||||
<name lang="ko">전기</name>
|
||||
<name lang="mn">Цахилгаан</name>
|
||||
<name lang="nb">Elektrisk</name>
|
||||
<name lang="nl">Elektrotechniek</name>
|
||||
<name lang="nl_BE">Elektrisch</name>
|
||||
<name lang="pl">Elektrotechnika</name>
|
||||
<name lang="pt">Elétrico</name>
|
||||
<name lang="pt_BR">Elétrica</name>
|
||||
<name lang="ro">Electric</name>
|
||||
<name lang="ru">Электротехника</name>
|
||||
<name lang="sk">Elektrické</name>
|
||||
<name lang="sl">Elektrika</name>
|
||||
<name lang="sr">Електрика</name>
|
||||
<name lang="sv">El</name>
|
||||
<name lang="tr">Elektrik</name>
|
||||
<name lang="uk">Електрика</name>
|
||||
<name lang="zh">10_电气</name>
|
||||
<name lang="zn">电气工程</name>
|
||||
</names>
|
||||
<category name="10_allpole">
|
||||
<names>
|
||||
<name lang="ar">متعدد الأقطاب</name>
|
||||
<name lang="ca">Multifilar</name>
|
||||
<name lang="cs">Vícežilový</name>
|
||||
<name lang="da">Flere ledere</name>
|
||||
<name lang="de">Allpolig</name>
|
||||
<name lang="el">Πολυγραμμικό</name>
|
||||
<name lang="en">All-pole</name>
|
||||
<name lang="es">Multifilar</name>
|
||||
<name lang="fi">Yleisnapa</name>
|
||||
<name lang="fr">Multifilaire</name>
|
||||
<name lang="hr">Univerzalni</name>
|
||||
<name lang="hu">Többvonalas szimbólumok</name>
|
||||
<name lang="it">Multifilare</name>
|
||||
<name lang="ja">複線</name>
|
||||
<name lang="ko">다-극</name>
|
||||
<name lang="mn">Бүх туйл</name>
|
||||
<name lang="nb">Alle poler</name>
|
||||
<name lang="nl">Veel polig</name>
|
||||
<name lang="nl_BE">All-pole</name>
|
||||
<name lang="pl">Schematy wieloliniowe</name>
|
||||
<name lang="pt">Multipolar</name>
|
||||
<name lang="pt_BR">Multifilar</name>
|
||||
<name lang="ro">Bipolar</name>
|
||||
<name lang="ru">Многополюсные</name>
|
||||
<name lang="sk">Všepólové</name>
|
||||
<name lang="sl">Vsi poli</name>
|
||||
<name lang="sr">Свe-полни</name>
|
||||
<name lang="sv">Allpolig</name>
|
||||
<name lang="tr">Çok kutuplu</name>
|
||||
<name lang="uk">Багатополюсні</name>
|
||||
<name lang="zh">10_全极</name>
|
||||
</names>
|
||||
<category name="310_relays_contactors_contacts">
|
||||
<names>
|
||||
<name lang="ar">المرحّلات والموصّلات وجهات الاتصال</name>
|
||||
<name lang="ca">Relés, contactors i contactes</name>
|
||||
<name lang="cs">Relé, stykače a kontakty</name>
|
||||
<name lang="da">Relæer, kontaktorer og kontakter</name>
|
||||
<name lang="de">Relais, Schütze und Kontakte</name>
|
||||
<name lang="el">Ηλεκτρονόμοι και επαφές</name>
|
||||
<name lang="en">Relays, contactors and contacts</name>
|
||||
<name lang="es">Relevadores, contactores y contactos</name>
|
||||
<name lang="fi">Releet, kontaktorit ja koskettimet</name>
|
||||
<name lang="fr">Relais, contacteurs et contacts</name>
|
||||
<name lang="hr">Releji, kontaktori i kontakti</name>
|
||||
<name lang="hu">Relék, kontaktorok és érintkezők</name>
|
||||
<name lang="it">Relè, contattori e contatti</name>
|
||||
<name lang="ja">継電器、接触器、接点</name>
|
||||
<name lang="ko">릴레이, 커넥터및 접점</name>
|
||||
<name lang="mn">Реле, контактор болон контакт</name>
|
||||
<name lang="nb">Reléer, kontaktorere og kontakter</name>
|
||||
<name lang="nl">Relais, contactoren en contacten</name>
|
||||
<name lang="nl_BE">Relais, contacteurs en contacten</name>
|
||||
<name lang="pl">Przekaźniki i styczniki</name>
|
||||
<name lang="pt">Relés, contatores e contactos</name>
|
||||
<name lang="pt_BR">Relés, Contatores e contatos</name>
|
||||
<name lang="ro">Relee, contactoare și contacte</name>
|
||||
<name lang="ru">Реле, пускатели и контакты</name>
|
||||
<name lang="sk">Relé, stykače a kontakty</name>
|
||||
<name lang="sl">Releji, kontaktorji in kontakti</name>
|
||||
<name lang="sr">Релеји, контактори и контакти</name>
|
||||
<name lang="sv">Reläer, kontaktorer och kontakter</name>
|
||||
<name lang="tr">Röleler, kontaktörler ve kontaklar</name>
|
||||
<name lang="uk">Реле, контактори та контакти</name>
|
||||
<name lang="zh">继电器、接触器与触点</name>
|
||||
</names>
|
||||
<category name="03_contacts">
|
||||
<names>
|
||||
<name lang="ar">جهات الاتصال</name>
|
||||
<name lang="ca">Contactes</name>
|
||||
<name lang="cs">Kontakty</name>
|
||||
<name lang="da">Kontaktpersoner</name>
|
||||
<name lang="de">Kontakte</name>
|
||||
<name lang="el">Επαφές</name>
|
||||
<name lang="en">Contacts</name>
|
||||
<name lang="es">Contactos</name>
|
||||
<name lang="fi">Kosketimet</name>
|
||||
<name lang="fr">Contacts</name>
|
||||
<name lang="hr">Kontakti</name>
|
||||
<name lang="hu">Kapcsolatok</name>
|
||||
<name lang="it">Contatti</name>
|
||||
<name lang="ja">連絡先</name>
|
||||
<name lang="ko">접점</name>
|
||||
<name lang="mn">Харилцагчид</name>
|
||||
<name lang="nb">Kontakter</name>
|
||||
<name lang="nl">Contacten</name>
|
||||
<name lang="nl_BE">Contacten</name>
|
||||
<name lang="pl">Łączność</name>
|
||||
<name lang="pt">Contatos</name>
|
||||
<name lang="pt_BR">Contatos</name>
|
||||
<name lang="ro">Contacte</name>
|
||||
<name lang="ru">Контакты</name>
|
||||
<name lang="sk">Kontakty</name>
|
||||
<name lang="sl">Stiki</name>
|
||||
<name lang="sr">Контакти</name>
|
||||
<name lang="sv">Kontakter</name>
|
||||
<name lang="tr">Kişiler</name>
|
||||
<name lang="uk">Контакти</name>
|
||||
<name lang="zh">触点</name>
|
||||
</names>
|
||||
<element name="act_comptage_no.elmt">
|
||||
<definition height="60" hotspot_x="22" hotspot_y="41" link_type="simple" type="element" version="0.100.0" width="30">
|
||||
<uuid uuid="{19fdfdc7-c1fe-4604-cb14-da967d1a5252}"/>
|
||||
<names>
|
||||
<name lang="ar">عدّاد مفتوح عادة</name>
|
||||
<name lang="ca">Taulell normalment obert</name>
|
||||
<name lang="cs">Kontakt čítače (NO)</name>
|
||||
<name lang="da">Normalt åben tæller</name>
|
||||
<name lang="de">Öffner Zähler betätigt</name>
|
||||
<name lang="el">Ανοικτή επαφή μετρητή</name>
|
||||
<name lang="en">Normally open counter</name>
|
||||
<name lang="es">Contador NA</name>
|
||||
<name lang="fi">Normaalisti avoin laskuri</name>
|
||||
<name lang="fr">Actionné par comptage NO</name>
|
||||
<name lang="hr">Brojač normalno otvoren</name>
|
||||
<name lang="hu">Alaphelyzetben nyitott számláló</name>
|
||||
<name lang="it">Contatto azionato da un contatore (NO)</name>
|
||||
<name lang="ja">常開カウンター</name>
|
||||
<name lang="ko">평상시 열림 카운터</name>
|
||||
<name lang="mn">Ихэвчлэн нээлттэй тоолуур</name>
|
||||
<name lang="nb">Normalt åpen teller</name>
|
||||
<name lang="nl">Teller contact (NO)</name>
|
||||
<name lang="nl_BE">Normaal open teller</name>
|
||||
<name lang="pl">Zestyk zwierny licznika</name>
|
||||
<name lang="pt">Contador normalmente aberto</name>
|
||||
<name lang="pt_BR">Contato NA acionado por contador</name>
|
||||
<name lang="ro">Contor normal deschis</name>
|
||||
<name lang="ru">Счётчик с нормально открытым контактом</name>
|
||||
<name lang="sk">Počítadlo s kontaktom NO</name>
|
||||
<name lang="sl">Števec normalno odprt</name>
|
||||
<name lang="sr">Бројач нормално отворен</name>
|
||||
<name lang="sv">Räknare med slutande kontakt</name>
|
||||
<name lang="tr">Normalde açık sayaç</name>
|
||||
<name lang="uk">Лічильник нормально розімкнений</name>
|
||||
<name lang="zh">计数驱动常开触点</name>
|
||||
</names>
|
||||
<informations>Author: The QElectroTech team
|
||||
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
|
||||
<description>
|
||||
<rect antialias="false" height="10" rx="0" ry="0" style="line-style:normal;line-weight:thin;filling:none;color:black" width="10" x="-20" y="-15"/>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:dashed;line-weight:thin;filling:none;color:black" x1="-10" x2="-3" y1="-10" y2="-10"/>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="UserText" text_width="-1" uuid="{a3321491-1dac-42e2-a9d1-1a43604116a9}" x="-13" y="-2" z="3">
|
||||
<text>14</text>
|
||||
</dynamic_text>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="UserText" text_width="-1" uuid="{a82a123f-f3cf-4dd7-93c0-44bbdd940429}" x="-13" y="-34" z="4">
|
||||
<text>11</text>
|
||||
</dynamic_text>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="ElementInfo" text_width="-1" uuid="{960d8065-c520-4d45-aa36-4bb600c0e6db}" x="1" y="-22" z="5">
|
||||
<text/>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_text>
|
||||
<polygon antialias="true" closed="false" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="-5" x2="0" x3="0" y1="-20" y2="0" y3="10"/>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="-30" y2="-20"/>
|
||||
<ellipse antialias="true" height="4" style="line-style:normal;line-weight:thin;filling:none;color:black" width="4" x="-17" y="-12"/>
|
||||
<terminal name="" orientation="s" type="Generic" uuid="{572018e1-9843-49dd-964f-836ff27b8240}" x="0" y="10"/>
|
||||
<terminal name="" orientation="n" type="Generic" uuid="{77224246-2278-498a-88e5-1ecd84e938e8}" x="0" y="-30"/>
|
||||
</description>
|
||||
</definition>
|
||||
</element>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</collection>
|
||||
</project>
|
||||
@@ -0,0 +1,320 @@
|
||||
<project title="one text per potential, tied lengths" uuid="{74544bb0-3f71-58af-9052-eb46f8a03dfd}" version="0.200.1">
|
||||
<properties>
|
||||
<property name="saveddate" show="1">10/2/26</property>
|
||||
<property name="saveddate-eu" show="1">02-10-2026</property>
|
||||
<property name="saveddate-us" show="1">2026-10-02</property>
|
||||
<property name="savedfilename" show="1">one_text_per_potential_tie</property>
|
||||
<property name="savedfilepath" show="1">one_text_per_potential_tie.qet</property>
|
||||
<property name="savedtime" show="1">22:07</property>
|
||||
</properties>
|
||||
<usage enabled="true" time_spent="0"/>
|
||||
<newdiagrams>
|
||||
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<report label="%f-%l%c"/>
|
||||
<xrefs>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||
</xrefs>
|
||||
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||
<folio_autonums/>
|
||||
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||
<guides/>
|
||||
</newdiagrams>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{91957ff3-7b74-4012-a876-f858c68b2d60}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="1" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<elements>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="R" type="embed://import/10_electric/10_allpole/395_electronics_semiconductors/01_resistors/resistance.elmt" uuid="{3817b0a1-cab7-454e-81f2-7678b242cced}" x="100" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="0" orientation="0" x="0" y="-16"/>
|
||||
<terminal id="1" orientation="2" x="0" y="16"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{1a7d317c-51b7-402f-807e-79aa136df233}" x="6" y="-11.5">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="R" type="embed://import/10_electric/10_allpole/395_electronics_semiconductors/01_resistors/resistance.elmt" uuid="{f571bdba-9562-4119-8a18-0d798ebb7c3d}" x="300" y="100" z="10">
|
||||
<terminals>
|
||||
<terminal id="2" orientation="0" x="0" y="-16"/>
|
||||
<terminal id="3" orientation="2" x="0" y="16"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{79099d69-dcec-4247-bd03-cdb07a0194d6}" x="6" y="-11.5">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="R" type="embed://import/10_electric/10_allpole/395_electronics_semiconductors/01_resistors/resistance.elmt" uuid="{c05fe3f1-f4a1-4beb-843b-90aadc3ca6c0}" x="300" y="500" z="10">
|
||||
<terminals>
|
||||
<terminal id="4" orientation="0" x="0" y="-16"/>
|
||||
<terminal id="5" orientation="2" x="0" y="16"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{075df885-82c2-49dd-817d-809137a3329d}" x="6" y="-11.5">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="R" type="embed://import/10_electric/10_allpole/395_electronics_semiconductors/01_resistors/resistance.elmt" uuid="{d0acb0f6-8c2f-4daa-8365-7ad1df6f7f86}" x="400" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="6" orientation="0" x="0" y="-16"/>
|
||||
<terminal id="7" orientation="2" x="0" y="16"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{11eafa99-e5a9-4baf-a2ed-8469780c7fa8}" x="6" y="-11.5">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
</elements>
|
||||
<conductors>
|
||||
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{3817b0a1-cab7-454e-81f2-7678b242cced}" element1_label="" element1_name="Resistor" element2="{f571bdba-9562-4119-8a18-0d798ebb7c3d}" element2_label="" element2_name="Resistor" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="42" numsize="7" onetextperfolio="1" tension_protocol="" terminal1="{09a3060b-a0fd-41ad-b605-f5c0ae63901e}" terminal2="{4410c57c-3796-4696-b63b-d8d5a0c9b788}" terminalname1="A2" terminalname2="A1" text_color="#000000" type="multi" uuid="{26591715-c028-44c9-bf2d-c828f8187527}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
|
||||
<sequentialNumbers/>
|
||||
</conductor>
|
||||
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{f571bdba-9562-4119-8a18-0d798ebb7c3d}" element1_label="" element1_name="Resistor" element2="{c05fe3f1-f4a1-4beb-843b-90aadc3ca6c0}" element2_label="" element2_name="Resistor" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="42" numsize="7" onetextperfolio="1" tension_protocol="" terminal1="{4410c57c-3796-4696-b63b-d8d5a0c9b788}" terminal2="{09a3060b-a0fd-41ad-b605-f5c0ae63901e}" terminalname1="A1" terminalname2="A2" text_color="#000000" type="multi" uuid="{0c9e2a9a-f402-49f4-9780-a8c72704acff}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
|
||||
<sequentialNumbers/>
|
||||
</conductor>
|
||||
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{f571bdba-9562-4119-8a18-0d798ebb7c3d}" element1_label="" element1_name="Resistor" element2="{d0acb0f6-8c2f-4daa-8365-7ad1df6f7f86}" element2_label="" element2_name="Resistor" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="42" numsize="7" onetextperfolio="1" tension_protocol="" terminal1="{4410c57c-3796-4696-b63b-d8d5a0c9b788}" terminal2="{09a3060b-a0fd-41ad-b605-f5c0ae63901e}" terminalname1="A1" terminalname2="A2" text_color="#000000" type="multi" uuid="{5ecce027-3b6b-4e2e-9d30-f5dd59b320c0}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
|
||||
<sequentialNumbers/>
|
||||
</conductor>
|
||||
</conductors>
|
||||
</diagram>
|
||||
<collection>
|
||||
<category name="import">
|
||||
<names>
|
||||
<name lang="ca">Elements importats</name>
|
||||
<name lang="cs">Zavedené prvky</name>
|
||||
<name lang="da">Importerede elementer</name>
|
||||
<name lang="de">Importierte Elemente</name>
|
||||
<name lang="el">Εισηγμένα στοιχεία</name>
|
||||
<name lang="en">Imported elements</name>
|
||||
<name lang="es">Elementos importados</name>
|
||||
<name lang="fr">Éléments importés</name>
|
||||
<name lang="hr">Uvezeni elementi</name>
|
||||
<name lang="it">Elementi importati</name>
|
||||
<name lang="ja">インバートされた要素</name>
|
||||
<name lang="ko">전기 심볼 요소</name>
|
||||
<name lang="ko_KR">전기 심볼 요소</name>
|
||||
<name lang="nl">Elementen geïmporteerd</name>
|
||||
<name lang="nl_BE">Elementen geïmporteerd</name>
|
||||
<name lang="pl">Elementy importowane</name>
|
||||
<name lang="pt">Elementos importados</name>
|
||||
<name lang="pt_BR">Elementos importados</name>
|
||||
<name lang="ro">Elemente importate</name>
|
||||
<name lang="ru">Импортированные элементы</name>
|
||||
<name lang="sl">Uvoženi elementi</name>
|
||||
<name lang="sv">Importerade element</name>
|
||||
<name lang="tr">İthal öğeler</name>
|
||||
<name lang="uk">Імпортовані елементи</name>
|
||||
<name lang="zh">导入元件</name>
|
||||
</names>
|
||||
<category name="10_electric">
|
||||
<names>
|
||||
<name lang="ar">كهربائي</name>
|
||||
<name lang="ca">Elèctric</name>
|
||||
<name lang="cs">Elektrotechnika</name>
|
||||
<name lang="da">Elektrisk</name>
|
||||
<name lang="de">Elektrik</name>
|
||||
<name lang="el">Ηλεκτρικά</name>
|
||||
<name lang="en">Electric</name>
|
||||
<name lang="es">Eléctrica</name>
|
||||
<name lang="fi">Sähkö</name>
|
||||
<name lang="fr">Electrique</name>
|
||||
<name lang="hr">Električno</name>
|
||||
<name lang="hu">Elektromos</name>
|
||||
<name lang="it">Elettrica</name>
|
||||
<name lang="ja">電気</name>
|
||||
<name lang="ko">전기</name>
|
||||
<name lang="mn">Цахилгаан</name>
|
||||
<name lang="nb">Elektrisk</name>
|
||||
<name lang="nl">Elektrotechniek</name>
|
||||
<name lang="nl_BE">Elektrisch</name>
|
||||
<name lang="pl">Elektrotechnika</name>
|
||||
<name lang="pt">Elétrico</name>
|
||||
<name lang="pt_BR">Elétrica</name>
|
||||
<name lang="ro">Electric</name>
|
||||
<name lang="ru">Электротехника</name>
|
||||
<name lang="sk">Elektrické</name>
|
||||
<name lang="sl">Elektrika</name>
|
||||
<name lang="sr">Електрика</name>
|
||||
<name lang="sv">El</name>
|
||||
<name lang="tr">Elektrik</name>
|
||||
<name lang="uk">Електрика</name>
|
||||
<name lang="zh">10_电气</name>
|
||||
<name lang="zn">电气工程</name>
|
||||
</names>
|
||||
<category name="10_allpole">
|
||||
<names>
|
||||
<name lang="ar">متعدد الأقطاب</name>
|
||||
<name lang="ca">Multifilar</name>
|
||||
<name lang="cs">Vícežilový</name>
|
||||
<name lang="da">Flere ledere</name>
|
||||
<name lang="de">Allpolig</name>
|
||||
<name lang="el">Πολυγραμμικό</name>
|
||||
<name lang="en">All-pole</name>
|
||||
<name lang="es">Multifilar</name>
|
||||
<name lang="fi">Yleisnapa</name>
|
||||
<name lang="fr">Multifilaire</name>
|
||||
<name lang="hr">Univerzalni</name>
|
||||
<name lang="hu">Többvonalas szimbólumok</name>
|
||||
<name lang="it">Multifilare</name>
|
||||
<name lang="ja">複線</name>
|
||||
<name lang="ko">다-극</name>
|
||||
<name lang="mn">Бүх туйл</name>
|
||||
<name lang="nb">Alle poler</name>
|
||||
<name lang="nl">Veel polig</name>
|
||||
<name lang="nl_BE">All-pole</name>
|
||||
<name lang="pl">Schematy wieloliniowe</name>
|
||||
<name lang="pt">Multipolar</name>
|
||||
<name lang="pt_BR">Multifilar</name>
|
||||
<name lang="ro">Bipolar</name>
|
||||
<name lang="ru">Многополюсные</name>
|
||||
<name lang="sk">Všepólové</name>
|
||||
<name lang="sl">Vsi poli</name>
|
||||
<name lang="sr">Свe-полни</name>
|
||||
<name lang="sv">Allpolig</name>
|
||||
<name lang="tr">Çok kutuplu</name>
|
||||
<name lang="uk">Багатополюсні</name>
|
||||
<name lang="zh">10_全极</name>
|
||||
</names>
|
||||
<category name="395_electronics_semiconductors">
|
||||
<names>
|
||||
<name lang="ar">إلكترونيات وأشباه موصلات</name>
|
||||
<name lang="ca">Electrònica i semiconductors</name>
|
||||
<name lang="cs">Elektronika a polovodiče</name>
|
||||
<name lang="da">Elektronik og halvledere</name>
|
||||
<name lang="de">Elektronik und Halbleiter</name>
|
||||
<name lang="el">Ηλεκτρονικά και ημιαγωγοί</name>
|
||||
<name lang="en">Electronics and semiconductors</name>
|
||||
<name lang="es">Electrónica y semiconductores</name>
|
||||
<name lang="fi">Elektroniikka ja puolijohteet</name>
|
||||
<name lang="fr">Electronique et semi-conducteurs</name>
|
||||
<name lang="hr">Elektronika i poluvodiči</name>
|
||||
<name lang="hu">Elektronika és félvezetők</name>
|
||||
<name lang="it">Elettronica e semiconduttori</name>
|
||||
<name lang="ja">電子と半導体</name>
|
||||
<name lang="ko">전자 공학 및 반도체</name>
|
||||
<name lang="mn">Электроник ба хагас дамжуулагч</name>
|
||||
<name lang="nb">Elektronikk og halvledere</name>
|
||||
<name lang="nl">Electronica en halfgeleiders</name>
|
||||
<name lang="nl_BE">Elektronica en halfgeleiders</name>
|
||||
<name lang="pl">Elektronika i półprzewodniki</name>
|
||||
<name lang="pt">Eletrónica e semicondutores</name>
|
||||
<name lang="pt_BR">Eletrônica e semi-condutoues</name>
|
||||
<name lang="ro">Electronică și semiconductoare</name>
|
||||
<name lang="ru">Электроника и полупроводники</name>
|
||||
<name lang="sk">Elektronika a polovodiče</name>
|
||||
<name lang="sl">Elektronika in polprevodniki</name>
|
||||
<name lang="sr">Електроника и полупроводници</name>
|
||||
<name lang="sv">Elektronik och halvledare</name>
|
||||
<name lang="tr">Elektronik ve yarı iletkenler</name>
|
||||
<name lang="uk">Електроніка та напівпровідники</name>
|
||||
<name lang="zh">电子与半导体</name>
|
||||
</names>
|
||||
<category name="01_resistors">
|
||||
<names>
|
||||
<name lang="ar">مُقاومة</name>
|
||||
<name lang="ca">Resistències</name>
|
||||
<name lang="cs">Odpory</name>
|
||||
<name lang="da">Modstande</name>
|
||||
<name lang="de">Widerstände</name>
|
||||
<name lang="el">Αντιστάσεις</name>
|
||||
<name lang="en">Resistors</name>
|
||||
<name lang="es">Resistencias</name>
|
||||
<name lang="fi">Vastukset</name>
|
||||
<name lang="fr">Résistances</name>
|
||||
<name lang="hr">Otpornici</name>
|
||||
<name lang="hu">Ellenállások</name>
|
||||
<name lang="it">Resistori</name>
|
||||
<name lang="ja">抵抗器</name>
|
||||
<name lang="ko">저항</name>
|
||||
<name lang="mn">Эсэргүүцэл</name>
|
||||
<name lang="nb">Motstander</name>
|
||||
<name lang="nl">Weerstand</name>
|
||||
<name lang="nl_BE">Weerstanden</name>
|
||||
<name lang="pl">Rezystory</name>
|
||||
<name lang="pt">Resistência</name>
|
||||
<name lang="pt_BR">Resistores</name>
|
||||
<name lang="ro">Rezistoare</name>
|
||||
<name lang="ru">Резисторы</name>
|
||||
<name lang="sk">Rezistory</name>
|
||||
<name lang="sl">Upori</name>
|
||||
<name lang="sr">Отпорници</name>
|
||||
<name lang="sv">Motstånd</name>
|
||||
<name lang="tr">Dirençler</name>
|
||||
<name lang="uk">Резистори</name>
|
||||
<name lang="zh">电阻器</name>
|
||||
</names>
|
||||
<element name="resistance.elmt">
|
||||
<definition height="60" hotspot_x="10" hotspot_y="30" link_type="simple" type="element" version="0.100.0" width="20">
|
||||
<uuid uuid="{2becfcef-f6f6-6a63-ef49-65f12026c4f8}"/>
|
||||
<names>
|
||||
<name lang="ar">مُقاومة</name>
|
||||
<name lang="ca">Resistència</name>
|
||||
<name lang="cs">Odpor</name>
|
||||
<name lang="da">Modstand</name>
|
||||
<name lang="de">Widerstand</name>
|
||||
<name lang="el">Αντίσταση</name>
|
||||
<name lang="en">Resistor</name>
|
||||
<name lang="es">Resistencia</name>
|
||||
<name lang="fi">Vastus</name>
|
||||
<name lang="fr">Résistance</name>
|
||||
<name lang="hr">Otpornik</name>
|
||||
<name lang="hu">Ellenállás</name>
|
||||
<name lang="it">Resistore</name>
|
||||
<name lang="ja">抵抗器</name>
|
||||
<name lang="ko">저항</name>
|
||||
<name lang="mn">Резистор</name>
|
||||
<name lang="nb">Motstand</name>
|
||||
<name lang="nl">Weerstand</name>
|
||||
<name lang="nl_BE">Weerstand</name>
|
||||
<name lang="pl">Rezystor</name>
|
||||
<name lang="pt">Resistência</name>
|
||||
<name lang="pt_BR">Resistor</name>
|
||||
<name lang="ro">Rezistor</name>
|
||||
<name lang="ru">Резистор</name>
|
||||
<name lang="sk">Rezistor</name>
|
||||
<name lang="sl">Upor</name>
|
||||
<name lang="sr">Отпорник</name>
|
||||
<name lang="sv">Resistor</name>
|
||||
<name lang="tr">Direnç</name>
|
||||
<name lang="uk">Резистор</name>
|
||||
<name lang="zh">电阻</name>
|
||||
</names>
|
||||
<informations>EN 60617: 04-01-01</informations>
|
||||
<description>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="20" y2="-20"/>
|
||||
<rect antialias="false" height="20" rx="0" ry="0" style="line-style:normal;line-weight:normal;filling:white;color:black" width="8" x="-4" y="-10"/>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="ElementInfo" text_width="-1" uuid="{570cd854-578d-4c4b-bd2a-3498353e46b6}" x="6" y="-11.5" z="7">
|
||||
<text/>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_text>
|
||||
<terminal name="A2" orientation="s" type="Generic" uuid="{09a3060b-a0fd-41ad-b605-f5c0ae63901e}" x="0" y="20"/>
|
||||
<terminal name="A1" orientation="n" type="Generic" uuid="{4410c57c-3796-4696-b63b-d8d5a0c9b788}" x="0" y="-20"/>
|
||||
</description>
|
||||
</definition>
|
||||
</element>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</collection>
|
||||
</project>
|
||||
@@ -0,0 +1,63 @@
|
||||
<project title="pdf page size" uuid="{63126da6-a8f2-5840-8149-a23a51406a32}" version="0.200.1">
|
||||
<properties>
|
||||
<property name="saveddate" show="1">10/2/26</property>
|
||||
<property name="saveddate-eu" show="1">02-10-2026</property>
|
||||
<property name="saveddate-us" show="1">2026-10-02</property>
|
||||
<property name="savedfilename" show="1">pdf_page_a3</property>
|
||||
<property name="savedtime" show="1">22:39</property>
|
||||
</properties>
|
||||
<usage enabled="true" time_spent="0"/>
|
||||
<newdiagrams>
|
||||
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<report label="%f-%l%c"/>
|
||||
<xrefs>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||
</xrefs>
|
||||
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||
<folio_autonums/>
|
||||
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||
<guides/>
|
||||
</newdiagrams>
|
||||
<diagram author="" auto_page_num="" cols="27" colsize="58" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="1070" indexrev="" locmach="" order="1" plant="" rows="14" rowsize="75" title="" uuid="{270fad7d-7d9f-4198-a0e1-dceb22c883cc}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
</diagram>
|
||||
<diagram author="" auto_page_num="" cols="20" colsize="55" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="1535" indexrev="" locmach="" order="2" plant="" rows="15" rowsize="101" title="" uuid="{9cf88404-d198-4690-90f7-647fb7940f68}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
</diagram>
|
||||
<collection>
|
||||
<category name="import">
|
||||
<names>
|
||||
<name lang="ca">Elements importats</name>
|
||||
<name lang="cs">Zavedené prvky</name>
|
||||
<name lang="da">Importerede elementer</name>
|
||||
<name lang="de">Importierte Elemente</name>
|
||||
<name lang="el">Εισηγμένα στοιχεία</name>
|
||||
<name lang="en">Imported elements</name>
|
||||
<name lang="es">Elementos importados</name>
|
||||
<name lang="fr">Éléments importés</name>
|
||||
<name lang="hr">Uvezeni elementi</name>
|
||||
<name lang="it">Elementi importati</name>
|
||||
<name lang="ja">インバートされた要素</name>
|
||||
<name lang="ko">전기 심볼 요소</name>
|
||||
<name lang="ko_KR">전기 심볼 요소</name>
|
||||
<name lang="nl">Elementen geïmporteerd</name>
|
||||
<name lang="nl_BE">Elementen geïmporteerd</name>
|
||||
<name lang="pl">Elementy importowane</name>
|
||||
<name lang="pt">Elementos importados</name>
|
||||
<name lang="pt_BR">Elementos importados</name>
|
||||
<name lang="ro">Elemente importate</name>
|
||||
<name lang="ru">Импортированные элементы</name>
|
||||
<name lang="sl">Uvoženi elementi</name>
|
||||
<name lang="sv">Importerade element</name>
|
||||
<name lang="tr">İthal öğeler</name>
|
||||
<name lang="uk">Імпортовані елементи</name>
|
||||
<name lang="zh">导入元件</name>
|
||||
</names>
|
||||
</category>
|
||||
</collection>
|
||||
</project>
|
||||
@@ -0,0 +1,359 @@
|
||||
<project title="Ten sequential numbers - i1280" uuid="{abc27554-b301-5651-8055-41e0ce4599cb}" version="0.200.1">
|
||||
<properties>
|
||||
<property name="saveddate" show="1">10/4/26</property>
|
||||
<property name="saveddate-eu" show="1">04-10-2026</property>
|
||||
<property name="saveddate-us" show="1">2026-10-04</property>
|
||||
<property name="savedfilename" show="1">skeleton</property>
|
||||
<property name="savedtime" show="1">10:28</property>
|
||||
</properties>
|
||||
<usage enabled="true" time_spent="0"/>
|
||||
<newdiagrams>
|
||||
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<report label="%f-%l%c"/>
|
||||
<xrefs>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||
</xrefs>
|
||||
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||
<folio_autonums/>
|
||||
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||
<guides/>
|
||||
</newdiagrams>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{a94486ed-a490-4ae0-9698-b075035de54d}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<elements>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" x="100" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="0" orientation="0" x="0" y="-26"/>
|
||||
<terminal id="1" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<elementInformations>
|
||||
<elementInformation name="formula" show="1">%sequ_10-%sequ_1</elementInformation>
|
||||
</elementInformations>
|
||||
<sequentialNumbers>
|
||||
<unit>A;B;C;D;E;F;G;H;I;J</unit>
|
||||
</sequentialNumbers>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{1b01d6c6-86f7-44e4-8e76-eba4b3a53dea}" x="-13" y="-2">
|
||||
<text>14</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{65cedcc2-1a82-4168-858c-8751bc1561d9}" x="-13" y="-34">
|
||||
<text>11</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{91920aeb-94ef-423e-a282-206489f04e5e}" x="1" y="-22">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" x="200" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="2" orientation="0" x="0" y="-26"/>
|
||||
<terminal id="3" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{827f6a67-cf32-448a-b33a-e52fbbb9cee7}" x="-13" y="-2">
|
||||
<text>14</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{8fe1bb6c-bd54-429f-b8b9-2b1ca789ed2c}" x="-13" y="-34">
|
||||
<text>11</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{15fa6b73-1702-4e17-b821-3bd12f81e7ba}" x="1" y="-22">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{b6bceef1-5f5c-4119-bc0c-4ea629eb0baa}" x="300" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="4" orientation="0" x="0" y="-26"/>
|
||||
<terminal id="5" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{93d83639-c2c1-4af5-9048-c0a5e0f241b8}" x="-13" y="-2">
|
||||
<text>14</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{c14602f0-24e8-463c-a183-3dac7f78e009}" x="-13" y="-34">
|
||||
<text>11</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{662e7f9d-9f6d-40b4-b517-7b584f6b3604}" x="1" y="-22">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/03_contacts/act_comptage_no.elmt" uuid="{5d8752c5-1f02-4ef1-a24f-c7844ede38c2}" x="400" y="300" z="10">
|
||||
<terminals>
|
||||
<terminal id="6" orientation="0" x="0" y="-26"/>
|
||||
<terminal id="7" orientation="2" x="0" y="6"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{3c604f1b-1498-4b2b-9f2d-0063e2bcb78c}" x="-13" y="-2">
|
||||
<text>14</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="UserText" text_width="-1" uuid="{dbb98cf7-9d66-4d99-91d9-2149077d6ba2}" x="-13" y="-34">
|
||||
<text>11</text>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{a8efc4c9-bfb3-4bde-a501-6b1bd5f93ae5}" x="1" y="-22">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
</elements>
|
||||
<conductors>
|
||||
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" element1_label="" element1_name="Normally open counter" element2="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{f5162c59-0c59-4e94-b6a6-4cf259a52d91}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
|
||||
<sequentialNumbers/>
|
||||
</conductor>
|
||||
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" element1_label="" element1_name="Normally open counter" element2="{5d8752c5-1f02-4ef1-a24f-c7844ede38c2}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{89cacce1-b12a-4ec3-aadf-d3f8feb7494d}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
|
||||
<sequentialNumbers/>
|
||||
</conductor>
|
||||
<conductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" element1="{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" element1_label="" element1_name="Normally open counter" element2="{b6bceef1-5f5c-4119-bc0c-4ea629eb0baa}" element2_label="" element2_name="Normally open counter" formula="" freezeLabel="false" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" terminal1="{77224246-2278-498a-88e5-1ecd84e938e8}" terminal2="{77224246-2278-498a-88e5-1ecd84e938e8}" terminalname1="" terminalname2="" text_color="#000000" type="multi" uuid="{944a0069-bad6-4d92-91ad-7aedf3961c8a}" vertical-alignment="AlignRight" vertirotatetext="270" x="0" y="0">
|
||||
<sequentialNumbers/>
|
||||
</conductor>
|
||||
</conductors>
|
||||
</diagram>
|
||||
<collection>
|
||||
<category name="import">
|
||||
<names>
|
||||
<name lang="ca">Elements importats</name>
|
||||
<name lang="cs">Zavedené prvky</name>
|
||||
<name lang="da">Importerede elementer</name>
|
||||
<name lang="de">Importierte Elemente</name>
|
||||
<name lang="el">Εισηγμένα στοιχεία</name>
|
||||
<name lang="en">Imported elements</name>
|
||||
<name lang="es">Elementos importados</name>
|
||||
<name lang="fr">Éléments importés</name>
|
||||
<name lang="hr">Uvezeni elementi</name>
|
||||
<name lang="it">Elementi importati</name>
|
||||
<name lang="ja">インバートされた要素</name>
|
||||
<name lang="ko">전기 심볼 요소</name>
|
||||
<name lang="ko_KR">전기 심볼 요소</name>
|
||||
<name lang="nl">Elementen geïmporteerd</name>
|
||||
<name lang="nl_BE">Elementen geïmporteerd</name>
|
||||
<name lang="pl">Elementy importowane</name>
|
||||
<name lang="pt">Elementos importados</name>
|
||||
<name lang="pt_BR">Elementos importados</name>
|
||||
<name lang="ro">Elemente importate</name>
|
||||
<name lang="ru">Импортированные элементы</name>
|
||||
<name lang="sl">Uvoženi elementi</name>
|
||||
<name lang="sv">Importerade element</name>
|
||||
<name lang="tr">İthal öğeler</name>
|
||||
<name lang="uk">Імпортовані елементи</name>
|
||||
<name lang="zh">导入元件</name>
|
||||
</names>
|
||||
<category name="10_electric">
|
||||
<names>
|
||||
<name lang="ar">كهربائي</name>
|
||||
<name lang="ca">Elèctric</name>
|
||||
<name lang="cs">Elektrotechnika</name>
|
||||
<name lang="da">Elektrisk</name>
|
||||
<name lang="de">Elektrik</name>
|
||||
<name lang="el">Ηλεκτρικά</name>
|
||||
<name lang="en">Electric</name>
|
||||
<name lang="es">Eléctrica</name>
|
||||
<name lang="fi">Sähkö</name>
|
||||
<name lang="fr">Electrique</name>
|
||||
<name lang="hr">Električno</name>
|
||||
<name lang="hu">Elektromos</name>
|
||||
<name lang="it">Elettrica</name>
|
||||
<name lang="ja">電気</name>
|
||||
<name lang="ko">전기</name>
|
||||
<name lang="mn">Цахилгаан</name>
|
||||
<name lang="nb">Elektrisk</name>
|
||||
<name lang="nl">Elektrotechniek</name>
|
||||
<name lang="nl_BE">Elektrisch</name>
|
||||
<name lang="pl">Elektrotechnika</name>
|
||||
<name lang="pt">Elétrico</name>
|
||||
<name lang="pt_BR">Elétrica</name>
|
||||
<name lang="ro">Electric</name>
|
||||
<name lang="ru">Электротехника</name>
|
||||
<name lang="sk">Elektrické</name>
|
||||
<name lang="sl">Elektrika</name>
|
||||
<name lang="sr">Електрика</name>
|
||||
<name lang="sv">El</name>
|
||||
<name lang="tr">Elektrik</name>
|
||||
<name lang="uk">Електрика</name>
|
||||
<name lang="zh">10_电气</name>
|
||||
<name lang="zn">电气工程</name>
|
||||
</names>
|
||||
<category name="10_allpole">
|
||||
<names>
|
||||
<name lang="ar">متعدد الأقطاب</name>
|
||||
<name lang="ca">Multifilar</name>
|
||||
<name lang="cs">Vícežilový</name>
|
||||
<name lang="da">Flere ledere</name>
|
||||
<name lang="de">Allpolig</name>
|
||||
<name lang="el">Πολυγραμμικό</name>
|
||||
<name lang="en">All-pole</name>
|
||||
<name lang="es">Multifilar</name>
|
||||
<name lang="fi">Yleisnapa</name>
|
||||
<name lang="fr">Multifilaire</name>
|
||||
<name lang="hr">Univerzalni</name>
|
||||
<name lang="hu">Többvonalas szimbólumok</name>
|
||||
<name lang="it">Multifilare</name>
|
||||
<name lang="ja">複線</name>
|
||||
<name lang="ko">다-극</name>
|
||||
<name lang="mn">Бүх туйл</name>
|
||||
<name lang="nb">Alle poler</name>
|
||||
<name lang="nl">Veel polig</name>
|
||||
<name lang="nl_BE">All-pole</name>
|
||||
<name lang="pl">Schematy wieloliniowe</name>
|
||||
<name lang="pt">Multipolar</name>
|
||||
<name lang="pt_BR">Multifilar</name>
|
||||
<name lang="ro">Bipolar</name>
|
||||
<name lang="ru">Многополюсные</name>
|
||||
<name lang="sk">Všepólové</name>
|
||||
<name lang="sl">Vsi poli</name>
|
||||
<name lang="sr">Свe-полни</name>
|
||||
<name lang="sv">Allpolig</name>
|
||||
<name lang="tr">Çok kutuplu</name>
|
||||
<name lang="uk">Багатополюсні</name>
|
||||
<name lang="zh">10_全极</name>
|
||||
</names>
|
||||
<category name="310_relays_contactors_contacts">
|
||||
<names>
|
||||
<name lang="ar">المرحّلات والموصّلات وجهات الاتصال</name>
|
||||
<name lang="ca">Relés, contactors i contactes</name>
|
||||
<name lang="cs">Relé, stykače a kontakty</name>
|
||||
<name lang="da">Relæer, kontaktorer og kontakter</name>
|
||||
<name lang="de">Relais, Schütze und Kontakte</name>
|
||||
<name lang="el">Ηλεκτρονόμοι και επαφές</name>
|
||||
<name lang="en">Relays, contactors and contacts</name>
|
||||
<name lang="es">Relevadores, contactores y contactos</name>
|
||||
<name lang="fi">Releet, kontaktorit ja koskettimet</name>
|
||||
<name lang="fr">Relais, contacteurs et contacts</name>
|
||||
<name lang="hr">Releji, kontaktori i kontakti</name>
|
||||
<name lang="hu">Relék, kontaktorok és érintkezők</name>
|
||||
<name lang="it">Relè, contattori e contatti</name>
|
||||
<name lang="ja">継電器、接触器、接点</name>
|
||||
<name lang="ko">릴레이, 커넥터및 접점</name>
|
||||
<name lang="mn">Реле, контактор болон контакт</name>
|
||||
<name lang="nb">Reléer, kontaktorere og kontakter</name>
|
||||
<name lang="nl">Relais, contactoren en contacten</name>
|
||||
<name lang="nl_BE">Relais, contacteurs en contacten</name>
|
||||
<name lang="pl">Przekaźniki i styczniki</name>
|
||||
<name lang="pt">Relés, contatores e contactos</name>
|
||||
<name lang="pt_BR">Relés, Contatores e contatos</name>
|
||||
<name lang="ro">Relee, contactoare și contacte</name>
|
||||
<name lang="ru">Реле, пускатели и контакты</name>
|
||||
<name lang="sk">Relé, stykače a kontakty</name>
|
||||
<name lang="sl">Releji, kontaktorji in kontakti</name>
|
||||
<name lang="sr">Релеји, контактори и контакти</name>
|
||||
<name lang="sv">Reläer, kontaktorer och kontakter</name>
|
||||
<name lang="tr">Röleler, kontaktörler ve kontaklar</name>
|
||||
<name lang="uk">Реле, контактори та контакти</name>
|
||||
<name lang="zh">继电器、接触器与触点</name>
|
||||
</names>
|
||||
<category name="03_contacts">
|
||||
<names>
|
||||
<name lang="ar">جهات الاتصال</name>
|
||||
<name lang="ca">Contactes</name>
|
||||
<name lang="cs">Kontakty</name>
|
||||
<name lang="da">Kontaktpersoner</name>
|
||||
<name lang="de">Kontakte</name>
|
||||
<name lang="el">Επαφές</name>
|
||||
<name lang="en">Contacts</name>
|
||||
<name lang="es">Contactos</name>
|
||||
<name lang="fi">Kosketimet</name>
|
||||
<name lang="fr">Contacts</name>
|
||||
<name lang="hr">Kontakti</name>
|
||||
<name lang="hu">Kapcsolatok</name>
|
||||
<name lang="it">Contatti</name>
|
||||
<name lang="ja">連絡先</name>
|
||||
<name lang="ko">접점</name>
|
||||
<name lang="mn">Харилцагчид</name>
|
||||
<name lang="nb">Kontakter</name>
|
||||
<name lang="nl">Contacten</name>
|
||||
<name lang="nl_BE">Contacten</name>
|
||||
<name lang="pl">Łączność</name>
|
||||
<name lang="pt">Contatos</name>
|
||||
<name lang="pt_BR">Contatos</name>
|
||||
<name lang="ro">Contacte</name>
|
||||
<name lang="ru">Контакты</name>
|
||||
<name lang="sk">Kontakty</name>
|
||||
<name lang="sl">Stiki</name>
|
||||
<name lang="sr">Контакти</name>
|
||||
<name lang="sv">Kontakter</name>
|
||||
<name lang="tr">Kişiler</name>
|
||||
<name lang="uk">Контакти</name>
|
||||
<name lang="zh">触点</name>
|
||||
</names>
|
||||
<element name="act_comptage_no.elmt">
|
||||
<definition height="60" hotspot_x="22" hotspot_y="41" link_type="simple" type="element" version="0.100.0" width="30">
|
||||
<uuid uuid="{19fdfdc7-c1fe-4604-cb14-da967d1a5252}"/>
|
||||
<names>
|
||||
<name lang="ar">عدّاد مفتوح عادة</name>
|
||||
<name lang="ca">Taulell normalment obert</name>
|
||||
<name lang="cs">Kontakt čítače (NO)</name>
|
||||
<name lang="da">Normalt åben tæller</name>
|
||||
<name lang="de">Öffner Zähler betätigt</name>
|
||||
<name lang="el">Ανοικτή επαφή μετρητή</name>
|
||||
<name lang="en">Normally open counter</name>
|
||||
<name lang="es">Contador NA</name>
|
||||
<name lang="fi">Normaalisti avoin laskuri</name>
|
||||
<name lang="fr">Actionné par comptage NO</name>
|
||||
<name lang="hr">Brojač normalno otvoren</name>
|
||||
<name lang="hu">Alaphelyzetben nyitott számláló</name>
|
||||
<name lang="it">Contatto azionato da un contatore (NO)</name>
|
||||
<name lang="ja">常開カウンター</name>
|
||||
<name lang="ko">평상시 열림 카운터</name>
|
||||
<name lang="mn">Ихэвчлэн нээлттэй тоолуур</name>
|
||||
<name lang="nb">Normalt åpen teller</name>
|
||||
<name lang="nl">Teller contact (NO)</name>
|
||||
<name lang="nl_BE">Normaal open teller</name>
|
||||
<name lang="pl">Zestyk zwierny licznika</name>
|
||||
<name lang="pt">Contador normalmente aberto</name>
|
||||
<name lang="pt_BR">Contato NA acionado por contador</name>
|
||||
<name lang="ro">Contor normal deschis</name>
|
||||
<name lang="ru">Счётчик с нормально открытым контактом</name>
|
||||
<name lang="sk">Počítadlo s kontaktom NO</name>
|
||||
<name lang="sl">Števec normalno odprt</name>
|
||||
<name lang="sr">Бројач нормално отворен</name>
|
||||
<name lang="sv">Räknare med slutande kontakt</name>
|
||||
<name lang="tr">Normalde açık sayaç</name>
|
||||
<name lang="uk">Лічильник нормально розімкнений</name>
|
||||
<name lang="zh">计数驱动常开触点</name>
|
||||
</names>
|
||||
<informations>Author: The QElectroTech team
|
||||
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
|
||||
<description>
|
||||
<rect antialias="false" height="10" rx="0" ry="0" style="line-style:normal;line-weight:thin;filling:none;color:black" width="10" x="-20" y="-15"/>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:dashed;line-weight:thin;filling:none;color:black" x1="-10" x2="-3" y1="-10" y2="-10"/>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="UserText" text_width="-1" uuid="{a3321491-1dac-42e2-a9d1-1a43604116a9}" x="-13" y="-2" z="3">
|
||||
<text>14</text>
|
||||
</dynamic_text>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,4,-1,5,25,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="UserText" text_width="-1" uuid="{a82a123f-f3cf-4dd7-93c0-44bbdd940429}" x="-13" y="-34" z="4">
|
||||
<text>11</text>
|
||||
</dynamic_text>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="ElementInfo" text_width="-1" uuid="{960d8065-c520-4d45-aa36-4bb600c0e6db}" x="1" y="-22" z="5">
|
||||
<text/>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_text>
|
||||
<polygon antialias="true" closed="false" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="-5" x2="0" x3="0" y1="-20" y2="0" y3="10"/>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="-30" y2="-20"/>
|
||||
<ellipse antialias="true" height="4" style="line-style:normal;line-weight:thin;filling:none;color:black" width="4" x="-17" y="-12"/>
|
||||
<terminal name="" orientation="s" type="Generic" uuid="{572018e1-9843-49dd-964f-836ff27b8240}" x="0" y="10"/>
|
||||
<terminal name="" orientation="n" type="Generic" uuid="{77224246-2278-498a-88e5-1ecd84e938e8}" x="0" y="-30"/>
|
||||
</description>
|
||||
</definition>
|
||||
</element>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</collection>
|
||||
</project>
|
||||
@@ -0,0 +1,315 @@
|
||||
<project title="Bugtracker 345" uuid="{55b219d1-12d1-5179-aef2-1e303429ecc6}" version="0.200.1">
|
||||
<properties>
|
||||
<property name="saveddate" show="1">10/2/26</property>
|
||||
<property name="saveddate-eu" show="1">02-10-2026</property>
|
||||
<property name="saveddate-us" show="1">2026-10-02</property>
|
||||
<property name="savedfilename" show="1">skeleton</property>
|
||||
<property name="savedfilepath" show="1">/tmp/qet-mcp-new-k41dmnyo/skeleton.qet</property>
|
||||
<property name="savedtime" show="1">18:39</property>
|
||||
</properties>
|
||||
<usage enabled="true" time_spent="0"/>
|
||||
<newdiagrams>
|
||||
<border cols="17" colsize="60" displaycols="true" displayrows="true" rows="8" rowsize="80"/>
|
||||
<inset author="" auto_page_num="" date="null" displayAt="bottom" filename="" folio="" indexrev="" locmach="" plant="" title="" version=""/>
|
||||
<conductors bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<report label="%f-%l%c"/>
|
||||
<xrefs>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="coil" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="commutator" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="plc" xrefpos="AlignBottom"/>
|
||||
<xref delayprefix="" displayhas="cross" master_label="%f-%l%c" offset="0" powerprefix="" showallconfiguredslaves="false" showpowerctc="true" showterminalname="true" slave_label="(%f-%l%c)" slave_offset="0" snapto="label" switchprefix="" type="protection" xrefpos="AlignBottom"/>
|
||||
</xrefs>
|
||||
<conductors_autonums auto_break_conductors="false" current_autonum="" freeze_new_conductors="false"/>
|
||||
<folio_autonums/>
|
||||
<element_autonums current_autonum="" freeze_new_elements="false"/>
|
||||
<guides/>
|
||||
</newdiagrams>
|
||||
<diagram author="" auto_page_num="" cols="17" colsize="60" date="null" displayAt="bottom" displaycols="true" displayrows="true" filename="" folio="" freezeNewConductor="false" freezeNewElement="false" height="660" indexrev="" locmach="" order="1" plant="" rows="8" rowsize="80" title="" uuid="{18d804b7-0e2c-4858-b529-33d0eeb63249}" version="0.200.1-dev">
|
||||
<defaultconductor bicolor="false" bus="" cable="" color2="#000000" condsize="1" conductor_color="" conductor_section="" dash-size="1" displaytext="1" formula="" function="" horizontal-alignment="AlignBottom" horizrotatetext="0" num="_" numsize="7" onetextperfolio="0" tension_protocol="" text_color="#000000" type="multi" vertical-alignment="AlignRight" vertirotatetext="270"/>
|
||||
<elements>
|
||||
<element freezeLabel="false" is_movable="1" orientation="0" prefix="S" type="embed://import/10_electric/10_allpole/310_relays_contactors_contacts/02_contacts_cross_referencing/15_protection_contacts/contact_relais_nf_esclave.elmt" uuid="{8987e487-0140-4cb9-b68d-7a99be379ab5}" x="200" y="200" z="10">
|
||||
<terminals>
|
||||
<terminal id="0" orientation="0" x="0" y="-16"/>
|
||||
<terminal id="1" orientation="2" x="0" y="16"/>
|
||||
</terminals>
|
||||
<inputs/>
|
||||
<dynamic_texts>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" rotation_point_center="false" text_from="ElementInfo" text_width="-1" uuid="{7e7f129d-fefa-41e6-9cf2-aa665d302dcc}" x="5.75" y="-9.5">
|
||||
<text></text>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_elmt_text>
|
||||
<dynamic_elmt_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,0,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="true" rotation="0" rotation_point_center="false" text_from="CompositeText" text_width="-1" uuid="{ea65f2b8-dec9-4968-a9a2-349ae1e66a9d}" x="20" y="0">
|
||||
<text></text>
|
||||
<composite_text>%{label}</composite_text>
|
||||
</dynamic_elmt_text>
|
||||
</dynamic_texts>
|
||||
<texts_groups/>
|
||||
</element>
|
||||
</elements>
|
||||
</diagram>
|
||||
<collection>
|
||||
<category name="import">
|
||||
<names>
|
||||
<name lang="ca">Elements importats</name>
|
||||
<name lang="cs">Zavedené prvky</name>
|
||||
<name lang="da">Importerede elementer</name>
|
||||
<name lang="de">Importierte Elemente</name>
|
||||
<name lang="el">Εισηγμένα στοιχεία</name>
|
||||
<name lang="en">Imported elements</name>
|
||||
<name lang="es">Elementos importados</name>
|
||||
<name lang="fr">Éléments importés</name>
|
||||
<name lang="hr">Uvezeni elementi</name>
|
||||
<name lang="it">Elementi importati</name>
|
||||
<name lang="ja">インバートされた要素</name>
|
||||
<name lang="ko">전기 심볼 요소</name>
|
||||
<name lang="ko_KR">전기 심볼 요소</name>
|
||||
<name lang="nl">Elementen geïmporteerd</name>
|
||||
<name lang="nl_BE">Elementen geïmporteerd</name>
|
||||
<name lang="pl">Elementy importowane</name>
|
||||
<name lang="pt">Elementos importados</name>
|
||||
<name lang="pt_BR">Elementos importados</name>
|
||||
<name lang="ro">Elemente importate</name>
|
||||
<name lang="ru">Импортированные элементы</name>
|
||||
<name lang="sl">Uvoženi elementi</name>
|
||||
<name lang="sv">Importerade element</name>
|
||||
<name lang="tr">İthal öğeler</name>
|
||||
<name lang="uk">Імпортовані елементи</name>
|
||||
<name lang="zh">导入元件</name>
|
||||
</names>
|
||||
<category name="10_electric">
|
||||
<names>
|
||||
<name lang="ar">كهربائي</name>
|
||||
<name lang="ca">Elèctric</name>
|
||||
<name lang="cs">Elektrotechnika</name>
|
||||
<name lang="da">Elektrisk</name>
|
||||
<name lang="de">Elektrik</name>
|
||||
<name lang="el">Ηλεκτρικά</name>
|
||||
<name lang="en">Electric</name>
|
||||
<name lang="es">Eléctrica</name>
|
||||
<name lang="fi">Sähkö</name>
|
||||
<name lang="fr">Electrique</name>
|
||||
<name lang="hr">Električno</name>
|
||||
<name lang="hu">Elektromos</name>
|
||||
<name lang="it">Elettrica</name>
|
||||
<name lang="ja">電気</name>
|
||||
<name lang="ko">전기</name>
|
||||
<name lang="mn">Цахилгаан</name>
|
||||
<name lang="nb">Elektrisk</name>
|
||||
<name lang="nl">Elektrotechniek</name>
|
||||
<name lang="nl_BE">Elektrisch</name>
|
||||
<name lang="pl">Elektrotechnika</name>
|
||||
<name lang="pt">Elétrico</name>
|
||||
<name lang="pt_BR">Elétrica</name>
|
||||
<name lang="ro">Electric</name>
|
||||
<name lang="ru">Электротехника</name>
|
||||
<name lang="sk">Elektrické</name>
|
||||
<name lang="sl">Elektrika</name>
|
||||
<name lang="sr">Електрика</name>
|
||||
<name lang="sv">El</name>
|
||||
<name lang="tr">Elektrik</name>
|
||||
<name lang="uk">Електрика</name>
|
||||
<name lang="zh">10_电气</name>
|
||||
<name lang="zn">电气工程</name>
|
||||
</names>
|
||||
<category name="10_allpole">
|
||||
<names>
|
||||
<name lang="ar">متعدد الأقطاب</name>
|
||||
<name lang="ca">Multifilar</name>
|
||||
<name lang="cs">Vícežilový</name>
|
||||
<name lang="da">Flere ledere</name>
|
||||
<name lang="de">Allpolig</name>
|
||||
<name lang="el">Πολυγραμμικό</name>
|
||||
<name lang="en">All-pole</name>
|
||||
<name lang="es">Multifilar</name>
|
||||
<name lang="fi">Yleisnapa</name>
|
||||
<name lang="fr">Multifilaire</name>
|
||||
<name lang="hr">Univerzalni</name>
|
||||
<name lang="hu">Többvonalas szimbólumok</name>
|
||||
<name lang="it">Multifilare</name>
|
||||
<name lang="ja">複線</name>
|
||||
<name lang="ko">다-극</name>
|
||||
<name lang="mn">Бүх туйл</name>
|
||||
<name lang="nb">Alle poler</name>
|
||||
<name lang="nl">Veel polig</name>
|
||||
<name lang="nl_BE">All-pole</name>
|
||||
<name lang="pl">Schematy wieloliniowe</name>
|
||||
<name lang="pt">Multipolar</name>
|
||||
<name lang="pt_BR">Multifilar</name>
|
||||
<name lang="ro">Bipolar</name>
|
||||
<name lang="ru">Многополюсные</name>
|
||||
<name lang="sk">Všepólové</name>
|
||||
<name lang="sl">Vsi poli</name>
|
||||
<name lang="sr">Свe-полни</name>
|
||||
<name lang="sv">Allpolig</name>
|
||||
<name lang="tr">Çok kutuplu</name>
|
||||
<name lang="uk">Багатополюсні</name>
|
||||
<name lang="zh">10_全极</name>
|
||||
</names>
|
||||
<category name="310_relays_contactors_contacts">
|
||||
<names>
|
||||
<name lang="ar">المرحّلات والموصّلات وجهات الاتصال</name>
|
||||
<name lang="ca">Relés, contactors i contactes</name>
|
||||
<name lang="cs">Relé, stykače a kontakty</name>
|
||||
<name lang="da">Relæer, kontaktorer og kontakter</name>
|
||||
<name lang="de">Relais, Schütze und Kontakte</name>
|
||||
<name lang="el">Ηλεκτρονόμοι και επαφές</name>
|
||||
<name lang="en">Relays, contactors and contacts</name>
|
||||
<name lang="es">Relevadores, contactores y contactos</name>
|
||||
<name lang="fi">Releet, kontaktorit ja koskettimet</name>
|
||||
<name lang="fr">Relais, contacteurs et contacts</name>
|
||||
<name lang="hr">Releji, kontaktori i kontakti</name>
|
||||
<name lang="hu">Relék, kontaktorok és érintkezők</name>
|
||||
<name lang="it">Relè, contattori e contatti</name>
|
||||
<name lang="ja">継電器、接触器、接点</name>
|
||||
<name lang="ko">릴레이, 커넥터및 접점</name>
|
||||
<name lang="mn">Реле, контактор болон контакт</name>
|
||||
<name lang="nb">Reléer, kontaktorere og kontakter</name>
|
||||
<name lang="nl">Relais, contactoren en contacten</name>
|
||||
<name lang="nl_BE">Relais, contacteurs en contacten</name>
|
||||
<name lang="pl">Przekaźniki i styczniki</name>
|
||||
<name lang="pt">Relés, contatores e contactos</name>
|
||||
<name lang="pt_BR">Relés, Contatores e contatos</name>
|
||||
<name lang="ro">Relee, contactoare și contacte</name>
|
||||
<name lang="ru">Реле, пускатели и контакты</name>
|
||||
<name lang="sk">Relé, stykače a kontakty</name>
|
||||
<name lang="sl">Releji, kontaktorji in kontakti</name>
|
||||
<name lang="sr">Релеји, контактори и контакти</name>
|
||||
<name lang="sv">Reläer, kontaktorer och kontakter</name>
|
||||
<name lang="tr">Röleler, kontaktörler ve kontaklar</name>
|
||||
<name lang="uk">Реле, контактори та контакти</name>
|
||||
<name lang="zh">继电器、接触器与触点</name>
|
||||
</names>
|
||||
<category name="02_contacts_cross_referencing">
|
||||
<names>
|
||||
<name lang="ar">تماسات مع إحالات</name>
|
||||
<name lang="ca">Contactes amb referències creuades</name>
|
||||
<name lang="cs">Kontakty s křížovými odkazy</name>
|
||||
<name lang="da">Kontaker med krydsreference</name>
|
||||
<name lang="de">Kontakte mit Querverweis</name>
|
||||
<name lang="el">Επαφές με παραπομπές</name>
|
||||
<name lang="en">Contacts with cross referencing</name>
|
||||
<name lang="es">Contactos con referencia cruzada</name>
|
||||
<name lang="fi">Kosketimet lehtiviittauksella</name>
|
||||
<name lang="fr">Contacts avec référence croisée</name>
|
||||
<name lang="hr">Kontakti s unakrsnim referenciranjem</name>
|
||||
<name lang="hu">Érintkezők kereszthivatkozásokkal</name>
|
||||
<name lang="it">Contatti con riferimenti incrociati</name>
|
||||
<name lang="ja">相互参照付き接点</name>
|
||||
<name lang="ko">교차 참조 접점</name>
|
||||
<name lang="mn">Харилцан холбоос бүхий контакт</name>
|
||||
<name lang="nb">Kontakter med kryssreferanse</name>
|
||||
<name lang="nl">Contacten met kruisreferentie</name>
|
||||
<name lang="nl_BE">Contacten met kruisverwijzing</name>
|
||||
<name lang="pl">Zestyki (automatyczne oznaczenia)</name>
|
||||
<name lang="pt">Contactos com renvoi de folio</name>
|
||||
<name lang="pt_BR">Contatos com referencia cruzada</name>
|
||||
<name lang="ro">Contacte cu renvoi de folio</name>
|
||||
<name lang="ru">Контакты с перекрестными ссылками</name>
|
||||
<name lang="sk">Kontakty s krížovým odkazom</name>
|
||||
<name lang="sl">Kontakti s sklici</name>
|
||||
<name lang="sr">Контакти са унакрсним референцирањем</name>
|
||||
<name lang="sv">Kontakter med bladhänvisning</name>
|
||||
<name lang="tr">Çapraz referanslı kontaklar</name>
|
||||
<name lang="uk">Контакти з перехресними посиланнями</name>
|
||||
<name lang="zh">触点交叉引用</name>
|
||||
</names>
|
||||
<category name="15_protection_contacts">
|
||||
<names>
|
||||
<name lang="ar">حمايات</name>
|
||||
<name lang="ca">Proteccions</name>
|
||||
<name lang="cs">Ochrany</name>
|
||||
<name lang="da">Beskyttelsesfunktioner</name>
|
||||
<name lang="de">Schutzfunktion</name>
|
||||
<name lang="el">Προστασία</name>
|
||||
<name lang="en">Protections</name>
|
||||
<name lang="es">Protecciones</name>
|
||||
<name lang="fi">Suojaukset</name>
|
||||
<name lang="fr">Protections</name>
|
||||
<name lang="hr">Zaštite</name>
|
||||
<name lang="hu">Védelmek</name>
|
||||
<name lang="it">Protezioni</name>
|
||||
<name lang="ja">保護</name>
|
||||
<name lang="ko">보호 접점</name>
|
||||
<name lang="mn">Хамгаалалт</name>
|
||||
<name lang="nb">Beskyttelser</name>
|
||||
<name lang="nl">Beveiliging</name>
|
||||
<name lang="nl_BE">Beveiligingen</name>
|
||||
<name lang="pl">Zabezpieczenia</name>
|
||||
<name lang="pt">Protecções</name>
|
||||
<name lang="pt_BR">Protecções</name>
|
||||
<name lang="ro">Protecții</name>
|
||||
<name lang="ru">Защитные уст-ва</name>
|
||||
<name lang="sk">Ochrany</name>
|
||||
<name lang="sl">Zaščite</name>
|
||||
<name lang="sr">Заштите</name>
|
||||
<name lang="sv">Skydd</name>
|
||||
<name lang="tr">Korumalar</name>
|
||||
<name lang="uk">Захисти</name>
|
||||
<name lang="zh">保护触点</name>
|
||||
</names>
|
||||
<element name="contact_relais_nf_esclave.elmt">
|
||||
<definition height="60" hotspot_x="9" hotspot_y="30" link_type="slave" type="element" version="0.100.0" width="20">
|
||||
<uuid uuid="{94d56f06-6814-5561-9148-2da3a9c4f846}"/>
|
||||
<names>
|
||||
<name lang="ar">مُلامس مُرحّل حراري (NC)</name>
|
||||
<name lang="ca">Relé tèrmic de contacte (NC)</name>
|
||||
<name lang="cs">Kontakt (NC) tepelného jističe</name>
|
||||
<name lang="da">Kontakt termisk relæ (NC)</name>
|
||||
<name lang="de">Relaisöffner</name>
|
||||
<name lang="el">Επαφή θερμικού κλειστή</name>
|
||||
<name lang="en">Contact thermal relay (NC)</name>
|
||||
<name lang="es">Contacto relé térmico NC</name>
|
||||
<name lang="fi">Kosketin lämpörele (NC)</name>
|
||||
<name lang="fr">Contact relais thermique (NF)</name>
|
||||
<name lang="hr">Kontakt termičkog releja (NC)</name>
|
||||
<name lang="hu">Hőrelé érintkező (NC)</name>
|
||||
<name lang="it">Contatto relé térmico (NC)</name>
|
||||
<name lang="ja">接点 熱動継電器 (NC)</name>
|
||||
<name lang="ko">접점 열 릴레이 (NC)</name>
|
||||
<name lang="mn">Дулааны релений контакт (NC)</name>
|
||||
<name lang="nb">Kontakt termisk relé (NC)</name>
|
||||
<name lang="nl">Thermisch bediend contact (NC)</name>
|
||||
<name lang="nl_BE">Contact thermisch relais (NC)</name>
|
||||
<name lang="pl">Zestyk rozwierny przekaźnika cieplnego</name>
|
||||
<name lang="pt">Contacto relé térmico (NC)</name>
|
||||
<name lang="pt_BR">Contato de relé térmico (NF)</name>
|
||||
<name lang="ro">Contact releu termic (NC)</name>
|
||||
<name lang="ru">Контакт теплового реле (НЗ)</name>
|
||||
<name lang="sk">Kontakt tepelného relé (NC)</name>
|
||||
<name lang="sl">Kontakt termičnega releja (NC)</name>
|
||||
<name lang="sr">Контакт термичког релеја (NC)</name>
|
||||
<name lang="sv">Kontakt termiskt relä (NC)</name>
|
||||
<name lang="tr">Termik röle kontağı (NC)</name>
|
||||
<name lang="uk">Контакт теплового реле (NC)</name>
|
||||
<name lang="zh">继电器常闭从属触点</name>
|
||||
</names>
|
||||
<kindInformations>
|
||||
<kindInformation name="type">simple</kindInformation>
|
||||
<kindInformation name="state">NC</kindInformation>
|
||||
<kindInformation name="number">1</kindInformation>
|
||||
</kindInformations>
|
||||
<informations>Author: The QElectroTech team
|
||||
License: see http://qelectrotech.org/wiki/doc/elements_license</informations>
|
||||
<description>
|
||||
<polygon antialias="true" closed="false" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="5" x2="0" x3="0" y1="-10" y2="10" y3="20"/>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="6" y1="-9" y2="-9"/>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:normal;filling:none;color:black" x1="0" x2="0" y1="-20" y2="-9"/>
|
||||
<dynamic_text Halignment="AlignLeft" Valignment="AlignTop" font="Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular" frame="false" keep_visual_rotation="false" rotation="0" text_from="ElementInfo" text_width="-1" uuid="{4a7b5a49-c49c-452b-8755-35b048dafc8e}" x="5.75" y="-9.5" z="4">
|
||||
<text/>
|
||||
<info_name>label</info_name>
|
||||
</dynamic_text>
|
||||
<polygon antialias="false" closed="false" style="line-style:normal;line-weight:thin;filling:none;color:black" x1="-4" x2="-4" x3="2" y1="4" y2="0" y3="0"/>
|
||||
<line antialias="false" end1="none" end2="none" length1="1.5" length2="1.5" style="line-style:normal;line-weight:thin;filling:none;color:black" x1="-2" x2="-2" y1="0" y2="3"/>
|
||||
<terminal name="" orientation="n" type="Generic" uuid="{b69eb17b-2ec8-47c4-bf92-0065d7e4cd7b}" x="0" y="-20"/>
|
||||
<terminal name="" orientation="s" type="Generic" uuid="{759c2015-c0d8-4059-8be1-cfbd8ce1bc76}" x="0" y="20"/>
|
||||
</description>
|
||||
</definition>
|
||||
</element>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</category>
|
||||
</collection>
|
||||
</project>
|
||||
@@ -0,0 +1,124 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Linked by run_image_dark_probe.py against the application's own objects.
|
||||
#include <QApplication>
|
||||
#include <QBuffer>
|
||||
#include <QCheckBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QFontDatabase>
|
||||
#include <QPrinter>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QSvgGenerator>
|
||||
#include <QTemporaryDir>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
#include "../../sources/diagram.h"
|
||||
#include "../../sources/qetproject.h"
|
||||
#include "../../sources/qetresult.h"
|
||||
#include "../../sources/qetmessagebox.h"
|
||||
#include "../../sources/qetpalette.h"
|
||||
#include "../../sources/palettegraphicsview.h"
|
||||
#include "../../sources/qetgraphicsitem/diagramimageitem.h"
|
||||
#include "../../sources/ui/imagepropertieswidget.h"
|
||||
#include "../../sources/PropertiesEditor/propertieseditordialog.h"
|
||||
|
||||
static void check(bool value, const char *message) { if (!value) throw std::runtime_error(message); }
|
||||
static QByteArray png(const QPixmap &pixmap) {
|
||||
QByteArray bytes; QBuffer buffer(&bytes); buffer.open(QIODevice::WriteOnly); pixmap.save(&buffer,"PNG"); return bytes;
|
||||
}
|
||||
static QByteArray read(const QString &path) { QFile file(path); check(file.open(QIODevice::ReadOnly),"read export"); return file.readAll(); }
|
||||
static DiagramImageItem *findImage(Diagram *diagram) {
|
||||
for (auto *item : diagram->items()) if (auto *image = dynamic_cast<DiagramImageItem *>(item)) return image;
|
||||
return nullptr;
|
||||
}
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QApplication app(argc,argv);
|
||||
std::freopen(argv[2],"w",stdout); std::freopen(argv[2],"a",stderr);
|
||||
QTemporaryDir settings;
|
||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||
QSettings::setPath(QSettings::IniFormat,QSettings::UserScope,settings.path());
|
||||
QCoreApplication::setOrganizationName("QETImageDarkRegression");
|
||||
QETProject::setBackupEnabled(false); QET::QetMessageBox::setNonInteractive(true);
|
||||
QFontDatabase::addApplicationFont(":/fonts/LiberationSans-Regular.ttf");
|
||||
try {
|
||||
const QString output=QString::fromLocal8Bit(argv[3]);
|
||||
QETProject project(QString::fromLocal8Bit(argv[1])); check(project.state()==QETProject::Ok,"load fixture");
|
||||
auto *diagram=project.diagrams().first();
|
||||
QImage original(40,30,QImage::Format_ARGB32); original.fill(QColor(197,81,33));
|
||||
for(int y=0;y<30;++y) for(int x=10;x<20;++x) original.setPixelColor(x,y,QColor(197,81,33,128));
|
||||
for(int y=10;y<16;++y) for(int x=24;x<30;++x) original.setPixelColor(x,y,Qt::transparent);
|
||||
QPixmap source=QPixmap::fromImage(original);
|
||||
auto *image=new DiagramImageItem(source); diagram->addItem(image);
|
||||
image->setPos(55,55); image->setLabel("Image test"); image->setRotationAngle(17); image->setScaleFactorX(1.3); image->setSkewX(8);
|
||||
check(!image->adaptToDarkTheme(),"new image defaults to original colours");
|
||||
QDomDocument xml; auto legacy=image->toXml(xml);
|
||||
check(!legacy.hasAttribute("adapt_to_dark_theme"),"default does not add an attribute to old files");
|
||||
const QRect crop(4,3,24,20);
|
||||
legacy.firstChild().setNodeValue(QString::fromLatin1(png(source.copy(crop)).toBase64()));
|
||||
auto base=xml.createElement("image_base"); base.appendChild(xml.createTextNode(QString::fromLatin1(png(source).toBase64()))); legacy.appendChild(base);
|
||||
auto cropElement=xml.createElement("crop"); cropElement.setAttribute("x",4); cropElement.setAttribute("y",3); cropElement.setAttribute("w",24); cropElement.setAttribute("h",20); legacy.appendChild(cropElement);
|
||||
image->setAdaptToDarkTheme(true); check(image->fromXml(legacy),"load cropped legacy image");
|
||||
check(!image->adaptToDarkTheme(),"missing attribute resets an existing image to original colours");
|
||||
check(image->pixmap().size()==crop.size(),"crop survives");
|
||||
const QByteArray pristine=png(image->pixmap()); const auto transform=image->transform(); const auto bounds=image->boundingRect();
|
||||
ImagePropertiesWidget widget(image); auto *box=widget.findChild<QCheckBox *>("m_adapt_to_dark_theme_cb");
|
||||
check(box && !box->isChecked(),"unchecked properties default");
|
||||
check(box->text()==QString::fromUtf8("Adapter l’image au thème sombre"),"checkbox wording");
|
||||
check(box->toolTip()==QString::fromUtf8("Adapte les couleurs à l’affichage sombre uniquement. L’image originale, les exports et les impressions restent inchangés."),"tooltip wording");
|
||||
box->setChecked(true); check(image->adaptToDarkTheme(),"preview"); widget.reset(); check(!image->adaptToDarkTheme(),"reset cancels preview");
|
||||
diagram->undoStack().clear(); box->setChecked(true); widget.apply(); check(image->adaptToDarkTheme(),"apply");
|
||||
diagram->undoStack().undo(); check(!image->adaptToDarkTheme()&&!box->isChecked(),"undo updates checkbox");
|
||||
diagram->undoStack().redo(); check(image->adaptToDarkTheme()&&box->isChecked(),"redo updates checkbox");
|
||||
widget.setLiveEdit(true); box->click(); check(!image->adaptToDarkTheme(),"live edit");
|
||||
check(diagram->undoStack().count()==2,"deliberate toggles do not merge");
|
||||
diagram->undoStack().undo(); check(image->adaptToDarkTheme(),"live undo");
|
||||
diagram->undoStack().redo(); check(!image->adaptToDarkTheme(),"live redo");
|
||||
box->click(); check(image->adaptToDarkTheme()&&diagram->undoStack().count()==3,"edit after undo has current baseline");
|
||||
for (auto role : {QDialogButtonBox::Cancel,QDialogButtonBox::Reset,QDialogButtonBox::Apply}) {
|
||||
image->setAdaptToDarkTheme(false);
|
||||
auto *editor=new ImagePropertiesWidget(image); PropertiesEditorDialog dialog(editor);
|
||||
editor->findChild<QCheckBox *>("m_adapt_to_dark_theme_cb")->setChecked(true);
|
||||
dialog.findChild<QDialogButtonBox *>()->button(role)->click();
|
||||
check(image->adaptToDarkTheme()==(role==QDialogButtonBox::Apply),"actual dialog button semantics");
|
||||
}
|
||||
check(png(image->pixmap())==pristine && image->transform()==transform && image->boundingRect()==bounds && image->label()=="Image test","choice cannot modify pixels, transform, crop or caption");
|
||||
image->setAdaptToDarkTheme(false);
|
||||
{
|
||||
auto *editor=new ImagePropertiesWidget(image); PropertiesEditorDialog dialog(editor);
|
||||
editor->findChild<QCheckBox *>("m_adapt_to_dark_theme_cb")->setChecked(true);
|
||||
dialog.reject(); check(!image->adaptToDarkTheme(),"Escape/title-bar rejection cancels preview");
|
||||
}
|
||||
PaletteGraphicsView view(diagram); view.setSceneRect(0,0,180,180); view.setFrameShape(QFrame::NoFrame);
|
||||
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
view.setAlignment(Qt::AlignLeft|Qt::AlignTop); view.resize(180,180); view.setAttribute(Qt::WA_DontShowOnScreen); view.show();
|
||||
for (bool dark : {false,true}) for(bool adapt : {false,true}) {
|
||||
const auto palette=dark?QET::Palette::fusionDark():QET::Palette::fusionLight(); QApplication::setPalette(palette);
|
||||
image->setAdaptToDarkTheme(adapt); QApplication::processEvents(); const auto shown=view.viewport()->grab().toImage();
|
||||
shown.save(output+QString("/screen-%1-%2.png").arg(dark).arg(adapt));
|
||||
const auto point=view.mapFromScene(image->mapToScene(QPointF(2,2))); const auto ratio=shown.devicePixelRatio();
|
||||
QImage expected(1,1,QImage::Format_RGB32); expected.fill(QColor(197,81,33));
|
||||
if(dark&&adapt) QET::Palette::invertLightness(expected,palette.color(QPalette::Base),palette.color(QPalette::Text));
|
||||
check(shown.pixelColor(qRound(point.x()*ratio),qRound(point.y()*ratio))==expected.pixelColor(0,0),"actual rotated/cropped image display colour");
|
||||
project.setFilePath(output+QString("/project-%1-%2.qet").arg(dark).arg(adapt)); check(project.write().isOk(),"project save");
|
||||
QETProject reopened(project.filePath()); check(reopened.state()==QETProject::Ok,"project reopen");
|
||||
auto *loaded=findImage(reopened.diagrams().first()); check(loaded && loaded->adaptToDarkTheme()==adapt,"per-image persisted choice");
|
||||
check(png(loaded->pixmap())==pristine && loaded->transform()==transform && loaded->label()=="Image test","reopen keeps original image/crop/transform/caption");
|
||||
}
|
||||
QApplication::setPalette(QET::Palette::fusionDark()); QImage exports[2];
|
||||
for(int i=0;i<2;++i) {
|
||||
image->setAdaptToDarkTheme(i); exports[i]=QImage(180,180,QImage::Format_RGB32); exports[i].fill(Qt::white);
|
||||
{QPainter painter(&exports[i]); diagram->render(&painter,QRectF(0,0,180,180),QRectF(0,0,180,180));}
|
||||
exports[i].save(output+QString("/export-%1.png").arg(i));
|
||||
QSvgGenerator svg; svg.setFileName(output+QString("/export-%1.svg").arg(i)); svg.setSize(QSize(180,180)); svg.setViewBox(QRect(0,0,180,180));
|
||||
{QPainter painter(&svg); diagram->render(&painter,QRectF(0,0,180,180),QRectF(0,0,180,180));}
|
||||
QPrinter printer; printer.setOutputFormat(QPrinter::PdfFormat); printer.setOutputFileName(output+QString("/print-%1.pdf").arg(i)); printer.setResolution(72);
|
||||
{QPainter painter(&printer); check(painter.isActive(),"PDF print painter"); diagram->render(&painter,QRectF(0,0,180,180),QRectF(0,0,180,180));}
|
||||
}
|
||||
check(exports[0]==exports[1],"raster export unchanged"); check(read(output+"/export-0.svg")==read(output+"/export-1.svg"),"SVG export unchanged");
|
||||
std::puts("PASS: image defaults, XML crop/alpha/transform/label, properties preview/apply/reset/cancel, undo/redo and live edit, four display modes, project save/reopen, raster/SVG export, PDF print generation");
|
||||
} catch(const std::exception &error) {std::fprintf(stderr,"FAIL: %s\n",error.what());return 1;}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Link the attached plain-paste integration probe using an existing Ninja app build.
|
||||
|
||||
Reuses app objects without duplicating the app or introducing a core-library
|
||||
refactor. No application source or existing executable is replaced. The
|
||||
generated manifest, probe binary, reports and export fixtures stay in build/.
|
||||
"""
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--build', required=True)
|
||||
parser.add_argument('--binary', required=True)
|
||||
parser.add_argument('--source', required=True)
|
||||
parser.add_argument('--fixture', required=True)
|
||||
args = parser.parse_args()
|
||||
build = pathlib.Path(args.build).resolve()
|
||||
binary = pathlib.Path(args.binary).resolve()
|
||||
source = pathlib.Path(args.source).resolve()
|
||||
fixture = pathlib.Path(args.fixture).resolve()
|
||||
manifest = (build / 'build.ninja').read_text(encoding='utf-8')
|
||||
match = re.search(r'^build (\S*?/sources/main\.cpp\.(?:obj|o)):', manifest, re.MULTILINE)
|
||||
if not match:
|
||||
sys.exit('Cannot find the application main object in the Ninja build.')
|
||||
main_object = match.group(1)
|
||||
probe_object = main_object.replace('main.cpp.', 'attached_plain_paste_probe.cpp.')
|
||||
probe_binary = binary.with_name('attached_plain_paste_probe' + binary.suffix)
|
||||
def escape(path):
|
||||
return pathlib.Path(path).as_posix().replace('$', '$$').replace(':', '$:').replace(' ', '$ ')
|
||||
main_source = source.parents[2] / 'sources' / 'main.cpp'
|
||||
manifest = manifest.replace(main_object, probe_object)
|
||||
manifest = manifest.replace(escape(main_source), escape(source))
|
||||
if binary.suffix:
|
||||
manifest = manifest.replace(binary.name, probe_binary.name)
|
||||
else:
|
||||
old_target = escape(binary.name if binary.parent == build else binary)
|
||||
new_target = escape(probe_binary.name if probe_binary.parent == build else probe_binary)
|
||||
manifest = manifest.replace('build ' + old_target + ':', 'build ' + new_target + ':')
|
||||
old_file = binary.name if binary.parent == build else binary.as_posix()
|
||||
new_file = probe_binary.name if probe_binary.parent == build else probe_binary.as_posix()
|
||||
manifest = re.sub(r'^ TARGET_FILE = ' + re.escape(old_file) + r'$',
|
||||
' TARGET_FILE = ' + new_file, manifest, flags=re.MULTILINE)
|
||||
probe_manifest = build / 'attached-plain-paste-probe.ninja'
|
||||
probe_manifest.write_text(manifest, encoding='utf-8')
|
||||
ninja = shutil.which('ninja')
|
||||
if not ninja:
|
||||
sys.exit('Ninja is required for the integration probe.')
|
||||
target = probe_binary.name if probe_binary.parent == build else str(probe_binary)
|
||||
subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], check=True)
|
||||
output = build / 'attached-plain-paste'
|
||||
output.mkdir(exist_ok=True)
|
||||
report = output / 'results.txt'
|
||||
result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=90)
|
||||
if report.exists():
|
||||
print(report.read_text(encoding='utf-8', errors='replace'))
|
||||
sys.exit(result.returncode)
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Link the real image/widget integration probe using an existing Ninja app build.
|
||||
|
||||
Reuses app objects without duplicating the app or introducing a core-library
|
||||
refactor. No application source or existing executable is replaced. The
|
||||
generated manifest, probe binary, reports and export fixtures stay in build/.
|
||||
"""
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--build', required=True)
|
||||
parser.add_argument('--binary', required=True)
|
||||
parser.add_argument('--source', required=True)
|
||||
parser.add_argument('--fixture', required=True)
|
||||
args = parser.parse_args()
|
||||
build = pathlib.Path(args.build).resolve()
|
||||
binary = pathlib.Path(args.binary).resolve()
|
||||
source = pathlib.Path(args.source).resolve()
|
||||
fixture = pathlib.Path(args.fixture).resolve()
|
||||
manifest = (build / 'build.ninja').read_text(encoding='utf-8')
|
||||
match = re.search(r'^build (\S*?/sources/main\.cpp\.(?:obj|o)):', manifest, re.MULTILINE)
|
||||
if not match:
|
||||
sys.exit('Cannot find the application main object in the Ninja build.')
|
||||
main_object = match.group(1)
|
||||
probe_object = main_object.replace('main.cpp.', 'image_dark_integration_probe.cpp.')
|
||||
probe_binary = binary.with_name('image_dark_integration_probe' + binary.suffix)
|
||||
def escape(path):
|
||||
return pathlib.Path(path).as_posix().replace('$', '$$').replace(':', '$:').replace(' ', '$ ')
|
||||
main_source = source.parents[2] / 'sources' / 'main.cpp'
|
||||
manifest = manifest.replace(main_object, probe_object)
|
||||
manifest = manifest.replace(escape(main_source), escape(source))
|
||||
if binary.suffix:
|
||||
manifest = manifest.replace(binary.name, probe_binary.name)
|
||||
else:
|
||||
old_target = escape(binary.name if binary.parent == build else binary)
|
||||
new_target = escape(probe_binary.name if probe_binary.parent == build else probe_binary)
|
||||
manifest = manifest.replace('build ' + old_target + ':', 'build ' + new_target + ':')
|
||||
old_file = binary.name if binary.parent == build else binary.as_posix()
|
||||
new_file = probe_binary.name if probe_binary.parent == build else probe_binary.as_posix()
|
||||
manifest = re.sub(r'^ TARGET_FILE = ' + re.escape(old_file) + r'$',
|
||||
' TARGET_FILE = ' + new_file, manifest, flags=re.MULTILINE)
|
||||
probe_manifest = build / 'image-dark-probe.ninja'
|
||||
probe_manifest.write_text(manifest, encoding='utf-8')
|
||||
ninja = shutil.which('ninja')
|
||||
if not ninja:
|
||||
sys.exit('Ninja is required for the integration probe.')
|
||||
target = probe_binary.name if probe_binary.parent == build else str(probe_binary)
|
||||
subprocess.run([ninja, '-C', str(build), '-f', probe_manifest.name, target], check=True)
|
||||
output = build / 'image-dark-integration'
|
||||
output.mkdir(exist_ok=True)
|
||||
report = output / 'results.txt'
|
||||
result = subprocess.run([str(probe_binary), str(fixture), str(report), str(output)], timeout=90)
|
||||
if report.exists():
|
||||
print(report.read_text(encoding='utf-8', errors='replace'))
|
||||
if result.returncode == 0:
|
||||
def print_streams(path):
|
||||
streams = re.findall(rb'\nstream\r?\n(.*?)\nendstream', path.read_bytes(), re.DOTALL)
|
||||
decoded = []
|
||||
for stream in streams:
|
||||
try:
|
||||
data = zlib.decompress(stream)
|
||||
except zlib.error:
|
||||
data = stream
|
||||
# Qt assigns every PDF a fresh XMP DocumentID (and dates).
|
||||
# Metadata is not printing content and must not be compared
|
||||
# as if it were a graphics/image/font stream.
|
||||
if b'<x:xmpmeta' not in data:
|
||||
decoded.append(data)
|
||||
return decoded
|
||||
first = print_streams(output / 'print-0.pdf')
|
||||
second = print_streams(output / 'print-1.pdf')
|
||||
if not first or first != second:
|
||||
sys.exit('PDF printing changed the decoded graphics/image/font streams.')
|
||||
print('PASS: PDF print graphics/image/font streams identical for both choices.')
|
||||
try:
|
||||
import pymupdf
|
||||
except ImportError:
|
||||
print('PDF raster comparison not run: optional PyMuPDF is unavailable.')
|
||||
else:
|
||||
documents = [pymupdf.open(output / f'print-{i}.pdf') for i in range(2)]
|
||||
if len(documents[0]) != len(documents[1]):
|
||||
sys.exit('PDF page counts differ.')
|
||||
for page in range(len(documents[0])):
|
||||
pixels = [doc[page].get_pixmap(dpi=144, alpha=False) for doc in documents]
|
||||
if (pixels[0].width, pixels[0].height, pixels[0].samples) != (pixels[1].width, pixels[1].height, pixels[1].samples):
|
||||
sys.exit('Rasterized PDF print pages differ.')
|
||||
for i, image in enumerate(pixels):
|
||||
image.save(output / f'print-{i}-page-{page}.png')
|
||||
print('PASS: PDF print pages rendered at 144 dpi are pixel-identical.')
|
||||
sys.exit(result.returncode)
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "borderproperties.h"
|
||||
|
||||
#include <QtTest>
|
||||
#include <QDomDocument>
|
||||
|
||||
/**
|
||||
BorderProperties::fromXml() -- the <border> element a project keeps for
|
||||
its new folios. Its column width and row height are decimals, written
|
||||
by toXml() as "%1"; they used to be read with toInt(), which is 0 for
|
||||
"60.5", and 0 is then clamped to the 5 px minimum by the folio.
|
||||
*/
|
||||
class tst_borderpropertiesxml : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
static QDomElement border(const QString &attributes)
|
||||
{
|
||||
static QDomDocument doc;
|
||||
doc.setContent(QStringLiteral("<border %1/>").arg(attributes));
|
||||
return doc.documentElement();
|
||||
}
|
||||
|
||||
static BorderProperties base()
|
||||
{
|
||||
BorderProperties p;
|
||||
p.columns_count = 17;
|
||||
p.columns_width = 60;
|
||||
p.rows_count = 8;
|
||||
p.rows_height = 80;
|
||||
p.display_columns = true;
|
||||
p.display_rows = true;
|
||||
return p;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void sizesAreReadAsDecimals_data()
|
||||
{
|
||||
QTest::addColumn<QString>("colsize");
|
||||
QTest::addColumn<QString>("rowsize");
|
||||
QTest::addColumn<qreal>("width");
|
||||
QTest::addColumn<qreal>("height");
|
||||
QTest::newRow("whole numbers") << "50" << "100" << 50.0 << 100.0;
|
||||
QTest::newRow("decimals") << "60.5" << "80.25" << 60.5 << 80.25;
|
||||
QTest::newRow("rounds down") << "61.3" << "61.3" << 61.3 << 61.3;
|
||||
QTest::newRow("rounds up") << "61.7" << "61.7" << 61.7 << 61.7;
|
||||
}
|
||||
|
||||
void sizesAreReadAsDecimals()
|
||||
{
|
||||
QFETCH(QString, colsize);
|
||||
QFETCH(QString, rowsize);
|
||||
QFETCH(qreal, width);
|
||||
QFETCH(qreal, height);
|
||||
|
||||
BorderProperties p = base();
|
||||
QDomElement e = border(QStringLiteral("cols=\"17\" colsize=\"%1\" rows=\"8\" rowsize=\"%2\"")
|
||||
.arg(colsize, rowsize));
|
||||
p.fromXml(e);
|
||||
QCOMPARE(p.columns_width, width);
|
||||
QCOMPARE(p.rows_height, height);
|
||||
QCOMPARE(p.columns_count, 17);
|
||||
QCOMPARE(p.rows_count, 8);
|
||||
}
|
||||
|
||||
void savedSizesComeBack()
|
||||
{
|
||||
BorderProperties saved = base();
|
||||
saved.columns_width = 60.5;
|
||||
saved.rows_height = 80.25;
|
||||
QDomDocument doc;
|
||||
QDomElement e = doc.createElement(QStringLiteral("border"));
|
||||
saved.toXml(e);
|
||||
|
||||
BorderProperties loaded = base();
|
||||
loaded.fromXml(e);
|
||||
QCOMPARE(loaded.columns_width, 60.5);
|
||||
QCOMPARE(loaded.rows_height, 80.25);
|
||||
}
|
||||
|
||||
// An unreadable size leaves the previous value, as a missing one does.
|
||||
void unreadableSizeIsLeftAlone_data()
|
||||
{
|
||||
QTest::addColumn<QString>("attributes");
|
||||
QTest::newRow("missing") << "cols=\"17\" rows=\"8\"";
|
||||
QTest::newRow("text") << "colsize=\"wide\" rowsize=\"tall\"";
|
||||
QTest::newRow("nan") << "colsize=\"nan\" rowsize=\"nan\"";
|
||||
QTest::newRow("inf") << "colsize=\"inf\" rowsize=\"-inf\"";
|
||||
}
|
||||
|
||||
void unreadableSizeIsLeftAlone()
|
||||
{
|
||||
QFETCH(QString, attributes);
|
||||
BorderProperties p = base();
|
||||
QDomElement e = border(attributes);
|
||||
p.fromXml(e);
|
||||
QCOMPARE(p.columns_width, 60.0);
|
||||
QCOMPARE(p.rows_height, 80.0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_borderpropertiesxml)
|
||||
#include "tst_borderpropertiesxml.moc"
|
||||
@@ -102,9 +102,11 @@ class tst_conductorapplychanges : public QObject
|
||||
QTest::newRow(list.at(i).first) << i;
|
||||
}
|
||||
|
||||
// The shown wire is all variant 0, the user changes one field
|
||||
// to variant 1, another selected wire is all variant 2: it
|
||||
// takes that one field and keeps every other one.
|
||||
// The shown wire is all variant 0 and the user changes one
|
||||
// field to variant 1. The other wire holds variant 1 in every
|
||||
// other field, so copying one of them by mistake (they are
|
||||
// variant 0 in the edit) shows, booleans included; and variant
|
||||
// 2 in the edited field, so not copying it shows too.
|
||||
void eachFieldAlone()
|
||||
{
|
||||
QFETCH(int, index);
|
||||
@@ -115,9 +117,11 @@ class tst_conductorapplychanges : public QObject
|
||||
set(after, 1);
|
||||
QVERIFY(after != before);
|
||||
|
||||
ConductorProperties other = all(2);
|
||||
ConductorProperties other = all(1);
|
||||
set(other, 2);
|
||||
ConductorProperties expected = other;
|
||||
set(expected, 1);
|
||||
QVERIFY(expected != other);
|
||||
|
||||
other.applyChanges(before, after);
|
||||
QVERIFY(other == expected);
|
||||
@@ -127,7 +131,7 @@ class tst_conductorapplychanges : public QObject
|
||||
void noEditChangesNothing()
|
||||
{
|
||||
const ConductorProperties shown = all(0);
|
||||
ConductorProperties other = all(2);
|
||||
ConductorProperties other = all(1);
|
||||
const ConductorProperties kept = other;
|
||||
other.applyChanges(shown, shown);
|
||||
QVERIFY(other == kept);
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QUndoStack>
|
||||
|
||||
#include "conductormultiedit.h"
|
||||
#include "qetapp.h"
|
||||
|
||||
// qet.cpp needs it; the application is not linked
|
||||
QString QETApp::m_interface_language;
|
||||
|
||||
// A wire as ConductorMultiEdit sees one: properties through the same
|
||||
// Q_PROPERTY the undo command writes, a potential, and a position.
|
||||
class FakeWire : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(ConductorProperties properties READ properties WRITE setProperties)
|
||||
|
||||
public:
|
||||
FakeWire(const QString &num, const QString &function,
|
||||
const QString &cable, QPointF pos) :
|
||||
m_pos(pos)
|
||||
{
|
||||
m_properties.text = num;
|
||||
m_properties.m_function = function;
|
||||
m_properties.m_cable = cable;
|
||||
}
|
||||
|
||||
ConductorProperties properties() const { return m_properties; }
|
||||
void setProperties(const ConductorProperties &p) { m_properties = p; }
|
||||
QSet<FakeWire *> relatedPotentialConductors() const { return m_potential; }
|
||||
QPointF pos() const { return m_pos; }
|
||||
|
||||
// Joins wires into one potential; like the real one, a wire's
|
||||
// potential lists the others, not itself.
|
||||
static void join(const QList<FakeWire *> &wires)
|
||||
{
|
||||
for (FakeWire *w : wires)
|
||||
for (FakeWire *o : wires)
|
||||
if (o != w)
|
||||
w->m_potential.insert(o);
|
||||
}
|
||||
|
||||
ConductorProperties m_properties;
|
||||
|
||||
private:
|
||||
QSet<FakeWire *> m_potential;
|
||||
QPointF m_pos;
|
||||
};
|
||||
|
||||
// The rules for editing several wires at once in the Selection properties
|
||||
// panel (#500): which wire is shown first, which wires an edit reaches,
|
||||
// what one undo step does to all of them, and the fields shown blank
|
||||
// because the wires do not agree on them.
|
||||
class tst_conductormultiedit : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
using Wires = QList<FakeWire *>;
|
||||
|
||||
static ConductorProperties edited(ConductorProperties p,
|
||||
QString ConductorProperties::*field,
|
||||
const QString &value)
|
||||
{
|
||||
p.*field = value;
|
||||
return p;
|
||||
}
|
||||
|
||||
private slots:
|
||||
// Same wires, any selection order: same first wire
|
||||
void firstWireIsStable()
|
||||
{
|
||||
FakeWire a("1", "", "", {50, 10}), b("2", "", "", {10, 10}),
|
||||
c("3", "", "", {0, 90});
|
||||
const auto pos = [](FakeWire *w) { return w->pos(); };
|
||||
|
||||
Wires one {&c, &a, &b}, two {&a, &b, &c};
|
||||
ConductorMultiEdit::sortByPosition(one, pos);
|
||||
ConductorMultiEdit::sortByPosition(two, pos);
|
||||
QCOMPARE(one, (Wires {&b, &a, &c}));
|
||||
QCOMPARE(two, one);
|
||||
}
|
||||
|
||||
void targetsWithoutPotential()
|
||||
{
|
||||
FakeWire a("1", "", "", {}), b("2", "", "", {}), c("3", "", "", {});
|
||||
FakeWire::join({&a, &c});
|
||||
QCOMPARE(ConductorMultiEdit::targets(Wires {&a, &b, &a}, false),
|
||||
(Wires {&a, &b}));
|
||||
}
|
||||
|
||||
// Each wire once: a selected wire already reached through
|
||||
// another one's potential is not added again
|
||||
void targetsWithPotential()
|
||||
{
|
||||
FakeWire a("1", "", "", {}), b("1", "", "", {}), c("1", "", "", {}),
|
||||
d("2", "", "", {});
|
||||
FakeWire::join({&a, &b, &c});
|
||||
const Wires t = ConductorMultiEdit::targets(Wires {&a, &d, &c}, true);
|
||||
QCOMPARE(t.size(), 4);
|
||||
QCOMPARE(QSet<FakeWire *>(t.begin(), t.end()),
|
||||
(QSet<FakeWire *> {&a, &b, &c, &d}));
|
||||
}
|
||||
|
||||
// One edit of three wires: each takes the function, keeps its
|
||||
// own number and cable; one undo restores all three exactly
|
||||
void undoRedoThreeWires()
|
||||
{
|
||||
FakeWire a("101", "fnA", "W1", {}), b("102", "fnB", "W2", {}),
|
||||
c("103", "fnC", "W3", {});
|
||||
const Wires wires {&a, &b, &c};
|
||||
const ConductorProperties before_a = a.properties(),
|
||||
before_b = b.properties(), before_c = c.properties();
|
||||
|
||||
const ConductorProperties shown = a.properties();
|
||||
QUndoStack stack;
|
||||
stack.push(ConductorMultiEdit::undo(wires, shown,
|
||||
edited(shown, &ConductorProperties::m_function, "PWR")));
|
||||
QCOMPARE(stack.count(), 1);
|
||||
|
||||
for (FakeWire *w : wires)
|
||||
QCOMPARE(w->m_properties.m_function, QStringLiteral("PWR"));
|
||||
QCOMPARE(b.m_properties.text, QStringLiteral("102"));
|
||||
QCOMPARE(c.m_properties.m_cable, QStringLiteral("W3"));
|
||||
|
||||
stack.undo();
|
||||
QVERIFY(a.properties() == before_a);
|
||||
QVERIFY(b.properties() == before_b);
|
||||
QVERIFY(c.properties() == before_c);
|
||||
|
||||
stack.redo();
|
||||
QCOMPARE(c.m_properties.m_function, QStringLiteral("PWR"));
|
||||
QCOMPARE(c.m_properties.text, QStringLiteral("103"));
|
||||
}
|
||||
|
||||
void noUndoWhenNothingChanges()
|
||||
{
|
||||
FakeWire a("1", "PWR", "", {}), b("2", "PWR", "", {});
|
||||
const ConductorProperties shown = a.properties();
|
||||
QCOMPARE(ConductorMultiEdit::undo(Wires {&a, &b}, shown, shown),
|
||||
static_cast<QUndoCommand *>(nullptr));
|
||||
// An edit every wire already has
|
||||
QCOMPARE(ConductorMultiEdit::undo(Wires {&a, &b}, edited(shown,
|
||||
&ConductorProperties::m_function, "X"), shown),
|
||||
static_cast<QUndoCommand *>(nullptr));
|
||||
}
|
||||
|
||||
// Wires that disagree on a text field show it blank; typing the
|
||||
// first wire's own value then reaches every wire (1.5 mm² on
|
||||
// a 1.5 and a 2.5 wire)
|
||||
void mixedFieldTakesFirstWiresValue()
|
||||
{
|
||||
FakeWire a("1", "fn", "", {}), b("2", "fn", "", {});
|
||||
a.m_properties.m_wire_section = "1.5";
|
||||
b.m_properties.m_wire_section = "2.5";
|
||||
const QList<ConductorProperties> list {a.properties(), b.properties()};
|
||||
|
||||
const auto mixed = ConductorMultiEdit::mixedTextFields(list);
|
||||
QCOMPARE(mixed, (QList<ConductorMultiEdit::TextField> {
|
||||
ConductorMultiEdit::Text, ConductorMultiEdit::WireSection}));
|
||||
|
||||
const ConductorProperties shown = ConductorMultiEdit::shown(list, mixed);
|
||||
QVERIFY(shown.m_wire_section.isEmpty());
|
||||
QCOMPARE(shown.m_function, QStringLiteral("fn"));
|
||||
|
||||
QUndoStack stack;
|
||||
stack.push(ConductorMultiEdit::undo(Wires {&a, &b}, shown,
|
||||
edited(shown, &ConductorProperties::m_wire_section, "1.5")));
|
||||
QCOMPARE(a.m_properties.m_wire_section, QStringLiteral("1.5"));
|
||||
QCOMPARE(b.m_properties.m_wire_section, QStringLiteral("1.5"));
|
||||
// Another field edited while the section is mixed leaves
|
||||
// each wire's section alone
|
||||
a.m_properties.m_wire_section = "1.5";
|
||||
b.m_properties.m_wire_section = "2.5";
|
||||
stack.push(ConductorMultiEdit::undo(Wires {&a, &b}, shown,
|
||||
edited(shown, &ConductorProperties::m_function, "PWR")));
|
||||
QCOMPARE(a.m_properties.m_wire_section, QStringLiteral("1.5"));
|
||||
QCOMPARE(b.m_properties.m_wire_section, QStringLiteral("2.5"));
|
||||
}
|
||||
|
||||
void oneWireIsNeverMixed()
|
||||
{
|
||||
FakeWire a("1", "fn", "", {});
|
||||
QVERIFY(ConductorMultiEdit::mixedTextFields({a.properties()}).isEmpty());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_conductormultiedit)
|
||||
#include "tst_conductormultiedit.moc"
|
||||
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include "conductorrouter.h"
|
||||
|
||||
#include <QtTest>
|
||||
|
||||
/**
|
||||
The geometry of ConductorRouter on its own: that a route is made of
|
||||
horizontal and vertical segments, leaves and enters each terminal in
|
||||
the terminal's direction, and goes around obstacles rather than
|
||||
through them.
|
||||
*/
|
||||
class TestConductorRouter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
using Direction = ConductorRouter::Direction;
|
||||
|
||||
static bool crosses(const QList<QPointF> &points, const QRectF &r)
|
||||
{
|
||||
for (int i = 0; i + 1 < points.size(); ++i) {
|
||||
const QPointF a = points.at(i), b = points.at(i + 1);
|
||||
const QRectF seg = QRectF(a, b).normalized();
|
||||
const qreal left = qMax(seg.left(), r.left()), right = qMin(seg.right(), r.right());
|
||||
const qreal top = qMax(seg.top(), r.top()), bottom = qMin(seg.bottom(), r.bottom());
|
||||
// a segment is a degenerate rectangle: it crosses the
|
||||
// interior when it overlaps it on one axis and lies
|
||||
// strictly inside it on the other
|
||||
if (seg.width() == 0 && seg.left() > r.left() && seg.left() < r.right() && bottom > top)
|
||||
return true;
|
||||
if (seg.height() == 0 && seg.top() > r.top() && seg.top() < r.bottom() && right > left)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void checkShape(const QList<QPointF> &p, const ConductorRouter::Request &r)
|
||||
{
|
||||
QVERIFY(p.size() >= 3);
|
||||
QCOMPARE(p.first(), r.start);
|
||||
QCOMPARE(p.last(), r.end);
|
||||
for (int i = 0; i + 1 < p.size(); ++i)
|
||||
QVERIFY2(p.at(i).x() == p.at(i + 1).x() || p.at(i).y() == p.at(i + 1).y(),
|
||||
"a segment is neither horizontal nor vertical");
|
||||
}
|
||||
|
||||
static QPointF unit(Direction d)
|
||||
{
|
||||
switch (d) {
|
||||
case Direction::North: return {0, -1};
|
||||
case Direction::East: return {1, 0};
|
||||
case Direction::South: return {0, 1};
|
||||
case Direction::West: return {-1, 0};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static QPointF direction(QPointF from, QPointF to)
|
||||
{
|
||||
const QPointF d = to - from;
|
||||
return {d.x() > 0 ? 1. : d.x() < 0 ? -1. : 0., d.y() > 0 ? 1. : d.y() < 0 ? -1. : 0.};
|
||||
}
|
||||
|
||||
private slots:
|
||||
void straightWhenNothingIsInTheWay()
|
||||
{
|
||||
ConductorRouter::Request r;
|
||||
r.start = {100, 100};
|
||||
r.start_direction = Direction::East;
|
||||
r.end = {300, 100};
|
||||
r.end_direction = Direction::West;
|
||||
const auto result = ConductorRouter::route(r);
|
||||
checkShape(result.points, r);
|
||||
// no bend at all: start, the two exit points, end
|
||||
for (const QPointF &p : result.points) QCOMPARE(p.y(), 100.);
|
||||
}
|
||||
|
||||
void goesAroundASymbol()
|
||||
{
|
||||
ConductorRouter::Request r;
|
||||
r.start = {100, 100};
|
||||
r.start_direction = Direction::East;
|
||||
r.end = {300, 100};
|
||||
r.end_direction = Direction::West;
|
||||
const QRectF symbol(170, 60, 60, 80);
|
||||
r.obstacles << symbol;
|
||||
const auto result = ConductorRouter::route(r);
|
||||
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
|
||||
checkShape(result.points, r);
|
||||
QVERIFY(!crosses(result.points, symbol.adjusted(-r.margin, -r.margin, r.margin, r.margin)));
|
||||
}
|
||||
|
||||
// The test that the check above can fail: a straight line through
|
||||
// the symbol is reported as crossing it.
|
||||
void crossingCheckCanFail()
|
||||
{
|
||||
QVERIFY(crosses({{100, 100}, {300, 100}}, QRectF(170, 60, 60, 80)));
|
||||
}
|
||||
|
||||
void leavesAndEntersInTheTerminalsDirections_data()
|
||||
{
|
||||
QTest::addColumn<int>("from");
|
||||
QTest::addColumn<int>("to");
|
||||
for (int a = 0; a < 4; ++a)
|
||||
for (int b = 0; b < 4; ++b)
|
||||
QTest::addRow("%d-%d", a, b) << a << b;
|
||||
}
|
||||
|
||||
void leavesAndEntersInTheTerminalsDirections()
|
||||
{
|
||||
QFETCH(int, from);
|
||||
QFETCH(int, to);
|
||||
ConductorRouter::Request r;
|
||||
r.start = {100, 100};
|
||||
r.start_direction = Direction(from);
|
||||
r.end = {250, 180};
|
||||
r.end_direction = Direction(to);
|
||||
// each terminal's own symbol, on the side opposite the way
|
||||
// the terminal points
|
||||
const QPointF s = unit(r.start_direction), e = unit(r.end_direction);
|
||||
const QRectF own_start(r.start - s * 40 - QPointF(20, 20), QSizeF(40, 40));
|
||||
const QRectF own_end(r.end - e * 40 - QPointF(20, 20), QSizeF(40, 40));
|
||||
r.obstacles << own_start << own_end;
|
||||
const auto result = ConductorRouter::route(r);
|
||||
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
|
||||
checkShape(result.points, r);
|
||||
const auto &p = result.points;
|
||||
QCOMPARE(direction(p.at(0), p.at(1)), s);
|
||||
QCOMPARE(direction(p.at(p.size() - 1), p.at(p.size() - 2)), e);
|
||||
QVERIFY(!crosses(p, own_start));
|
||||
QVERIFY(!crosses(p, own_end));
|
||||
}
|
||||
|
||||
void prefersNotToRunAlongAnotherWire()
|
||||
{
|
||||
ConductorRouter::Request r;
|
||||
r.start = {100, 100};
|
||||
r.start_direction = Direction::East;
|
||||
r.end = {300, 100};
|
||||
r.end_direction = Direction::West;
|
||||
// a wire lying exactly on the straight route
|
||||
r.wires << QVector<QPointF>{{110, 100}, {290, 100}};
|
||||
const auto result = ConductorRouter::route(r);
|
||||
checkShape(result.points, r);
|
||||
bool on_it = false;
|
||||
for (int i = 1; i + 2 < result.points.size(); ++i)
|
||||
if (result.points.at(i).y() == 100 && result.points.at(i + 1).y() == 100)
|
||||
on_it = true;
|
||||
QVERIFY(!on_it);
|
||||
}
|
||||
|
||||
void staysOnTheFolio()
|
||||
{
|
||||
ConductorRouter::Request r;
|
||||
r.start = {100, 30};
|
||||
r.start_direction = Direction::East;
|
||||
r.end = {300, 30};
|
||||
r.end_direction = Direction::West;
|
||||
r.obstacles << QRectF(170, 0, 60, 200);
|
||||
r.bounds = QRectF(0, 0, 500, 400);
|
||||
const auto result = ConductorRouter::route(r);
|
||||
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
|
||||
for (const QPointF &p : result.points) QVERIFY(r.bounds.contains(p));
|
||||
}
|
||||
|
||||
void reportsWhenThereIsNoWay()
|
||||
{
|
||||
ConductorRouter::Request r;
|
||||
r.start = {100, 100};
|
||||
r.start_direction = Direction::East;
|
||||
r.end = {300, 100};
|
||||
r.end_direction = Direction::West;
|
||||
// a wall from the top of the folio to the bottom
|
||||
r.obstacles << QRectF(170, -10, 60, 520);
|
||||
r.bounds = QRectF(0, 0, 500, 500);
|
||||
const auto result = ConductorRouter::route(r);
|
||||
QVERIFY(result.points.isEmpty());
|
||||
QVERIFY(!result.error.isEmpty());
|
||||
}
|
||||
|
||||
void terminalsOffTheGrid()
|
||||
{
|
||||
ConductorRouter::Request r;
|
||||
r.start = {103, 97};
|
||||
r.start_direction = Direction::South;
|
||||
r.end = {287, 213};
|
||||
r.end_direction = Direction::North;
|
||||
r.obstacles << QRectF(120, 140, 200, 30);
|
||||
const auto result = ConductorRouter::route(r);
|
||||
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
|
||||
checkShape(result.points, r);
|
||||
}
|
||||
|
||||
// #1178: two contacts one above the other, the upper one's bottom
|
||||
// terminal wired to the lower one's top terminal. Each docking
|
||||
// point is 10 px inside its symbol, as in con_simple.elmt. Close
|
||||
// together, the route went down through the lower contact, back
|
||||
// up past the upper one and down again.
|
||||
void closeFacingTerminalsGoStraight_data()
|
||||
{
|
||||
QTest::addColumn<int>("gap");
|
||||
for (int gap : {2, 8, 10, 18, 20, 28, 30, 38, 40, 60})
|
||||
QTest::addRow("%d px", gap) << gap;
|
||||
}
|
||||
|
||||
void closeFacingTerminalsGoStraight()
|
||||
{
|
||||
QFETCH(int, gap);
|
||||
ConductorRouter::Request r;
|
||||
r.start = {100, 120};
|
||||
r.start_direction = Direction::South;
|
||||
r.end = {100, 120. + gap};
|
||||
r.end_direction = Direction::North;
|
||||
r.start_symbol = QRectF(90, 70, 20, 60);
|
||||
r.end_symbol = QRectF(90, r.end.y() - 10, 20, 60);
|
||||
r.obstacles << r.start_symbol << r.end_symbol;
|
||||
const auto result = ConductorRouter::route(r);
|
||||
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
|
||||
checkShape(result.points, r);
|
||||
for (int i = 0; i + 1 < result.points.size(); ++i) {
|
||||
QCOMPARE(result.points.at(i).x(), 100.);
|
||||
QVERIFY2(result.points.at(i + 1).y() > result.points.at(i).y(),
|
||||
"the route turns back on itself");
|
||||
}
|
||||
}
|
||||
|
||||
// The straight line is taken only when nothing is in its way.
|
||||
void facingTerminalsGoAroundWhatIsBetweenThem()
|
||||
{
|
||||
ConductorRouter::Request r;
|
||||
r.start = {100, 120};
|
||||
r.start_direction = Direction::South;
|
||||
r.end = {100, 250};
|
||||
r.end_direction = Direction::North;
|
||||
r.start_symbol = QRectF(90, 70, 20, 60);
|
||||
r.end_symbol = QRectF(90, 240, 20, 60);
|
||||
const QRectF between(80, 170, 40, 30);
|
||||
r.obstacles << r.start_symbol << r.end_symbol << between;
|
||||
const auto result = ConductorRouter::route(r);
|
||||
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
|
||||
checkShape(result.points, r);
|
||||
QVERIFY(!crosses(result.points, between.adjusted(-r.margin, -r.margin, r.margin, r.margin)));
|
||||
}
|
||||
|
||||
// A terminal pointing straight into a symbol that is not the other
|
||||
// end: no route, rather than one that runs through that symbol.
|
||||
void noRouteThroughASymbolInFrontOfATerminal()
|
||||
{
|
||||
ConductorRouter::Request r;
|
||||
r.start = {100, 120};
|
||||
r.start_direction = Direction::South;
|
||||
r.end = {300, 300};
|
||||
r.end_direction = Direction::West;
|
||||
r.start_symbol = QRectF(90, 70, 20, 60);
|
||||
r.end_symbol = QRectF(300, 280, 40, 40);
|
||||
const QRectF in_front(60, 132, 80, 40);
|
||||
r.obstacles << r.start_symbol << r.end_symbol << in_front;
|
||||
const auto result = ConductorRouter::route(r);
|
||||
QVERIFY(result.points.isEmpty());
|
||||
QVERIFY(!result.error.isEmpty());
|
||||
}
|
||||
|
||||
// A symbol drawn around others (a cabinet made as one element):
|
||||
// the wire between two symbols inside it stays inside it, going
|
||||
// round what lies between them.
|
||||
void routesInsideAnEnclosingSymbol()
|
||||
{
|
||||
ConductorRouter::Request r;
|
||||
r.start = {100, 100};
|
||||
r.start_direction = Direction::East;
|
||||
r.end = {300, 100};
|
||||
r.end_direction = Direction::West;
|
||||
r.start_symbol = QRectF(60, 80, 40, 40);
|
||||
r.end_symbol = QRectF(300, 80, 40, 40);
|
||||
const QRectF frame(20, 20, 360, 200);
|
||||
const QRectF between(170, 60, 60, 80);
|
||||
r.obstacles << frame << r.start_symbol << r.end_symbol << between;
|
||||
r.bounds = QRectF(0, 0, 500, 400);
|
||||
const auto result = ConductorRouter::route(r);
|
||||
QVERIFY2(result.error.isEmpty(), qPrintable(result.error));
|
||||
checkShape(result.points, r);
|
||||
QVERIFY(!crosses(result.points, between.adjusted(-r.margin, -r.margin, r.margin, r.margin)));
|
||||
for (const QPointF &p : result.points)
|
||||
QVERIFY2(frame.contains(p), "the route leaves the frame");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestConductorRouter)
|
||||
#include "tst_conductorrouter.moc"
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "conductorproperties.h"
|
||||
#include "qetapp.h"
|
||||
|
||||
#include <QtTest>
|
||||
#include <QSettings>
|
||||
|
||||
// qet.cpp, compiled in for ConductorProperties, refers to this static.
|
||||
QString QETApp::m_interface_language;
|
||||
|
||||
/**
|
||||
ConductorProperties::toSettings()/fromSettings() keep the conductor
|
||||
width (cond_size) as it was saved. The width is a decimal, written with
|
||||
QString::number(); reading it with toInt() gave 0 for every value that
|
||||
was not a whole number, so a default width of 1.4 came back as 0 after
|
||||
a restart.
|
||||
|
||||
This test owns its own QSettings scope (organization + application
|
||||
name), so it cannot read or write the configuration of whoever runs it.
|
||||
*/
|
||||
class tst_conductorsizesetting : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QCoreApplication::setOrganizationName(
|
||||
QStringLiteral("QElectroTech-tst_conductorsizesetting"));
|
||||
QCoreApplication::setApplicationName(QStringLiteral("tst_conductorsizesetting"));
|
||||
QSettings settings;
|
||||
settings.clear();
|
||||
}
|
||||
|
||||
void init()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.clear();
|
||||
settings.sync();
|
||||
}
|
||||
|
||||
void widthSurvivesARoundTrip_data()
|
||||
{
|
||||
QTest::addColumn<qreal>("width");
|
||||
QTest::newRow("whole number") << 2.0;
|
||||
QTest::newRow("decimal") << 1.4;
|
||||
QTest::newRow("minimum") << 0.4;
|
||||
QTest::newRow("rounds down") << 61.3;
|
||||
QTest::newRow("rounds up") << 61.7;
|
||||
}
|
||||
|
||||
void widthSurvivesARoundTrip()
|
||||
{
|
||||
QFETCH(qreal, width);
|
||||
QSettings settings;
|
||||
ConductorProperties saved;
|
||||
saved.cond_size = width;
|
||||
saved.toSettings(settings, QStringLiteral("test/"));
|
||||
|
||||
ConductorProperties loaded;
|
||||
loaded.fromSettings(settings, QStringLiteral("test/"));
|
||||
QCOMPARE(loaded.cond_size, width);
|
||||
}
|
||||
|
||||
// A hand-edited or truncated settings file must not give a width of 0.
|
||||
void unusableValueFallsBackToOne_data()
|
||||
{
|
||||
QTest::addColumn<QString>("value");
|
||||
QTest::newRow("text") << QStringLiteral("wide");
|
||||
QTest::newRow("zero") << QStringLiteral("0");
|
||||
QTest::newRow("negative") << QStringLiteral("-1");
|
||||
QTest::newRow("nan") << QStringLiteral("nan");
|
||||
QTest::newRow("inf") << QStringLiteral("inf");
|
||||
}
|
||||
|
||||
void unusableValueFallsBackToOne()
|
||||
{
|
||||
QFETCH(QString, value);
|
||||
QSettings settings;
|
||||
settings.setValue(QStringLiteral("test/size"), value);
|
||||
|
||||
ConductorProperties loaded;
|
||||
loaded.fromSettings(settings, QStringLiteral("test/"));
|
||||
QCOMPARE(loaded.cond_size, 1.0);
|
||||
}
|
||||
|
||||
void missingValueIsOne()
|
||||
{
|
||||
QSettings settings;
|
||||
ConductorProperties loaded;
|
||||
loaded.fromSettings(settings, QStringLiteral("test/"));
|
||||
QCOMPARE(loaded.cond_size, 1.0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_conductorsizesetting)
|
||||
#include "tst_conductorsizesetting.moc"
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "ui/customizedialog.h"
|
||||
#include "ui/configpage/configpage.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QIcon>
|
||||
#include <QPushButton>
|
||||
#include <QTabWidget>
|
||||
#include <QTest>
|
||||
|
||||
/// A configuration page that counts how often it was applied
|
||||
class CountingPage : public ConfigPage
|
||||
{
|
||||
public:
|
||||
CountingPage(const QString &title) : ConfigPage(nullptr), m_title(title) {}
|
||||
void applyConf() override { ++applied; }
|
||||
QString title() const override { return m_title; }
|
||||
QIcon icon() const override { return QIcon(); }
|
||||
int applied = 0;
|
||||
private:
|
||||
QString m_title;
|
||||
};
|
||||
|
||||
/**
|
||||
CustomizeDialog: one tab per page in the order added, OK applies every
|
||||
page once, Cancel applies none.
|
||||
*/
|
||||
class tst_customizedialog : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void tabsInOrder()
|
||||
{
|
||||
CustomizeDialog dialog;
|
||||
auto *a = new CountingPage(QStringLiteral("Barres d'outils"));
|
||||
auto *b = new CountingPage(QStringLiteral("Clavier"));
|
||||
dialog.addPage(a);
|
||||
dialog.addPage(b);
|
||||
dialog.addPage(a); // twice: ignored
|
||||
dialog.addPage(nullptr);
|
||||
auto *tabs = dialog.findChild<QTabWidget *>(QStringLiteral("customizeTabs"));
|
||||
QVERIFY(tabs);
|
||||
QCOMPARE(tabs->count(), 2);
|
||||
QCOMPARE(tabs->tabText(0), QStringLiteral("Barres d'outils"));
|
||||
QCOMPARE(tabs->tabText(1), QStringLiteral("Clavier"));
|
||||
QCOMPARE(tabs->widget(1), b);
|
||||
}
|
||||
|
||||
void okAppliesEveryPage()
|
||||
{
|
||||
CustomizeDialog dialog;
|
||||
auto *a = new CountingPage(QStringLiteral("a"));
|
||||
auto *b = new CountingPage(QStringLiteral("b"));
|
||||
dialog.addPage(a);
|
||||
dialog.addPage(b);
|
||||
dialog.show();
|
||||
dialog.findChild<QDialogButtonBox *>()->button(QDialogButtonBox::Ok)->click();
|
||||
QCOMPARE(a->applied, 1);
|
||||
QCOMPARE(b->applied, 1);
|
||||
QCOMPARE(dialog.result(), int(QDialog::Accepted));
|
||||
}
|
||||
|
||||
void cancelAppliesNothing()
|
||||
{
|
||||
CustomizeDialog dialog;
|
||||
auto *a = new CountingPage(QStringLiteral("a"));
|
||||
dialog.addPage(a);
|
||||
dialog.show();
|
||||
dialog.findChild<QDialogButtonBox *>()->button(QDialogButtonBox::Cancel)->click();
|
||||
QCOMPARE(a->applied, 0);
|
||||
QCOMPARE(dialog.result(), int(QDialog::Rejected));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_customizedialog)
|
||||
#include "tst_customizedialog.moc"
|
||||
@@ -0,0 +1,129 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
#include <QGraphicsRectItem>
|
||||
#include <QGraphicsScene>
|
||||
#include "darkimagerendering.h"
|
||||
#include "palettegraphicsview.h"
|
||||
#include "qetpalette.h"
|
||||
|
||||
class Raster : public QGraphicsItem {
|
||||
public:
|
||||
QPixmap pixmap;
|
||||
bool adapt = false;
|
||||
explicit Raster(QGraphicsItem *parent = nullptr) : QGraphicsItem(parent) {
|
||||
QImage image(40, 30, QImage::Format_ARGB32);
|
||||
image.fill(QColor(197, 81, 33));
|
||||
for (int y = 0; y < 30; ++y)
|
||||
for (int x = 10; x < 20; ++x) image.setPixelColor(x, y, QColor(197,81,33,128));
|
||||
for (int y = 0; y < 30; ++y)
|
||||
for (int x = 30; x < 40; ++x) image.setPixelColor(x, y, Qt::transparent);
|
||||
pixmap = QPixmap::fromImage(image);
|
||||
}
|
||||
QRectF boundingRect() const override { return QRectF(0,0,40,40); }
|
||||
void paint(QPainter *p, const QStyleOptionGraphicsItem *, QWidget *) override {
|
||||
DarkImageRendering::paintPixmap(p, pixmap, adapt);
|
||||
p->fillRect(QRectF(0,34,8,4), Qt::black); // caption/decorations remain adapted
|
||||
}
|
||||
};
|
||||
|
||||
class tst_darkimagelayers : public QObject {
|
||||
Q_OBJECT
|
||||
static QColor sample(const QImage &image, const PaletteGraphicsView &view, QPointF scene) {
|
||||
const QPoint point = view.mapFromScene(scene);
|
||||
const qreal dpr = image.devicePixelRatio();
|
||||
return image.pixelColor(qRound(point.x()*dpr), qRound(point.y()*dpr));
|
||||
}
|
||||
static void setup(PaletteGraphicsView &view) {
|
||||
view.setFrameShape(QFrame::NoFrame);
|
||||
view.setAlignment(Qt::AlignLeft | Qt::AlignTop);
|
||||
view.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
view.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
view.resize(100,80); view.show();
|
||||
}
|
||||
private slots:
|
||||
void cleanup() {
|
||||
QApplication::setPalette(QET::Palette::fusionLight());
|
||||
PaletteGraphicsView::setCustomBackgroundColor(false);
|
||||
}
|
||||
void modes_data() {
|
||||
QTest::addColumn<bool>("dark"); QTest::addColumn<bool>("adapt"); QTest::addColumn<bool>("custom");
|
||||
QTest::newRow("light-original") << false << false << false;
|
||||
QTest::newRow("light-adapt") << false << true << false;
|
||||
QTest::newRow("dark-original") << true << false << false;
|
||||
QTest::newRow("dark-adapt") << true << true << false;
|
||||
QTest::newRow("light-custom-original") << false << false << true;
|
||||
QTest::newRow("light-custom-adapt") << false << true << true;
|
||||
QTest::newRow("dark-custom-original") << true << false << true;
|
||||
QTest::newRow("dark-custom-adapt") << true << true << true;
|
||||
}
|
||||
void modes() {
|
||||
QFETCH(bool,dark); QFETCH(bool,adapt); QFETCH(bool,custom);
|
||||
PaletteGraphicsView::setCustomBackgroundColor(custom);
|
||||
const QPalette palette = dark ? QET::Palette::fusionDark() : QET::Palette::fusionLight();
|
||||
QApplication::setPalette(palette);
|
||||
const QColor background = custom ? QColor(80,90,100) : QColor(Qt::white);
|
||||
QGraphicsScene scene(0,0,100,80); scene.setBackgroundBrush(background);
|
||||
auto *raster = new Raster; raster->adapt = adapt; raster->setPos(10,10); scene.addItem(raster);
|
||||
scene.addRect(32,10,5,20,QPen(Qt::NoPen),QBrush(Qt::black))->setZValue(1);
|
||||
PaletteGraphicsView view(&scene); setup(view);
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
const QImage shown = view.viewport()->grab().toImage();
|
||||
QImage expected(100,80,QImage::Format_RGB32);
|
||||
expected.fill(custom ? background : (dark && !adapt ? palette.color(QPalette::Base) : QColor(Qt::white)));
|
||||
QPainter p(&expected);
|
||||
if (dark && custom && adapt) {
|
||||
QImage adapted = raster->pixmap.toImage();
|
||||
QET::Palette::invertLightnessLayer(adapted,palette.color(QPalette::Base),palette.color(QPalette::Text));
|
||||
p.drawImage(QPoint(10,10),adapted);
|
||||
} else p.drawPixmap(QPoint(10,10),raster->pixmap);
|
||||
p.fillRect(QRect(32,10,5,20), dark && !adapt && !custom ? palette.color(QPalette::Text) : QColor(Qt::black));
|
||||
p.end();
|
||||
if (dark && adapt && !custom) QET::Palette::invertLightness(expected,palette.color(QPalette::Base),palette.color(QPalette::Text));
|
||||
for (const QPoint point : {QPoint(15,15),QPoint(25,15),QPoint(45,15),QPoint(34,15)})
|
||||
QCOMPARE(sample(shown,view,point), expected.pixelColor(point));
|
||||
QCOMPARE(sample(shown,view,QPoint(13,45)), dark && !custom ? palette.color(QPalette::Text) : QColor(Qt::black));
|
||||
}
|
||||
void parentClippingRotationOpacityAndOverlap() {
|
||||
const auto palette = QET::Palette::fusionDark(); QApplication::setPalette(palette);
|
||||
QGraphicsScene scene(0,0,100,80); scene.setBackgroundBrush(Qt::white);
|
||||
auto *parent = scene.addRect(0,0,25,40,QPen(Qt::NoPen),QBrush(Qt::NoBrush));
|
||||
parent->setFlag(QGraphicsItem::ItemClipsChildrenToShape); parent->setPos(35,15); parent->setRotation(12);
|
||||
auto *first = new Raster(parent); first->setOpacity(0.5);
|
||||
auto *second = new Raster; second->setPos(36,30); scene.addItem(second); second->setZValue(2);
|
||||
QImage blue(40,30,QImage::Format_ARGB32); blue.fill(QColor(20,80,210,128)); second->pixmap=QPixmap::fromImage(blue);
|
||||
PaletteGraphicsView view(&scene); setup(view); QVERIFY(QTest::qWaitForWindowExposed(&view));
|
||||
const auto shown = view.viewport()->grab().toImage();
|
||||
// Original pixels blend once over the dark sheet, then the upper
|
||||
// original image blends once over those pixels. No inverse transform.
|
||||
QImage reference(1,1,QImage::Format_RGB32); reference.fill(palette.color(QPalette::Base));
|
||||
QPainter p(&reference); p.setOpacity(0.5); p.fillRect(reference.rect(),QColor(197,81,33)); p.end();
|
||||
QCOMPARE(sample(shown,view,first->mapToScene(QPointF(5,5))),reference.pixelColor(0,0));
|
||||
QPainter upper(&reference); upper.fillRect(reference.rect(),QColor(20,80,210,128)); upper.end();
|
||||
QCOMPARE(sample(shown,view,first->mapToScene(QPointF(5,20))),reference.pixelColor(0,0));
|
||||
// Pixel in the child's transparent stripe / beyond the parent's clip.
|
||||
QCOMPARE(sample(shown,view,first->mapToScene(QPointF(28,5))),palette.color(QPalette::Base));
|
||||
first->setPos(3,0); scene.update(); QApplication::processEvents();
|
||||
QVERIFY(!view.viewport()->grab().isNull());
|
||||
}
|
||||
void alphaMapping() {
|
||||
QImage image(2,1,QImage::Format_ARGB32_Premultiplied);
|
||||
image.setPixelColor(0,0,QColor(0,0,0,128)); image.setPixelColor(1,0,Qt::transparent);
|
||||
QET::Palette::invertLightnessLayer(image,QColor(30,30,30),QColor(220,220,220));
|
||||
QCOMPARE(image.pixelColor(0,0),QColor(220,220,220,128));
|
||||
QCOMPARE(image.pixelColor(1,0).alpha(),0);
|
||||
}
|
||||
void exportUnaffected() {
|
||||
QApplication::setPalette(QET::Palette::fusionDark());
|
||||
QGraphicsScene scene(0,0,100,80); scene.setBackgroundBrush(Qt::white);
|
||||
auto *raster=new Raster; raster->setPos(10,10); scene.addItem(raster);
|
||||
QImage results[2];
|
||||
for(int i=0;i<2;++i) {
|
||||
raster->adapt=i; results[i]=QImage(100,80,QImage::Format_RGB32); results[i].fill(Qt::white);
|
||||
QPainter p(&results[i]); scene.render(&p,QRectF(0,0,100,80),scene.sceneRect());
|
||||
}
|
||||
QCOMPARE(results[0],results[1]);
|
||||
QCOMPARE(results[0].pixelColor(15,15),QColor(197,81,33));
|
||||
}
|
||||
};
|
||||
QTEST_MAIN(tst_darkimagelayers)
|
||||
#include "tst_darkimagelayers.moc"
|
||||
@@ -65,6 +65,24 @@ private slots:
|
||||
QCOMPARE(fromDom(xml(value)), expected);
|
||||
QCOMPARE(fromPugi(xml(value)), expected);
|
||||
}
|
||||
|
||||
// remove() forgets the key entirely: a context with a key added
|
||||
// then removed equals one that never had it. remove() used to keep
|
||||
// the key's show flag, so the two never compared equal, and
|
||||
// Project properties > OK saw a change where there was none.
|
||||
void removeForgetsTheKey()
|
||||
{
|
||||
DiagramContext edited;
|
||||
edited.addValue(QStringLiteral("author"), QStringLiteral("N.V."));
|
||||
edited.addValue(QStringLiteral("01-numprojet"), QString());
|
||||
edited.remove(QStringLiteral("01-numprojet"));
|
||||
|
||||
DiagramContext stored;
|
||||
stored.addValue(QStringLiteral("author"), QStringLiteral("N.V."));
|
||||
|
||||
QCOMPARE(edited.keys(), stored.keys());
|
||||
QVERIFY(edited == stored);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_diagramcontext)
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "diagramtoolbarsettings.h"
|
||||
#include "shortcutmanager.h"
|
||||
#include "ui/configpage/toolbarcommandsconfigpage.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QComboBox>
|
||||
#include <QListWidget>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <QTest>
|
||||
#include <QToolBar>
|
||||
#include <QWidget>
|
||||
|
||||
/**
|
||||
A top-level window standing in for the diagram editor: counts the
|
||||
calls applyToAll() makes.
|
||||
*/
|
||||
class RebuildCounter : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
int rebuilds = 0;
|
||||
public slots:
|
||||
void rebuildToolBars() { ++rebuilds; }
|
||||
};
|
||||
|
||||
/**
|
||||
DiagramToolbarSettings and its configuration page: an unchanged
|
||||
toolbar stores nothing and is built as before, unknown ids and stray
|
||||
separators are skipped, the user's own toolbars are kept and removed
|
||||
with their contents, and the page saves what it shows.
|
||||
*/
|
||||
class tst_diagramtoolbarsettings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QObject m_owner;
|
||||
QHash<QString, QAction *> m_actions;
|
||||
|
||||
/// The action for @a id, made on first use, as the editor's resolver
|
||||
QAction *resolve(const QString &id)
|
||||
{
|
||||
if (id.startsWith(QLatin1String("missing."))) return nullptr;
|
||||
if (!m_actions.contains(id)) {
|
||||
auto *action = new QAction(id, &m_owner);
|
||||
m_actions.insert(id, action);
|
||||
}
|
||||
return m_actions.value(id);
|
||||
}
|
||||
|
||||
static QStringList shape(QToolBar *toolbar)
|
||||
{
|
||||
QStringList list;
|
||||
for (QAction *action : toolbar->actions()) {
|
||||
list << (action->isSeparator() ? QStringLiteral("|") : action->text());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
static void clearSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.remove(QStringLiteral("diagrameditor/toolbars"));
|
||||
settings.remove(QStringLiteral("diagrameditor/custom_toolbars"));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
clearSettings();
|
||||
//A few of the diagram editor's commands, as it registers them
|
||||
for (const QString &id : {QStringLiteral("diagrameditor.copy"),
|
||||
QStringLiteral("diagrameditor.paste"),
|
||||
QStringLiteral("diagrameditor.zoom_fit"),
|
||||
QStringLiteral("depth.raise"),
|
||||
QStringLiteral("elementeditor.copy")}) {
|
||||
ShortcutManager::instance().registerAction(
|
||||
new QAction(id, &m_owner), id, QStringLiteral("test"), QKeySequence());
|
||||
}
|
||||
}
|
||||
|
||||
void init() { clearSettings(); }
|
||||
|
||||
void defaultsStoreNothing()
|
||||
{
|
||||
for (const QString &name : DiagramToolbarSettings::builtInNames()) {
|
||||
QVERIFY(!DiagramToolbarSettings::defaultIds(name).isEmpty());
|
||||
QCOMPARE(DiagramToolbarSettings::ids(name), DiagramToolbarSettings::defaultIds(name));
|
||||
DiagramToolbarSettings::setIds(name, DiagramToolbarSettings::defaultIds(name));
|
||||
}
|
||||
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/toolbars/toolbar")));
|
||||
|
||||
DiagramToolbarSettings::setIds(QStringLiteral("toolbar"), {QStringLiteral("diagrameditor.copy")});
|
||||
QCOMPARE(DiagramToolbarSettings::ids(QStringLiteral("toolbar")),
|
||||
QStringList{QStringLiteral("diagrameditor.copy")});
|
||||
//Saving an empty toolbar keeps it empty, not back to the defaults
|
||||
DiagramToolbarSettings::setIds(QStringLiteral("toolbar"), {});
|
||||
QVERIFY(DiagramToolbarSettings::ids(QStringLiteral("toolbar")).isEmpty());
|
||||
}
|
||||
|
||||
/// The defaults are built exactly as the toolbars were hand-built
|
||||
/// before: the main toolbar's three separators in place
|
||||
void defaultsBuildAsBefore()
|
||||
{
|
||||
QToolBar toolbar;
|
||||
DiagramToolbarSettings::fill(&toolbar, DiagramToolbarSettings::defaultIds(QStringLiteral("toolbar")),
|
||||
[this](const QString &id) { return resolve(id); });
|
||||
QCOMPARE(toolbar.actions().size(), 18);
|
||||
QCOMPARE(shape(&toolbar).mid(6, 2),
|
||||
(QStringList{QStringLiteral("diagrameditor.export_to_pdf"), QStringLiteral("|")}));
|
||||
QCOMPARE(shape(&toolbar).count(QStringLiteral("|")), 3);
|
||||
}
|
||||
|
||||
void fillSkipsUnknownAndStraySeparators()
|
||||
{
|
||||
const QString sep = DiagramToolbarSettings::separatorId();
|
||||
QToolBar toolbar;
|
||||
toolbar.addAction(QStringLiteral("left over"));
|
||||
DiagramToolbarSettings::fill(&toolbar,
|
||||
{sep, QStringLiteral("a"), sep, sep, QStringLiteral("missing.b"),
|
||||
QStringLiteral("c"), sep, QStringLiteral("missing.d"), sep},
|
||||
[this](const QString &id) { return resolve(id); });
|
||||
QCOMPARE(shape(&toolbar), (QStringList{QStringLiteral("a"), QStringLiteral("|"), QStringLiteral("c")}));
|
||||
|
||||
DiagramToolbarSettings::fill(&toolbar, {}, [this](const QString &id) { return resolve(id); });
|
||||
QVERIFY(toolbar.actions().isEmpty());
|
||||
}
|
||||
|
||||
void customToolbarsKeptAndRemoved()
|
||||
{
|
||||
using T = DiagramToolbarSettings::Toolbar;
|
||||
const QString first = DiagramToolbarSettings::newCustomName({});
|
||||
QCOMPARE(first, QStringLiteral("custom_1"));
|
||||
DiagramToolbarSettings::setCustomToolbars({T{first, QStringLiteral("Mine"), true},
|
||||
T{QStringLiteral("custom_2"), QStringLiteral("Other"), true}});
|
||||
DiagramToolbarSettings::setIds(first, {QStringLiteral("diagrameditor.copy")});
|
||||
QCOMPARE(DiagramToolbarSettings::customToolbars().size(), 2);
|
||||
QCOMPARE(DiagramToolbarSettings::customToolbars().first().title, QStringLiteral("Mine"));
|
||||
QCOMPARE(DiagramToolbarSettings::toolbars().size(),
|
||||
DiagramToolbarSettings::builtInNames().size() + 2);
|
||||
QCOMPARE(DiagramToolbarSettings::newCustomName({}), QStringLiteral("custom_3"));
|
||||
//A new toolbar starts empty
|
||||
QVERIFY(DiagramToolbarSettings::ids(QStringLiteral("custom_2")).isEmpty());
|
||||
|
||||
DiagramToolbarSettings::setCustomToolbars({T{QStringLiteral("custom_2"), QStringLiteral("Other"), true}});
|
||||
QCOMPARE(DiagramToolbarSettings::customToolbars().size(), 1);
|
||||
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/toolbars/") + first));
|
||||
|
||||
DiagramToolbarSettings::setCustomToolbars({});
|
||||
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/custom_toolbars/names")));
|
||||
}
|
||||
|
||||
void availableCommandsAreTheDiagramEditors()
|
||||
{
|
||||
const QStringList ids = DiagramToolbarSettings::availableCommandIds();
|
||||
QVERIFY(ids.contains(QStringLiteral("diagrameditor.copy")));
|
||||
QVERIFY(ids.contains(QStringLiteral("depth.raise")));
|
||||
QVERIFY(!ids.contains(QStringLiteral("elementeditor.copy")));
|
||||
}
|
||||
|
||||
void pageSavesAndRebuilds()
|
||||
{
|
||||
RebuildCounter marked, unmarked;
|
||||
DiagramToolbarSettings::markWindow(&marked);
|
||||
|
||||
ToolbarCommandsConfigPage page;
|
||||
auto *combo = page.findChild<QComboBox *>(QStringLiteral("toolbarCombo"));
|
||||
auto *available = page.findChild<QListWidget *>(QStringLiteral("availableList"));
|
||||
auto *chosen = page.findChild<QListWidget *>(QStringLiteral("chosenList"));
|
||||
QVERIFY(combo && available && chosen);
|
||||
QCOMPARE(combo->count(), DiagramToolbarSettings::builtInNames().size());
|
||||
QCOMPARE(chosen->count(), DiagramToolbarSettings::defaultIds(QStringLiteral("toolbar")).size());
|
||||
|
||||
//OK without a change stores nothing
|
||||
page.applyConf();
|
||||
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/toolbars/toolbar")));
|
||||
QCOMPARE(marked.rebuilds, 1);
|
||||
QCOMPARE(unmarked.rebuilds, 0);
|
||||
|
||||
//Remove everything but the first command, add a separator
|
||||
while (chosen->count() > 1) delete chosen->takeItem(1);
|
||||
chosen->setCurrentRow(0);
|
||||
page.findChild<QPushButton *>(QStringLiteral("separatorButton"))->click();
|
||||
page.applyConf();
|
||||
QCOMPARE(DiagramToolbarSettings::ids(QStringLiteral("toolbar")),
|
||||
(QStringList{QStringLiteral("diagrameditor.new_file"), DiagramToolbarSettings::separatorId()}));
|
||||
QCOMPARE(marked.rebuilds, 2);
|
||||
}
|
||||
|
||||
/// A widget button can be in one place only: once moved to another
|
||||
/// toolbar it is no longer offered, and resetting its own toolbar
|
||||
/// takes it back
|
||||
void widgetInOnePlace()
|
||||
{
|
||||
ToolbarCommandsConfigPage page;
|
||||
auto *combo = page.findChild<QComboBox *>(QStringLiteral("toolbarCombo"));
|
||||
auto *available = page.findChild<QListWidget *>(QStringLiteral("availableList"));
|
||||
auto *chosen = page.findChild<QListWidget *>(QStringLiteral("chosenList"));
|
||||
auto offered = [available](const QString &id) {
|
||||
for (int i = 0 ; i < available->count() ; ++i)
|
||||
if (available->item(i)->data(Qt::UserRole).toString() == id) return true;
|
||||
return false;
|
||||
};
|
||||
const QString colour = QStringLiteral("widget:conductor_color");
|
||||
|
||||
//On "diagram" by default, so not offered for "toolbar"
|
||||
QVERIFY(!offered(colour));
|
||||
//Taken off "diagram" (switching toolbars keeps the change)
|
||||
combo->setCurrentIndex(combo->findData(QStringLiteral("diagram")));
|
||||
for (int i = chosen->count() - 1 ; i >= 0 ; --i) {
|
||||
if (chosen->item(i)->data(Qt::UserRole).toString() == colour) delete chosen->takeItem(i);
|
||||
}
|
||||
combo->setCurrentIndex(combo->findData(QStringLiteral("toolbar")));
|
||||
QVERIFY(offered(colour));
|
||||
|
||||
//Reset "diagram": the colour button goes back there
|
||||
combo->setCurrentIndex(combo->findData(QStringLiteral("diagram")));
|
||||
page.findChild<QPushButton *>(QStringLiteral("resetButton"))->click();
|
||||
QCOMPARE(chosen->count(), DiagramToolbarSettings::defaultIds(QStringLiteral("diagram")).size());
|
||||
combo->setCurrentIndex(combo->findData(QStringLiteral("toolbar")));
|
||||
QVERIFY(!offered(colour));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_diagramtoolbarsettings)
|
||||
#include "tst_diagramtoolbarsettings.moc"
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QProcess>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
/**
|
||||
--export-dxf --dxf-blocks (issue #1339): a symbol placed twice is one
|
||||
BLOCK and two INSERTs, the second turned a quarter, and each symbol's
|
||||
information is a hidden attribute of it; with --dxf-attributes its
|
||||
label is a visible one. Without the switches there is neither.
|
||||
*/
|
||||
class tst_dxfblocks : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/// The group code / value pairs of the exported folio
|
||||
static QList<QPair<QString, QString>> exportPairs(const QStringList &extra)
|
||||
{
|
||||
const QString project = QFINDTESTDATA("fixtures/dxf_blocks.qet");
|
||||
QTemporaryDir dir;
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
QStringList{QStringLiteral("--export-dxf"), project, dir.path()} + extra);
|
||||
if (!proc.waitForFinished(60000) || proc.exitCode() != 0)
|
||||
return {};
|
||||
QFile file(dir.filePath(QStringLiteral("01_diagram.dxf")));
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
return {};
|
||||
const QStringList values = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));
|
||||
QList<QPair<QString, QString>> pairs;
|
||||
for (int i = 0 ; i + 1 < values.size() ; i += 2)
|
||||
pairs << qMakePair(values.at(i).trimmed(), values.at(i + 1).trimmed());
|
||||
return pairs;
|
||||
}
|
||||
|
||||
/// The values of code @a code in each entity of type @a type
|
||||
static QStringList values(const QList<QPair<QString, QString>> &pairs,
|
||||
const QString &type, const QString &code)
|
||||
{
|
||||
QStringList found;
|
||||
for (int i = 0 ; i < pairs.size() ; ++i) {
|
||||
if (pairs.at(i).first != QLatin1String("0") || pairs.at(i).second != type)
|
||||
continue;
|
||||
for (int j = i + 1 ; j < pairs.size() && pairs.at(j).first != QLatin1String("0") ; ++j)
|
||||
if (pairs.at(j).first == code)
|
||||
found << pairs.at(j).second;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void blocks()
|
||||
{
|
||||
const auto pairs = exportPairs({QStringLiteral("--dxf-blocks")});
|
||||
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-blocks failed");
|
||||
|
||||
QCOMPARE(values(pairs, QStringLiteral("BLOCK"), QStringLiteral("2")),
|
||||
QStringList{QStringLiteral("QET_CURVES")});
|
||||
QCOMPARE(values(pairs, QStringLiteral("INSERT"), QStringLiteral("2")),
|
||||
(QStringList{QStringLiteral("QET_CURVES"), QStringLiteral("QET_CURVES")}));
|
||||
QCOMPARE(values(pairs, QStringLiteral("INSERT"), QStringLiteral("50")),
|
||||
(QStringList{QStringLiteral("0"), QStringLiteral("270")}));
|
||||
//The circle is drawn once, inside the block
|
||||
QCOMPARE(values(pairs, QStringLiteral("CIRCLE"), QStringLiteral("8")).size(), 1);
|
||||
}
|
||||
|
||||
void labelsAsAttributes()
|
||||
{
|
||||
const auto pairs = exportPairs({QStringLiteral("--dxf-attributes")});
|
||||
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-attributes failed");
|
||||
|
||||
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("2"))
|
||||
.count(QStringLiteral("LABEL")), 2);
|
||||
const QStringList texts = values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("1"));
|
||||
QVERIFY(texts.contains(QStringLiteral("K1")));
|
||||
QVERIFY(texts.contains(QStringLiteral("K2")));
|
||||
//Defined once in the block, and no longer a loose text
|
||||
QCOMPARE(values(pairs, QStringLiteral("ATTDEF"), QStringLiteral("2"))
|
||||
.count(QStringLiteral("LABEL")), 1);
|
||||
QVERIFY(!values(pairs, QStringLiteral("TEXT"), QStringLiteral("1")).contains(QStringLiteral("K1")));
|
||||
}
|
||||
|
||||
void labelsStayTextsWithBlocksOnly()
|
||||
{
|
||||
const auto pairs = exportPairs({QStringLiteral("--dxf-blocks")});
|
||||
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-blocks failed");
|
||||
//Only hidden attributes (70 = 1); the labels are texts
|
||||
QVERIFY(!values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("70")).contains(QStringLiteral("0")));
|
||||
QVERIFY(values(pairs, QStringLiteral("TEXT"), QStringLiteral("1")).contains(QStringLiteral("K1")));
|
||||
}
|
||||
|
||||
void dataAsHiddenAttributes()
|
||||
{
|
||||
//The label is a text, so it is part data too: K1's label and
|
||||
//manufacturer, K2's label, all hidden (70 = 1)
|
||||
auto pairs = exportPairs({QStringLiteral("--dxf-blocks")});
|
||||
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-blocks failed");
|
||||
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("2")),
|
||||
(QStringList{QStringLiteral("LABEL"), QStringLiteral("MANUFACTURER"),
|
||||
QStringLiteral("LABEL")}));
|
||||
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("70")),
|
||||
(QStringList{QStringLiteral("1"), QStringLiteral("1"), QStringLiteral("1")}));
|
||||
QVERIFY(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("1"))
|
||||
.contains(QStringLiteral("Schneider")));
|
||||
|
||||
//With the labels as attributes, the label is not repeated
|
||||
pairs = exportPairs({QStringLiteral("--dxf-attributes")});
|
||||
QVERIFY2(!pairs.isEmpty(), "--export-dxf --dxf-attributes failed");
|
||||
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("2")),
|
||||
(QStringList{QStringLiteral("LABEL"), QStringLiteral("MANUFACTURER"),
|
||||
QStringLiteral("LABEL")}));
|
||||
QCOMPARE(values(pairs, QStringLiteral("ATTRIB"), QStringLiteral("70")),
|
||||
(QStringList{QStringLiteral("0"), QStringLiteral("1"), QStringLiteral("0")}));
|
||||
}
|
||||
|
||||
void flatWithoutSwitch()
|
||||
{
|
||||
const auto pairs = exportPairs({});
|
||||
QVERIFY2(!pairs.isEmpty(), "--export-dxf failed");
|
||||
QVERIFY(values(pairs, QStringLiteral("BLOCK"), QStringLiteral("2")).isEmpty());
|
||||
QVERIFY(values(pairs, QStringLiteral("INSERT"), QStringLiteral("2")).isEmpty());
|
||||
QCOMPARE(values(pairs, QStringLiteral("CIRCLE"), QStringLiteral("8")).size(), 2);
|
||||
QVERIFY(values(pairs, QStringLiteral("TEXT"), QStringLiteral("1")).contains(QStringLiteral("K1")));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_dxfblocks)
|
||||
#include "tst_dxfblocks.moc"
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QProcess>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
/**
|
||||
--export-dxf on a folio whose title block is at the right (issue #1339):
|
||||
a symbol's circle is one CIRCLE and its arc one ARC with the symbol's own
|
||||
angles, which needs the same scale on both axes; the title block is
|
||||
turned a quarter, as it is on screen.
|
||||
*/
|
||||
class tst_dxfcurves : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
/// The entities on @a layer, each as its group codes and values.
|
||||
static QList<QMultiHash<int, QString>> entities(const QStringList &values,
|
||||
const QString &type,
|
||||
const QString &layer)
|
||||
{
|
||||
QList<QMultiHash<int, QString>> found;
|
||||
for (int i = 0 ; i + 1 < values.size() ; i += 2)
|
||||
{
|
||||
if (values.at(i).trimmed() != QLatin1String("0")
|
||||
|| values.at(i + 1).trimmed() != type)
|
||||
continue;
|
||||
QMultiHash<int, QString> entity;
|
||||
for (int j = i + 2 ; j + 1 < values.size()
|
||||
&& values.at(j).trimmed() != QLatin1String("0") ; j += 2)
|
||||
entity.insert(values.at(j).trimmed().toInt(), values.at(j + 1).trimmed());
|
||||
if (entity.value(8) == layer)
|
||||
found << entity;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void curvesAndTitleBlock()
|
||||
{
|
||||
const QString project = QFINDTESTDATA("fixtures/dxf_curves.qet");
|
||||
QVERIFY2(!project.isEmpty(), "fixture project not found");
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--export-dxf"), project, dir.path()});
|
||||
QVERIFY2(proc.waitForFinished(60000), "--export-dxf timed out");
|
||||
QCOMPARE(proc.exitCode(), 0);
|
||||
|
||||
QFile file(dir.filePath(QStringLiteral("01_diagram.dxf")));
|
||||
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
|
||||
const QStringList values = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));
|
||||
const QString symbols = QStringLiteral("QET_SYMBOLS");
|
||||
|
||||
//The 20 px circle: one CIRCLE, nothing approximated
|
||||
const auto circles = entities(values, QStringLiteral("CIRCLE"), symbols);
|
||||
QCOMPARE(circles.size(), 1);
|
||||
QVERIFY(entities(values, QStringLiteral("POLYLINE"), symbols).isEmpty());
|
||||
|
||||
//The arc from 30 to 150 degrees, same radius as the circle
|
||||
const auto arcs = entities(values, QStringLiteral("ARC"), symbols);
|
||||
QCOMPARE(arcs.size(), 1);
|
||||
QCOMPARE(arcs.first().value(50).toDouble(), 30.0);
|
||||
QCOMPARE(arcs.first().value(51).toDouble(), 150.0);
|
||||
QCOMPARE(arcs.first().value(40).toDouble(), circles.first().value(40).toDouble());
|
||||
|
||||
//Every title block text turned a quarter, as drawn on screen
|
||||
const auto texts = entities(values, QStringLiteral("TEXT"), QStringLiteral("QET_TITLEBLOCK"));
|
||||
QVERIFY(!texts.isEmpty());
|
||||
for (const auto &text : texts)
|
||||
QCOMPARE(text.value(50).toDouble(), 90.0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_dxfcurves)
|
||||
#include "tst_dxfcurves.moc"
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QProcess>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
/**
|
||||
--export-dxf writes a symbol text wrapped to its width line by line,
|
||||
as it is drawn, rather than as one long line. A text without a width
|
||||
is still written on one line.
|
||||
*/
|
||||
class tst_dxfwrappedtext : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void wrappedLines()
|
||||
{
|
||||
const QString project = QFINDTESTDATA("fixtures/dxf_wrapped_text.qet");
|
||||
QVERIFY2(!project.isEmpty(), "fixture project not found");
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--export-dxf"), project, dir.path()});
|
||||
QVERIFY2(proc.waitForFinished(60000), "--export-dxf timed out");
|
||||
QCOMPARE(proc.exitCode(), 0);
|
||||
|
||||
QFile file(dir.filePath(QStringLiteral("01_diagram.dxf")));
|
||||
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
|
||||
const QStringList values = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));
|
||||
|
||||
//One text entity per word of the text 70 px wide...
|
||||
for (const QString &word : {QStringLiteral("WrapAlpha"), QStringLiteral("WrapGamma"), QStringLiteral("WrapEpsilon")})
|
||||
QVERIFY2(values.contains(word), qPrintable(word));
|
||||
//...one for the whole text without a width
|
||||
QVERIFY(values.contains(QStringLiteral("LineAlpha LineBeta LineGamma LineDelta LineEpsilon")));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_dxfwrappedtext)
|
||||
#include "tst_dxfwrappedtext.moc"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include "editor/elementviewgrid.h"
|
||||
|
||||
// The symbol editor's snap step at each zoom (bugtracker #112): below
|
||||
// 100 % the grid is hidden but parts must still snap every 10 units.
|
||||
class tst_elementviewgrid : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void stepAtZoom_data()
|
||||
{
|
||||
QTest::addColumn<qreal>("zoom");
|
||||
QTest::addColumn<int>("step");
|
||||
QTest::addColumn<bool>("drawGrid");
|
||||
QTest::addColumn<bool>("drawCross");
|
||||
|
||||
QTest::newRow("10 % (fit to a big symbol)") << 0.10 << 10 << false << false;
|
||||
QTest::newRow("62.5 %, the #112 case") << 0.625 << 10 << false << false;
|
||||
QTest::newRow("100 %") << 1.0 << 10 << true << false;
|
||||
QTest::newRow("200 %") << 2.0 << 10 << true << false;
|
||||
QTest::newRow("400 %") << 4.0 << 5 << true << true;
|
||||
QTest::newRow("800 %") << 8.0 << 2 << true << true;
|
||||
QTest::newRow("1000 %") << 10.0 << 1 << true << true;
|
||||
|
||||
// Just below each threshold: the comparisons are strict (<), so a
|
||||
// change to <= would move these rows into the next band.
|
||||
QTest::newRow("99.9 %") << 0.999 << 10 << false << false;
|
||||
QTest::newRow("399.9 %") << 3.999 << 10 << true << false;
|
||||
QTest::newRow("401 %") << 4.01 << 5 << true << true;
|
||||
QTest::newRow("799.9 %") << 7.999 << 5 << true << true;
|
||||
QTest::newRow("999.9 %") << 9.999 << 2 << true << true;
|
||||
}
|
||||
|
||||
void stepAtZoom()
|
||||
{
|
||||
QFETCH(qreal, zoom);
|
||||
QFETCH(int, step);
|
||||
QFETCH(bool, drawGrid);
|
||||
QFETCH(bool, drawCross);
|
||||
|
||||
const ElementViewGrid grid = ElementViewGrid::forZoom(zoom);
|
||||
QCOMPARE(grid.step, step);
|
||||
QCOMPARE(grid.draw_grid, drawGrid);
|
||||
QCOMPARE(grid.draw_cross, drawCross);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_elementviewgrid)
|
||||
|
||||
#include "tst_elementviewgrid.moc"
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QDomDocument>
|
||||
#include <QProcess>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
/**
|
||||
A folio's "freeze new elements" and "freeze new conductors" flags are
|
||||
saved as freezeNewElement="true" and freezeNewConductor="true" on its
|
||||
<diagram>. Loading must read the words back: the flags used to be read
|
||||
with toInt(), which is 0 for "true" as well as for "false", so both were
|
||||
off again after every reload. Runs the real binary's --resave on a
|
||||
project with one folio per combination and reads the saved attributes.
|
||||
*/
|
||||
class tst_foliofreezeflags : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
static bool runQet(const QStringList &arguments)
|
||||
{
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
|
||||
return proc.waitForFinished(60000)
|
||||
&& proc.exitStatus() == QProcess::NormalExit
|
||||
&& proc.exitCode() == 0;
|
||||
}
|
||||
|
||||
/// The <diagram> elements of a saved project, in file order.
|
||||
static QList<QDomElement> diagrams(const QString &path)
|
||||
{
|
||||
QFile file(path);
|
||||
QDomDocument doc;
|
||||
if (!file.open(QIODevice::ReadOnly) || !doc.setContent(&file)) return {};
|
||||
QList<QDomElement> list;
|
||||
for (QDomElement e = doc.documentElement().firstChildElement(QStringLiteral("diagram"));
|
||||
!e.isNull();
|
||||
e = e.nextSiblingElement(QStringLiteral("diagram")))
|
||||
list << e;
|
||||
return list;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found");
|
||||
QVERIFY(m_dir.isValid());
|
||||
}
|
||||
|
||||
// Folio 1 has both flags, folio 2 only the element one, folio 3 none,
|
||||
// and folio 4 is an older file without the attributes at all.
|
||||
void flagsSurviveASave_data()
|
||||
{
|
||||
QTest::addColumn<int>("folio");
|
||||
QTest::addColumn<QString>("element");
|
||||
QTest::addColumn<QString>("conductor");
|
||||
QTest::newRow("both frozen") << 0 << "true" << "true";
|
||||
QTest::newRow("elements frozen") << 1 << "true" << "false";
|
||||
QTest::newRow("not frozen") << 2 << "false" << "false";
|
||||
QTest::newRow("no attributes") << 3 << "false" << "false";
|
||||
}
|
||||
|
||||
void flagsSurviveASave()
|
||||
{
|
||||
QFETCH(int, folio);
|
||||
QFETCH(QString, element);
|
||||
QFETCH(QString, conductor);
|
||||
|
||||
const QString fixture = QFINDTESTDATA("fixtures/folio_freeze_flags.qet");
|
||||
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
|
||||
const QString out = m_dir.filePath(QStringLiteral("resaved%1.qet").arg(folio));
|
||||
QVERIFY2(runQet({QStringLiteral("--resave"), fixture, out}), "--resave failed");
|
||||
|
||||
const QList<QDomElement> list = diagrams(out);
|
||||
QCOMPARE(list.size(), 4);
|
||||
const QDomElement e = list.at(folio);
|
||||
QCOMPARE(e.attribute(QStringLiteral("freezeNewElement")), element);
|
||||
QCOMPARE(e.attribute(QStringLiteral("freezeNewConductor")), conductor);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_foliofreezeflags)
|
||||
#include "tst_foliofreezeflags.moc"
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "foliogrid.h"
|
||||
|
||||
#include <QtTest>
|
||||
#include <QSettings>
|
||||
|
||||
/**
|
||||
FolioGrid::step() -- the folio grid step as read from the settings.
|
||||
Every reader of diagrameditor/Xgrid and Ygrid goes through it, so a
|
||||
settings file holding 0 (a division by zero in Diagram::snapToGrid()
|
||||
and an endless "% 0" in drawBackground()), a negative number (an
|
||||
endless loop) or text can no longer reach the code that uses the step.
|
||||
*/
|
||||
class tst_foliogrid : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void usableValuesAreKept_data()
|
||||
{
|
||||
QTest::addColumn<QVariant>("value");
|
||||
QTest::addColumn<int>("expected");
|
||||
QTest::newRow("one") << QVariant(1) << 1;
|
||||
QTest::newRow("default") << QVariant(10) << 10;
|
||||
QTest::newRow("text number") << QVariant(QStringLiteral("7")) << 7;
|
||||
QTest::newRow("large") << QVariant(100000) << 100000;
|
||||
QTest::newRow("int max") << QVariant(2147483647) << 2147483647;
|
||||
}
|
||||
|
||||
void usableValuesAreKept()
|
||||
{
|
||||
QFETCH(QVariant, value);
|
||||
QFETCH(int, expected);
|
||||
QCOMPARE(FolioGrid::step(value, 10), expected);
|
||||
}
|
||||
|
||||
void unusableValuesFallBack_data()
|
||||
{
|
||||
QTest::addColumn<QVariant>("value");
|
||||
QTest::newRow("missing") << QVariant();
|
||||
QTest::newRow("zero") << QVariant(0);
|
||||
QTest::newRow("negative") << QVariant(-5);
|
||||
QTest::newRow("text") << QVariant(QStringLiteral("ten"));
|
||||
QTest::newRow("empty") << QVariant(QString());
|
||||
QTest::newRow("nan") << QVariant(QStringLiteral("nan"));
|
||||
QTest::newRow("too large") << QVariant(QStringLiteral("99999999999"));
|
||||
QTest::newRow("too large number") << QVariant(qlonglong(99999999999));
|
||||
QTest::newRow("int max + 1") << QVariant(qlonglong(2147483648));
|
||||
}
|
||||
|
||||
void unusableValuesFallBack()
|
||||
{
|
||||
QFETCH(QVariant, value);
|
||||
QCOMPARE(FolioGrid::step(value, 10), 10);
|
||||
QCOMPARE(FolioGrid::step(value, 20), 20);
|
||||
}
|
||||
|
||||
// A decimal in the file is rounded to a whole number, as the plain
|
||||
// QVariant::toInt() read did before.
|
||||
void decimalIsRoundedLikeBefore()
|
||||
{
|
||||
QCOMPARE(FolioGrid::step(QVariant(7.9), 10), 8);
|
||||
QCOMPARE(FolioGrid::step(QVariant(0.4), 10), 10);
|
||||
}
|
||||
|
||||
// Through a real settings file, in a scope of this test's own.
|
||||
void readsTheSettingsKeys()
|
||||
{
|
||||
QCoreApplication::setOrganizationName(QStringLiteral("QElectroTech-tst_foliogrid"));
|
||||
QCoreApplication::setApplicationName(QStringLiteral("tst_foliogrid"));
|
||||
QSettings settings;
|
||||
settings.clear();
|
||||
QCOMPARE(FolioGrid::step(settings.value(FolioGrid::x_key), 10), 10);
|
||||
settings.setValue(FolioGrid::x_key, 0);
|
||||
settings.setValue(FolioGrid::y_key, 15);
|
||||
QCOMPARE(FolioGrid::step(settings.value(FolioGrid::x_key), 10), 10);
|
||||
QCOMPARE(FolioGrid::step(settings.value(FolioGrid::y_key), 10), 15);
|
||||
settings.clear();
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_foliogrid)
|
||||
#include "tst_foliogrid.moc"
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QDomDocument>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTextDocument>
|
||||
|
||||
/// Run the qelectrotech binary with arguments, without a display.
|
||||
static bool runQet(const QStringList &arguments)
|
||||
{
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
|
||||
return proc.waitForFinished(60000)
|
||||
&& proc.exitStatus() == QProcess::NormalExit
|
||||
&& proc.exitCode() == 0;
|
||||
}
|
||||
|
||||
/// The number of lines the words starting with prefix are drawn on.
|
||||
static int drawnLines(const QString &svg, const QString &prefix)
|
||||
{
|
||||
const QRegularExpression text_re(
|
||||
QStringLiteral("<text[^>]*\\by=\"([^\"]+)\"[^>]*>\\s*%1").arg(prefix));
|
||||
QSet<QString> lines;
|
||||
auto it = text_re.globalMatch(svg);
|
||||
while (it.hasNext())
|
||||
lines.insert(it.next().captured(1));
|
||||
return lines.size();
|
||||
}
|
||||
|
||||
/**
|
||||
A free text with a width (text_width on its <input>) keeps it when the
|
||||
project is saved again, and wraps to it. A text without a width is saved
|
||||
without the attribute, as before, so older versions read it unchanged.
|
||||
*/
|
||||
class tst_freetextwidth : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found");
|
||||
m_fixture = QFINDTESTDATA("fixtures/free_text_width.qet");
|
||||
QVERIFY2(!m_fixture.isEmpty(), "fixture project not found");
|
||||
QVERIFY(m_dir.isValid());
|
||||
}
|
||||
|
||||
void widthIsSavedOnlyWhenSet()
|
||||
{
|
||||
const QString out = m_dir.filePath(QStringLiteral("resaved.qet"));
|
||||
QVERIFY2(runQet({QStringLiteral("--resave"), m_fixture, out}), "--resave failed");
|
||||
|
||||
QFile file(out);
|
||||
QVERIFY(file.open(QIODevice::ReadOnly));
|
||||
QDomDocument document;
|
||||
QVERIFY(document.setContent(&file));
|
||||
|
||||
QMap<QString, QString> widths; // first word -> text_width
|
||||
const QDomNodeList inputs = document.elementsByTagName(QStringLiteral("input"));
|
||||
for (int i = 0 ; i < inputs.size() ; ++i) {
|
||||
const QDomElement input = inputs.at(i).toElement();
|
||||
QTextDocument text;
|
||||
text.setHtml(input.attribute(QStringLiteral("text")));
|
||||
const QString first_word = text.toPlainText().section(QLatin1Char(' '), 0, 0);
|
||||
widths.insert(first_word, input.hasAttribute(QStringLiteral("text_width"))
|
||||
? input.attribute(QStringLiteral("text_width"))
|
||||
: QStringLiteral("none"));
|
||||
}
|
||||
|
||||
QCOMPARE(widths.value(QStringLiteral("FreeAlpha")), QStringLiteral("70"));
|
||||
QCOMPARE(widths.value(QStringLiteral("OpenAlpha")), QStringLiteral("none"));
|
||||
//Centred lines: the width of the user, not the one setHtml() gives
|
||||
QCOMPARE(widths.value(QStringLiteral("CentAlpha")), QStringLiteral("120"));
|
||||
}
|
||||
|
||||
void textWrapsToItsWidth()
|
||||
{
|
||||
QVERIFY2(runQet({QStringLiteral("--export-svg"), m_fixture, m_dir.path()}), "--export-svg failed");
|
||||
QFile file(m_dir.filePath(QStringLiteral("01_diagram.svg")));
|
||||
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
|
||||
const QString svg = QString::fromUtf8(file.readAll());
|
||||
|
||||
QVERIFY(drawnLines(svg, QStringLiteral("Free")) > 1);
|
||||
QCOMPARE(drawnLines(svg, QStringLiteral("Open")), 1);
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_fixture;
|
||||
QTemporaryDir m_dir;
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_freetextwidth)
|
||||
#include "tst_freetextwidth.moc"
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "diagramgestureoverlay.h"
|
||||
#include "gesturesettings.h"
|
||||
#include "shortcutmanager.h"
|
||||
#include "ui/configpage/gesturesconfigpage.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QComboBox>
|
||||
#include <QDropEvent>
|
||||
#include <QListWidget>
|
||||
#include <QMainWindow>
|
||||
#include <QMimeData>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <QTest>
|
||||
|
||||
/**
|
||||
The mouse gesture ring's own command list: until the user picks one it
|
||||
follows the shortcut bar as before; a picked list keeps its directions;
|
||||
4 or 8 directions; and the configuration page that edits it.
|
||||
*/
|
||||
class tst_gesturesettings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QMainWindow m_window;
|
||||
|
||||
QAction *addCommand(const QString &id, const QString &text)
|
||||
{
|
||||
auto *action = new QAction(text, &m_window);
|
||||
ShortcutManager::instance().registerAction(
|
||||
action, id, QStringLiteral("Éditeur de schémas"), QKeySequence());
|
||||
return action;
|
||||
}
|
||||
|
||||
QListWidgetItem *listItem(QListWidget *list, const QString &id)
|
||||
{
|
||||
for (int i = 0 ; i < list->count() ; ++i) {
|
||||
if (list->item(i)->data(Qt::UserRole).toString() == id) {
|
||||
return list->item(i);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
QSettings().remove(QStringLiteral("diagrameditor"));
|
||||
addCommand(QStringLiteral("diagrameditor.copy"), QStringLiteral("Copier"));
|
||||
addCommand(QStringLiteral("diagrameditor.cut"), QStringLiteral("Couper"));
|
||||
addCommand(QStringLiteral("diagrameditor.paste"), QStringLiteral("Coller"));
|
||||
}
|
||||
|
||||
void cleanup()
|
||||
{
|
||||
QSettings().remove(QStringLiteral("diagrameditor"));
|
||||
}
|
||||
|
||||
void defaultsFollowTheBar()
|
||||
{
|
||||
QCOMPARE(GestureSettings::directions(), 8);
|
||||
for (const auto c : ShortcutBarSettings::contexts()) {
|
||||
QVERIFY(!GestureSettings::isCustom(c));
|
||||
QCOMPARE(GestureSettings::ids(c), ShortcutBarSettings::ids(c));
|
||||
}
|
||||
//A changed bar still reaches the ring, without its elements
|
||||
ShortcutBarSettings::setIds(ShortcutBarSettings::Selection,
|
||||
{QStringLiteral("diagrameditor.copy"),
|
||||
QStringLiteral("common://10_electric/relay.elmt"),
|
||||
QStringLiteral("diagrameditor.cut")});
|
||||
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Selection),
|
||||
QStringList({QStringLiteral("diagrameditor.copy"),
|
||||
QStringLiteral("diagrameditor.cut")}));
|
||||
}
|
||||
|
||||
void pickedListKeepsDirections()
|
||||
{
|
||||
GestureSettings::setIds(ShortcutBarSettings::Canvas,
|
||||
{QStringLiteral("diagrameditor.copy"), QString(),
|
||||
QStringLiteral("diagrameditor.paste"), QString(), QString()});
|
||||
QVERIFY(GestureSettings::isCustom(ShortcutBarSettings::Canvas));
|
||||
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Canvas),
|
||||
QStringList({QStringLiteral("diagrameditor.copy"), QString(),
|
||||
QStringLiteral("diagrameditor.paste")}));
|
||||
//The bar no longer drives it
|
||||
ShortcutBarSettings::setIds(ShortcutBarSettings::Canvas,
|
||||
{QStringLiteral("diagrameditor.cut")});
|
||||
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Canvas).first(),
|
||||
QStringLiteral("diagrameditor.copy"));
|
||||
//Other contexts still follow the bar
|
||||
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Selection));
|
||||
}
|
||||
|
||||
void savingTheDefaultsFollowsTheBarAgain()
|
||||
{
|
||||
GestureSettings::setIds(ShortcutBarSettings::Conductor, {QStringLiteral("diagrameditor.cut")});
|
||||
QVERIFY(GestureSettings::isCustom(ShortcutBarSettings::Conductor));
|
||||
GestureSettings::setIds(ShortcutBarSettings::Conductor,
|
||||
GestureSettings::defaultIds(ShortcutBarSettings::Conductor));
|
||||
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Conductor));
|
||||
}
|
||||
|
||||
void directions()
|
||||
{
|
||||
GestureSettings::setDirections(4);
|
||||
QCOMPARE(GestureSettings::directions(), 4);
|
||||
GestureSettings::setDirections(8);
|
||||
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/gestures/directions")));
|
||||
QSettings().setValue(QStringLiteral("diagrameditor/gestures/directions"), 5);
|
||||
QCOMPARE(GestureSettings::directions(), 8);
|
||||
}
|
||||
|
||||
void overlayDirectionsAndEmptySlots()
|
||||
{
|
||||
QWidget viewport;
|
||||
viewport.resize(400, 400);
|
||||
DiagramGestureOverlay overlay(&viewport);
|
||||
QAction *copy = ShortcutManager::instance().action(QStringLiteral("diagrameditor.copy"), &m_window);
|
||||
QAction *paste = ShortcutManager::instance().action(QStringLiteral("diagrameditor.paste"), &m_window);
|
||||
const QPoint c(200, 200);
|
||||
const QPoint right(260, 200), down(200, 260);
|
||||
|
||||
overlay.showAt(c, {copy, nullptr, paste}, 4);
|
||||
QCOMPARE(overlay.sectors(), 4);
|
||||
QCOMPARE(overlay.sectorAt(right), 1);
|
||||
QCOMPARE(overlay.actionAt(right), nullptr);
|
||||
QCOMPARE(overlay.actionAt(down), paste);
|
||||
overlay.setPointer(right);
|
||||
QVERIFY(!overlay.grab().isNull());
|
||||
|
||||
//Today's ring: eight directions
|
||||
overlay.showAt(c, {copy, nullptr, paste});
|
||||
QCOMPARE(overlay.sectors(), 8);
|
||||
QCOMPARE(overlay.sectorAt(right), 2);
|
||||
QCOMPARE(overlay.actionAt(right), paste);
|
||||
}
|
||||
|
||||
void pagePlacesAndSaves()
|
||||
{
|
||||
{
|
||||
GesturesConfigPage page;
|
||||
auto *directions = page.findChild<QComboBox *>(QStringLiteral("directionsCombo"));
|
||||
auto *list = page.findChild<QListWidget *>(QStringLiteral("availableList"));
|
||||
auto *ring = page.findChild<GestureRingEditor *>(QStringLiteral("ringEditor"));
|
||||
QCOMPARE(directions->currentData().toInt(), 8);
|
||||
QCOMPARE(ring->ids(), GestureSettings::defaultIds(ShortcutBarSettings::Canvas));
|
||||
|
||||
directions->setCurrentIndex(directions->findData(4));
|
||||
ring->setCurrentSlot(1);
|
||||
list->setCurrentItem(listItem(list, QStringLiteral("diagrameditor.cut")));
|
||||
page.findChild<QPushButton *>(QStringLiteral("placeButton"))->click();
|
||||
page.applyConf();
|
||||
}
|
||||
QCOMPARE(GestureSettings::directions(), 4);
|
||||
QVERIFY(GestureSettings::isCustom(ShortcutBarSettings::Canvas));
|
||||
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Canvas).at(1),
|
||||
QStringLiteral("diagrameditor.cut"));
|
||||
//Contexts left alone still follow the bar
|
||||
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Selection));
|
||||
|
||||
{
|
||||
GesturesConfigPage page;
|
||||
page.findChild<QPushButton *>(QStringLiteral("followBarButton"))->click();
|
||||
page.applyConf();
|
||||
}
|
||||
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Canvas));
|
||||
}
|
||||
|
||||
void dropOnASlot()
|
||||
{
|
||||
GesturesConfigPage page;
|
||||
auto *list = page.findChild<QListWidget *>(QStringLiteral("availableList"));
|
||||
auto *ring = page.findChild<GestureRingEditor *>(QStringLiteral("ringEditor"));
|
||||
ring->resize(ring->sizeHint());
|
||||
const QModelIndex index = list->model()->index(
|
||||
list->row(listItem(list, QStringLiteral("diagrameditor.paste"))), 0);
|
||||
QMimeData *mime = list->model()->mimeData({index});
|
||||
//The right-hand slot of eight
|
||||
const QPointF target = QRectF(ring->rect()).center() + QPointF(68, 0);
|
||||
QCOMPARE(ring->slotAt(target.toPoint()), 2);
|
||||
QDropEvent drop(target, Qt::CopyAction, mime, Qt::LeftButton, Qt::NoModifier);
|
||||
//Straight to the widget: QApplication drops drag events when
|
||||
//no real drag is running
|
||||
QVERIFY(static_cast<QObject *>(ring)->event(&drop));
|
||||
QVERIFY(drop.isAccepted());
|
||||
QCOMPARE(ring->currentSlot(), 2);
|
||||
QCOMPARE(ring->ids().value(2), QStringLiteral("diagrameditor.paste"));
|
||||
//Moved, not copied: paste was already on the default canvas ring
|
||||
QCOMPARE(ring->ids().count(QStringLiteral("diagrameditor.paste")), 1);
|
||||
delete mime;
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_gesturesettings)
|
||||
#include "tst_gesturesettings.moc"
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QImage>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QSize>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
/**
|
||||
Undoing a crop restores the crop rectangle too, not only the pixels
|
||||
shown: before, a project saved after Ctrl+Z still recorded the undone
|
||||
crop, and the picture came back cropped once reopened. Runs the real
|
||||
binary on a script: add a picture, crop it, undo, save; redo, save.
|
||||
|
||||
It also restores whether the pivot was placed by hand: a crop moves
|
||||
the pivot to the centre of the kept region and marks it as default,
|
||||
and before, Ctrl+Z brought the hand-placed pivot back without its
|
||||
mark, so the next save dropped it.
|
||||
*/
|
||||
class tst_imagecropundo : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
QJsonObject run(const QString &script, const QString &project)
|
||||
{
|
||||
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QDir().mkpath(home);
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly)) return {};
|
||||
f.write(script.toUtf8());
|
||||
f.close();
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--run"), path, project});
|
||||
if (!proc.waitForFinished(120000)) return {};
|
||||
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
|
||||
+ proc.readAllStandardError());
|
||||
const QString mark = QStringLiteral("PROBE ");
|
||||
for (const QString &line : out.split(QLatin1Char('\n'))) {
|
||||
const int i = line.indexOf(mark);
|
||||
if (i >= 0)
|
||||
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static QByteArray read(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
|
||||
}
|
||||
// The size of the picture as shown, from the <image> element's own
|
||||
// text (its first text node: a cropped picture carries <image_base>
|
||||
// as a child too).
|
||||
static QSize shownSize(const QByteArray &xml)
|
||||
{
|
||||
const QRegularExpression image(QStringLiteral("<image\\b[^>]*>\\s*([A-Za-z0-9+/=]+)"));
|
||||
const QRegularExpressionMatch m = image.match(QString::fromUtf8(xml));
|
||||
if (!m.hasMatch()) return {};
|
||||
return QImage::fromData(QByteArray::fromBase64(m.captured(1).toLatin1())).size();
|
||||
}
|
||||
// qet.imageCrop()'s {x, y, width, height}, as logged by the script.
|
||||
static QRect rect(const QJsonValue &crop)
|
||||
{
|
||||
const QJsonObject o = crop.toObject();
|
||||
return QRect(o.value("x").toInt(-1), o.value("y").toInt(-1),
|
||||
o.value("width").toInt(-1), o.value("height").toInt(-1));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
QImage img(40, 30, QImage::Format_RGB32);
|
||||
img.fill(Qt::darkGreen);
|
||||
QVERIFY(img.save(m_dir.filePath(QStringLiteral("pic.png"))));
|
||||
}
|
||||
|
||||
void undoneCropIsNotSaved()
|
||||
{
|
||||
const QString undone = m_dir.filePath(QStringLiteral("undone.qet"));
|
||||
const QString redone = m_dir.filePath(QStringLiteral("redone.qet"));
|
||||
const QString script = QStringLiteral(R"JS(
|
||||
var i = qet.addImage(0, '%1', 100, 100);
|
||||
var r = {full: qet.imageCrop(0, i)};
|
||||
r.cropped_ok = qet.cropImage(0, i, 10, 5, 20, 10);
|
||||
r.cropped = qet.imageCrop(0, i);
|
||||
r.same_ok = qet.cropImage(0, i, 10, 5, 20, 10);
|
||||
r.empty_ok = qet.cropImage(0, i, 10, 5, 0, 10);
|
||||
r.outside_ok = qet.cropImage(0, i, 100, 100, 20, 10);
|
||||
qet.undo();
|
||||
r.undone = qet.imageCrop(0, i);
|
||||
qet.save('%2');
|
||||
qet.redo();
|
||||
r.redone = qet.imageCrop(0, i);
|
||||
qet.save('%3');
|
||||
qet.log('PROBE ' + JSON.stringify(r));
|
||||
)JS").arg(m_dir.filePath(QStringLiteral("pic.png")), undone, redone);
|
||||
|
||||
const QJsonObject r = run(script, QStringLiteral(QET_EXAMPLES_DIR "/741.qet"));
|
||||
QVERIFY2(!r.isEmpty(), "the script logged nothing");
|
||||
QCOMPARE(rect(r.value("full")), QRect(0, 0, 40, 30));
|
||||
QVERIFY(r.value("cropped_ok").toBool());
|
||||
QCOMPARE(rect(r.value("cropped")), QRect(10, 5, 20, 10));
|
||||
// Crops that change nothing report it, and push no undo step:
|
||||
// the undo below still undoes the real crop.
|
||||
QCOMPARE(r.value("same_ok").toBool(true), false);
|
||||
QCOMPARE(r.value("empty_ok").toBool(true), false);
|
||||
QCOMPARE(r.value("outside_ok").toBool(true), false);
|
||||
QCOMPARE(rect(r.value("undone")), QRect(0, 0, 40, 30));
|
||||
QCOMPARE(rect(r.value("redone")), QRect(10, 5, 20, 10));
|
||||
|
||||
const QByteArray undone_xml = read(undone);
|
||||
QVERIFY(undone_xml.contains("<image "));
|
||||
QVERIFY2(!undone_xml.contains("<crop "), "an undone crop was saved");
|
||||
// The picture shown follows: whole after undo, the kept region after redo.
|
||||
QCOMPARE(shownSize(undone_xml), QSize(40, 30));
|
||||
QCOMPARE(shownSize(read(redone)), QSize(20, 10));
|
||||
const QRegularExpressionMatch crop =
|
||||
QRegularExpression(QStringLiteral("<crop ([^>]*)/>")).match(QString::fromUtf8(read(redone)));
|
||||
QVERIFY2(crop.hasMatch(), "the redone crop was not saved");
|
||||
for (const char *attribute : {R"(x="10")", R"(y="5")", R"(w="20")", R"(h="10")"})
|
||||
QVERIFY2(crop.captured(1).contains(QLatin1String(attribute)), attribute);
|
||||
}
|
||||
|
||||
void undoneCropKeepsHandPlacedPivot()
|
||||
{
|
||||
// A picture whose pivot was placed by hand: saved as <transform>
|
||||
// with pivotX/pivotY, which no script call writes, so the test
|
||||
// adds it to a saved project itself.
|
||||
const QString plain = m_dir.filePath(QStringLiteral("plain.qet"));
|
||||
const QString pivot = m_dir.filePath(QStringLiteral("pivot.qet"));
|
||||
QJsonObject r = run(QStringLiteral(R"JS(
|
||||
var i = qet.addImage(0, '%1', 100, 100);
|
||||
qet.save('%2');
|
||||
qet.log('PROBE ' + JSON.stringify({added: i >= 0}));
|
||||
)JS").arg(m_dir.filePath(QStringLiteral("pic.png")), plain),
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/741.qet"));
|
||||
QVERIFY(r.value("added").toBool());
|
||||
QByteArray xml = read(plain);
|
||||
QCOMPARE(xml.count("</image>"), 1);
|
||||
xml.replace("</image>", "<transform rotation=\"0\" skewX=\"0\" skewY=\"0\" "
|
||||
"scaleX=\"1\" scaleY=\"1\" pivotX=\"5\" pivotY=\"7\"/></image>");
|
||||
QFile f(pivot);
|
||||
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||
f.write(xml);
|
||||
f.close();
|
||||
|
||||
const QString before = m_dir.filePath(QStringLiteral("before.qet"));
|
||||
const QString undone = m_dir.filePath(QStringLiteral("pivot-undone.qet"));
|
||||
r = run(QStringLiteral(R"JS(
|
||||
var r = {};
|
||||
qet.save('%1');
|
||||
r.cropped_ok = qet.cropImage(0, 0, 10, 5, 20, 10);
|
||||
qet.undo();
|
||||
qet.save('%2');
|
||||
qet.log('PROBE ' + JSON.stringify(r));
|
||||
)JS").arg(before, undone), pivot);
|
||||
QVERIFY(r.value("cropped_ok").toBool());
|
||||
// The fixture is read as intended: saved untouched, it keeps the pivot.
|
||||
const QByteArray saved = read(before);
|
||||
QVERIFY(saved.contains("pivotX=\"5\"") && saved.contains("pivotY=\"7\""));
|
||||
const QByteArray afterUndo = read(undone);
|
||||
QVERIFY2(afterUndo.contains("pivotX=\"5\"") && afterUndo.contains("pivotY=\"7\""),
|
||||
"the hand-placed pivot was dropped after undoing the crop");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_imagecropundo)
|
||||
#include "tst_imagecropundo.moc"
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QBuffer>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QImage>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
/**
|
||||
Before #1310, a project saved after a crop was undone kept the undone
|
||||
<crop>, with the uncropped picture shown. Loading such a file has to
|
||||
drop that crop: the picture shown is always exactly the crop's size,
|
||||
so a picture of another size means the crop is stale. Otherwise the
|
||||
next edit of the picture applies the crop again.
|
||||
|
||||
Runs the real binary: a script adds and crops a picture and saves;
|
||||
the test then puts another picture where the shown one is, and lets
|
||||
the binary load and save that file again.
|
||||
*/
|
||||
class tst_imagestalecrop : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
// Runs the binary on @p project with a script that saves it to @p out.
|
||||
bool run(const QString &script, const QString &project)
|
||||
{
|
||||
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QDir().mkpath(home);
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly)) return false;
|
||||
f.write(script.toUtf8());
|
||||
f.close();
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--run"), path, project});
|
||||
return proc.waitForFinished(120000) && proc.exitStatus() == QProcess::NormalExit;
|
||||
}
|
||||
|
||||
// Opens @p in and saves it to @p out.
|
||||
bool resave(const QString &in, const QString &out)
|
||||
{
|
||||
return run(QStringLiteral("qet.save('%1');\n").arg(out), in) && QFile::exists(out);
|
||||
}
|
||||
|
||||
static QByteArray read(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
return f.open(QIODevice::ReadOnly) ? f.readAll() : QByteArray();
|
||||
}
|
||||
|
||||
static QByteArray png(const QSize &size)
|
||||
{
|
||||
QImage img(size, QImage::Format_RGB32);
|
||||
img.fill(Qt::darkGreen);
|
||||
QByteArray bytes;
|
||||
QBuffer buffer(&bytes);
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
img.save(&buffer, "PNG");
|
||||
return bytes.toBase64();
|
||||
}
|
||||
|
||||
// The size of the saved original (<image_base>), or an empty size.
|
||||
static QSize baseSize(const QByteArray &xml)
|
||||
{
|
||||
const QRegularExpression base(QStringLiteral("<image_base>\\s*([A-Za-z0-9+/=]+)"));
|
||||
const QRegularExpressionMatch m = base.match(QString::fromLatin1(xml));
|
||||
if (!m.hasMatch()) return {};
|
||||
return QImage::fromData(QByteArray::fromBase64(m.captured(1).toLatin1())).size();
|
||||
}
|
||||
|
||||
// @p xml with the shown picture (the <image> element's first text)
|
||||
// replaced by a PNG of @p size.
|
||||
static QByteArray withShown(QByteArray xml, const QSize &size)
|
||||
{
|
||||
const QRegularExpression shown(QStringLiteral("(<image\\b[^>]*>\\s*)([A-Za-z0-9+/=]+)"));
|
||||
const QRegularExpressionMatch m = shown.match(QString::fromLatin1(xml));
|
||||
if (!m.hasMatch()) return {};
|
||||
return xml.replace(m.capturedStart(2), m.capturedLength(2), png(size));
|
||||
}
|
||||
|
||||
QString m_cropped; // a picture of 40 x 30, cropped to 10,5,20,10
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
QImage img(40, 30, QImage::Format_RGB32);
|
||||
img.fill(Qt::darkGreen);
|
||||
const QString pic = m_dir.filePath(QStringLiteral("pic.png"));
|
||||
QVERIFY(img.save(pic));
|
||||
|
||||
m_cropped = m_dir.filePath(QStringLiteral("cropped.qet"));
|
||||
QVERIFY(run(QStringLiteral(R"JS(
|
||||
var i = qet.addImage(0, '%1', 100, 100);
|
||||
qet.cropImage(0, i, 10, 5, 20, 10);
|
||||
qet.save('%2');
|
||||
)JS").arg(pic, m_cropped), QStringLiteral(QET_EXAMPLES_DIR "/741.qet")));
|
||||
const QByteArray xml = read(m_cropped);
|
||||
QCOMPARE(xml.count("<image "), 1);
|
||||
QVERIFY(xml.contains("<crop "));
|
||||
QVERIFY(xml.contains("<image_base>"));
|
||||
}
|
||||
|
||||
// A valid crop is kept, byte for byte.
|
||||
void validCropIsKept()
|
||||
{
|
||||
const QString out = m_dir.filePath(QStringLiteral("valid-out.qet"));
|
||||
QVERIFY(resave(m_cropped, out));
|
||||
const QByteArray xml = read(out);
|
||||
// Attributes in any order: Qt before 6.5 writes them unordered.
|
||||
const QRegularExpression crop(QStringLiteral("<crop (?=[^>]*\\bx=\"10\")(?=[^>]*\\by=\"5\")"
|
||||
"(?=[^>]*\\bw=\"20\")(?=[^>]*\\bh=\"10\")"));
|
||||
QVERIFY2(crop.match(QString::fromLatin1(xml)).hasMatch(), "the valid crop was lost");
|
||||
QVERIFY(xml.contains("<image_base>"));
|
||||
}
|
||||
|
||||
// The shown picture is the whole original (40 x 30): the crop was
|
||||
// undone before the file was saved. It is dropped, and with nothing
|
||||
// else to remember the original is not saved twice any more.
|
||||
void staleCropOfWholeOriginalIsDropped()
|
||||
{
|
||||
const QString stale = m_dir.filePath(QStringLiteral("stale.qet"));
|
||||
const QByteArray xml = withShown(read(m_cropped), QSize(40, 30));
|
||||
QVERIFY(!xml.isEmpty());
|
||||
QFile f(stale);
|
||||
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||
f.write(xml);
|
||||
f.close();
|
||||
|
||||
const QString out = m_dir.filePath(QStringLiteral("stale-out.qet"));
|
||||
QVERIFY(resave(stale, out));
|
||||
const QByteArray saved = read(out);
|
||||
QCOMPARE(saved.count("<image "), 1);
|
||||
QVERIFY2(!saved.contains("<crop "), "the stale crop was kept");
|
||||
QVERIFY(!saved.contains("<image_base>"));
|
||||
}
|
||||
|
||||
// The shown picture matches neither the crop nor the original: it is
|
||||
// what the user saw, so it becomes the original -- a crop made after
|
||||
// loading cuts it, not the old original.
|
||||
void shownPictureOfOtherSizeBecomesTheOriginal()
|
||||
{
|
||||
const QString odd = m_dir.filePath(QStringLiteral("odd.qet"));
|
||||
const QByteArray xml = withShown(read(m_cropped), QSize(25, 25));
|
||||
QVERIFY(!xml.isEmpty());
|
||||
QFile f(odd);
|
||||
QVERIFY(f.open(QIODevice::WriteOnly));
|
||||
f.write(xml);
|
||||
f.close();
|
||||
|
||||
const QString out = m_dir.filePath(QStringLiteral("odd-out.qet"));
|
||||
QVERIFY(resave(odd, out));
|
||||
const QByteArray saved = read(out);
|
||||
QVERIFY2(!saved.contains("<crop "), "the stale crop was kept");
|
||||
QVERIFY(!saved.contains("<image_base>"));
|
||||
|
||||
const QString recropped = m_dir.filePath(QStringLiteral("odd-recropped.qet"));
|
||||
QVERIFY(run(QStringLiteral("qet.cropImage(0, 0, 0, 0, 10, 10);\nqet.save('%1');\n")
|
||||
.arg(recropped), odd));
|
||||
QCOMPARE(baseSize(read(recropped)), QSize(25, 25));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_imagestalecrop)
|
||||
#include "tst_imagestalecrop.moc"
|
||||
@@ -0,0 +1,90 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// Placing the same symbol twice in lmdg.qet: its embedded collection has a
|
||||
// "k_elem" category before "import". Imported symbols were filed under the
|
||||
// first category while every lookup is under "import/", so the second
|
||||
// placement could not find the first copy, tried to import it again, and
|
||||
// failed -- in a script (qet.addElement() returned "") and in the editor,
|
||||
// where the drop silently placed nothing.
|
||||
class tst_importcategory : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
// Run @p script on @p project in a sandbox of its own and return the
|
||||
// JSON object it logged.
|
||||
QJsonObject run(const QString &script, const QString &project)
|
||||
{
|
||||
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly)) return {};
|
||||
f.write(script.toUtf8());
|
||||
f.close();
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
//The shipped collection, so common:// paths resolve. Given
|
||||
//on the command line: macOS does not read a settings file
|
||||
//under XDG_CONFIG_HOME.
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--common-elements-dir=" QET_ELEMENTS_DIR "/"),
|
||||
QStringLiteral("--run"), path, project});
|
||||
if (!proc.waitForFinished(120000)) return {};
|
||||
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
|
||||
+ proc.readAllStandardError());
|
||||
const QString mark = QStringLiteral("PROBE ");
|
||||
for (const QString &line : out.split(QLatin1Char('\n'))) {
|
||||
const int i = line.indexOf(mark);
|
||||
if (i >= 0)
|
||||
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void sameSymbolTwiceInLmdg()
|
||||
{
|
||||
const QString project = m_dir.filePath(QStringLiteral("lmdg.qet"));
|
||||
QVERIFY(QFile::copy(QStringLiteral(QET_EXAMPLES_DIR "/lmdg.qet"), project));
|
||||
|
||||
const QJsonObject r = run(QStringLiteral(
|
||||
"var p = 'common://10_electric/10_allpole/310_relays_contactors_contacts/"
|
||||
"01_coils/bobine_ka_a_remanence.elmt';\n"
|
||||
"var a = qet.addElement(0, p, 100, 100);\n"
|
||||
"var b = qet.addElement(0, p, 300, 100);\n"
|
||||
"qet.log('PROBE ' + JSON.stringify({first: a, second: b}));\n"),
|
||||
project);
|
||||
QVERIFY2(!r.isEmpty(), "the script logged nothing");
|
||||
QVERIFY2(!r.value(QStringLiteral("first")).toString().isEmpty(),
|
||||
"the first placement failed");
|
||||
QVERIFY2(!r.value(QStringLiteral("second")).toString().isEmpty(),
|
||||
"the second placement of the same symbol failed");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_importcategory)
|
||||
|
||||
#include "tst_importcategory.moc"
|
||||
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QSet>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// fixtures/junction_dot_shared_potential.qet: four contacts K1..K4 in a row,
|
||||
// wired K1-K2, K1-K4 and K2-K3. All three wires run along one line above the
|
||||
// contacts, so there is a T above K2 and another above K3. The corner above
|
||||
// K3 belongs to K2-K3 and lies on K1-K4, which shares no terminal with it:
|
||||
// that dot used to be missing (issue #1280).
|
||||
class tst_junctiondots : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
// Export the fixture to SVG and return the distinct positions of the
|
||||
// junction dots drawn on it.
|
||||
QSet<QString> dots()
|
||||
{
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
const QString out = m_dir.filePath(QStringLiteral("svg"));
|
||||
QDir().mkpath(home);
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
env.insert(QStringLiteral("TMPDIR"), home);
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--export-svg"),
|
||||
QFINDTESTDATA("fixtures/junction_dot_shared_potential.qet"), out});
|
||||
if (!proc.waitForFinished(60000) || proc.exitCode() != 0)
|
||||
return {};
|
||||
|
||||
const QStringList svgs = QDir(out).entryList({QStringLiteral("*.svg")});
|
||||
if (svgs.size() != 1)
|
||||
return {};
|
||||
QFile f(QDir(out).filePath(svgs.first()));
|
||||
if (!f.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
const QString svg = QString::fromUtf8(f.readAll());
|
||||
|
||||
// A junction dot on a wire of the default width is a circle
|
||||
// 3.0 across; the symbols in the fixture draw none that size.
|
||||
static const QRegularExpression re(
|
||||
QStringLiteral("<circle cx=\"([^\"]*)\" cy=\"([^\"]*)\" r=\"1.5\"/>"));
|
||||
QSet<QString> found;
|
||||
auto it = re.globalMatch(svg);
|
||||
while (it.hasNext()) {
|
||||
const auto m = it.next();
|
||||
found << m.captured(1) + QLatin1Char(',') + m.captured(2);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void dotAtEveryT()
|
||||
{
|
||||
const QSet<QString> expected{QStringLiteral("200,260"),
|
||||
QStringLiteral("300,260")};
|
||||
QCOMPARE(dots(), expected);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_junctiondots)
|
||||
#include "tst_junctiondots.moc"
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QDomDocument>
|
||||
#include <QProcess>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
/**
|
||||
Projects saved before the <sequentialNumbers> element keep an element's
|
||||
or conductor's sequential numbers as attributes: sequ_1, sequf_1, seqt_1,
|
||||
seqtf_1, seqh_1 and seqhf_1. The loaders look for one of them to take
|
||||
the old route; the list named sequf_1 twice and seqhf_1 never, so a
|
||||
file whose only sequence was the hundred-folio one lost it. Runs the
|
||||
real binary's --resave on fixtures/legacy_sequential_attributes.qet
|
||||
and reads the <sequentialNumbers> it writes.
|
||||
*/
|
||||
class tst_legacysequentialattributes : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
QDomDocument m_resaved;
|
||||
|
||||
static bool runQet(const QStringList &arguments)
|
||||
{
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
|
||||
return proc.waitForFinished(60000)
|
||||
&& proc.exitStatus() == QProcess::NormalExit
|
||||
&& proc.exitCode() == 0;
|
||||
}
|
||||
|
||||
/// The first @p tag element of the saved project with this uuid.
|
||||
QDomElement item(const QString &tag, const QString &uuid) const
|
||||
{
|
||||
const QDomNodeList list = m_resaved.elementsByTagName(tag);
|
||||
for (int i = 0; i < list.size(); ++i) {
|
||||
const QDomElement e = list.at(i).toElement();
|
||||
if (e.attribute(QStringLiteral("uuid")) == uuid) return e;
|
||||
}
|
||||
return QDomElement();
|
||||
}
|
||||
|
||||
/// The text of <sequentialNumbers><@p part> under @p e.
|
||||
static QString sequence(const QDomElement &e, const QString &part)
|
||||
{
|
||||
return e.firstChildElement(QStringLiteral("sequentialNumbers"))
|
||||
.firstChildElement(part).text();
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found");
|
||||
QVERIFY(m_dir.isValid());
|
||||
const QString fixture = QFINDTESTDATA("fixtures/legacy_sequential_attributes.qet");
|
||||
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
|
||||
const QString out = m_dir.filePath(QStringLiteral("resaved.qet"));
|
||||
QVERIFY2(runQet({QStringLiteral("--resave"), fixture, out}), "--resave failed");
|
||||
QFile file(out);
|
||||
QVERIFY(file.open(QIODevice::ReadOnly));
|
||||
QVERIFY(m_resaved.setContent(&file));
|
||||
}
|
||||
|
||||
// The element and the conductor whose only old attribute is seqhf_1.
|
||||
void hundredFolioSequenceIsKept_data()
|
||||
{
|
||||
QTest::addColumn<QString>("tag");
|
||||
QTest::addColumn<QString>("uuid");
|
||||
QTest::addColumn<QString>("value");
|
||||
QTest::newRow("element") << "element" << "{6c58a5a1-aa4e-40c4-9e9d-a76524f76e52}" << "5";
|
||||
QTest::newRow("conductor") << "conductor" << "{f5162c59-0c59-4e94-b6a6-4cf259a52d91}" << "7";
|
||||
}
|
||||
|
||||
void hundredFolioSequenceIsKept()
|
||||
{
|
||||
QFETCH(QString, tag);
|
||||
QFETCH(QString, uuid);
|
||||
QFETCH(QString, value);
|
||||
const QDomElement e = item(tag, uuid);
|
||||
QVERIFY2(!e.isNull(), "item not found in the resaved project");
|
||||
QVERIFY2(!e.hasAttribute(QStringLiteral("seqhf_1")), "the old attribute is not written back");
|
||||
QCOMPARE(sequence(e, QStringLiteral("hundredFolio")), value);
|
||||
}
|
||||
|
||||
// The unit sequence, whose attribute was already in the list, as before.
|
||||
void unitSequenceIsKept_data()
|
||||
{
|
||||
QTest::addColumn<QString>("tag");
|
||||
QTest::addColumn<QString>("uuid");
|
||||
QTest::addColumn<QString>("value");
|
||||
QTest::newRow("element") << "element" << "{29b32d7b-5ec3-42c8-afda-788e9614c0fd}" << "3";
|
||||
QTest::newRow("conductor") << "conductor" << "{89cacce1-b12a-4ec3-aadf-d3f8feb7494d}" << "4";
|
||||
}
|
||||
|
||||
void unitSequenceIsKept()
|
||||
{
|
||||
QFETCH(QString, tag);
|
||||
QFETCH(QString, uuid);
|
||||
QFETCH(QString, value);
|
||||
const QDomElement e = item(tag, uuid);
|
||||
QVERIFY2(!e.isNull(), "item not found in the resaved project");
|
||||
QCOMPARE(sequence(e, QStringLiteral("unit")), value);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_legacysequentialattributes)
|
||||
#include "tst_legacysequentialattributes.moc"
|
||||
@@ -0,0 +1,80 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
--export-pdf writes a project's cross-reference links in the same order
|
||||
every time. They used to come out in the order of Element pointers, so
|
||||
the same project gave a different PDF in each run. Exported in separate
|
||||
processes, since pointers only change between runs.
|
||||
*/
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
#include <QtTest>
|
||||
|
||||
class tst_pdflinkorder : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
// The link annotations' rectangles and destinations, in file order.
|
||||
QStringList exportLinks(const QString &project, int run)
|
||||
{
|
||||
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(run));
|
||||
const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(run));
|
||||
const QString out = m_dir.filePath(QStringLiteral("out%1.pdf").arg(run));
|
||||
QDir().mkpath(home);
|
||||
QDir().mkpath(tmp);
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/.config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/.local/share"));
|
||||
env.insert(QStringLiteral("TMPDIR"), tmp);
|
||||
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--export-pdf"), project, out});
|
||||
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
|
||||
return {};
|
||||
|
||||
QFile file(out);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
const QString pdf = QString::fromLatin1(file.readAll());
|
||||
static const QRegularExpression link(QStringLiteral(
|
||||
R"(/Subtype /Link\s*/Rect \[([^\]]*)\][\s\S]*?/D \[\d+ 0 R ([^\]]*)\])"));
|
||||
QStringList links;
|
||||
auto it = link.globalMatch(pdf);
|
||||
while (it.hasNext()) {
|
||||
const auto m = it.next();
|
||||
links << m.captured(1).simplified() + QStringLiteral(" -> ")
|
||||
+ m.captured(2).simplified();
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void sameOrderEveryRun()
|
||||
{
|
||||
const QString project =
|
||||
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/industrial.qet");
|
||||
QVERIFY2(QFile::exists(project), "examples/industrial.qet not found");
|
||||
|
||||
const QStringList first = exportLinks(project, 0);
|
||||
QVERIFY2(first.size() >= 2, "expected several cross-reference links");
|
||||
for (int run = 1; run < 5; ++run)
|
||||
QCOMPARE(exportLinks(project, run), first);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_pdflinkorder)
|
||||
#include "tst_pdflinkorder.moc"
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QPageSize>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// --export-pdf sizes each page to its folio, and QPageSize rounds a size
|
||||
// within 3 pt of a standard sheet to the sheet. It knows the sheets upright
|
||||
// only, so a wide folio used to miss: the fixture's A3 landscape folio
|
||||
// (frame 1190.25 x 840.75 pt) came out 1190 x 841 pt while its A3 portrait
|
||||
// folio came out 842 x 1191, the sheet. Both must land on A3.
|
||||
class tst_pdfpagesize : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
// The MediaBox of each page, in page order. Qt writes one uncompressed
|
||||
// "/MediaBox [0 0 w h]" per page object.
|
||||
QList<QSizeF> exportPages(const QString &project)
|
||||
{
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
const QString tmp = m_dir.filePath(QStringLiteral("tmp"));
|
||||
const QString out = m_dir.filePath(QStringLiteral("out.pdf"));
|
||||
QDir().mkpath(home);
|
||||
QDir().mkpath(tmp);
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/.config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/.local/share"));
|
||||
env.insert(QStringLiteral("TMPDIR"), tmp);
|
||||
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--export-pdf"), project, out});
|
||||
if (!proc.waitForFinished(60000) || proc.exitCode() != 0)
|
||||
return {};
|
||||
|
||||
QFile file(out);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
const QString pdf = QString::fromLatin1(file.readAll());
|
||||
static const QRegularExpression box(
|
||||
QStringLiteral(R"(/MediaBox \[0 0 ([0-9.]+) ([0-9.]+)\])"));
|
||||
QList<QSizeF> pages;
|
||||
auto it = box.globalMatch(pdf);
|
||||
while (it.hasNext()) {
|
||||
const auto m = it.next();
|
||||
pages << QSizeF(m.captured(1).toDouble(), m.captured(2).toDouble());
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void a3BothWays()
|
||||
{
|
||||
const QString fixture = QFINDTESTDATA("fixtures/pdf_page_a3.qet");
|
||||
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
|
||||
|
||||
const QList<QSizeF> pages = exportPages(fixture);
|
||||
QCOMPARE(pages.size(), 2);
|
||||
|
||||
// Qt writes the sheet's whole points: 842 x 1191 for A3.
|
||||
const QSize a3 = QPageSize(QPageSize::A3).sizePoints();
|
||||
QCOMPARE(pages.at(0), QSizeF(a3.height(), a3.width())); // landscape
|
||||
QCOMPARE(pages.at(1), QSizeF(a3)); // portrait
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_pdfpagesize)
|
||||
#include "tst_pdfpagesize.moc"
|
||||
@@ -0,0 +1,183 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
/*
|
||||
With SOURCE_DATE_EPOCH set, --export-pdf writes the same bytes for the
|
||||
same project in every run: the dates are the ones it names, the document
|
||||
id comes from the content of the PDF, and the fonts come in a fixed order.
|
||||
Without it the dates are the time of the export, as before. Exported in
|
||||
separate processes, since what used to differ changed between runs.
|
||||
*/
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
#include <QtTest>
|
||||
#include <QUuid>
|
||||
|
||||
class tst_pdfreproducible : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
int m_run = 0;
|
||||
|
||||
QByteArray exportPdf(const QString &project, const QByteArray &epoch,
|
||||
const QString &folder = QString())
|
||||
{
|
||||
const int run = m_run++;
|
||||
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(run));
|
||||
const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(run));
|
||||
const QString out = folder.isEmpty()
|
||||
? m_dir.filePath(QStringLiteral("out%1.pdf").arg(run))
|
||||
: m_dir.filePath(folder + QStringLiteral("/same.pdf"));
|
||||
if (!folder.isEmpty())
|
||||
QDir().mkpath(m_dir.filePath(folder));
|
||||
QDir().mkpath(home);
|
||||
QDir().mkpath(tmp);
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/.config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/.local/share"));
|
||||
env.insert(QStringLiteral("TMPDIR"), tmp);
|
||||
env.remove(QStringLiteral("QT_HASH_SEED"));
|
||||
env.remove(QStringLiteral("SOURCE_DATE_EPOCH"));
|
||||
if (!epoch.isEmpty())
|
||||
env.insert(QStringLiteral("SOURCE_DATE_EPOCH"), QString::fromLatin1(epoch));
|
||||
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--export-pdf"), project, out});
|
||||
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
|
||||
return {};
|
||||
QFile file(out);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
return file.readAll();
|
||||
}
|
||||
|
||||
/// Every object the xref table lists starts where the table says: the
|
||||
/// dates are rewritten after Qt writes the file, which moves them.
|
||||
static bool xrefMatches(const QByteArray &pdf)
|
||||
{
|
||||
const int sx = pdf.lastIndexOf("startxref");
|
||||
if (sx == -1)
|
||||
return false;
|
||||
const int xref = pdf.mid(sx + 9).trimmed().split('\n').value(0).toInt();
|
||||
if (!pdf.mid(xref).startsWith("xref"))
|
||||
return false;
|
||||
const QList<QByteArray> lines = pdf.mid(xref).split('\n');
|
||||
const QList<QByteArray> header = lines.value(1).split(' ');
|
||||
const int first = header.value(0).toInt();
|
||||
const int count = header.value(1).toInt();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const QList<QByteArray> entry = lines.value(2 + i).split(' ');
|
||||
if (entry.value(2) != "n")
|
||||
continue;
|
||||
const QByteArray obj = QByteArray::number(first + i) + " 0 obj";
|
||||
if (pdf.mid(entry.value(0).toInt(), obj.size()) != obj)
|
||||
return false;
|
||||
}
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void sameBytesEveryRun()
|
||||
{
|
||||
// A project with no cross-reference links: their order is fixed
|
||||
// separately.
|
||||
const QString project =
|
||||
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/741.qet");
|
||||
QVERIFY2(QFile::exists(project), "examples/741.qet not found");
|
||||
|
||||
const QByteArray first = exportPdf(project, "1700000000");
|
||||
QVERIFY(!first.isEmpty());
|
||||
QVERIFY(first.contains("/CreationDate (D:20231114221320Z)"));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
QVERIFY(first.contains("xmp:CreateDate=\"2023-11-14T22:13:20Z\""));
|
||||
#endif
|
||||
QVERIFY2(xrefMatches(first), "the xref table does not match the file");
|
||||
for (int i = 0; i < 3; ++i)
|
||||
QVERIFY2(exportPdf(project, "1700000000") == first,
|
||||
"two exports of the same project differ");
|
||||
}
|
||||
|
||||
void sameBytesWithNewUuids()
|
||||
{
|
||||
// #1178: a project generated again from the same data has the same
|
||||
// drawing but new uuids, and its PDF used to differ in the
|
||||
// document id, which came from the project file.
|
||||
const QString project =
|
||||
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/741.qet");
|
||||
QFile in(project);
|
||||
QVERIFY(in.open(QIODevice::ReadOnly));
|
||||
QString xml = QString::fromUtf8(in.readAll());
|
||||
static const QRegularExpression uuid(QStringLiteral(
|
||||
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"));
|
||||
QHash<QString, QString> renamed;
|
||||
QString regenerated;
|
||||
qsizetype last = 0;
|
||||
for (auto it = uuid.globalMatch(xml); it.hasNext(); ) {
|
||||
const auto match = it.next();
|
||||
const QString key = match.captured().toLower();
|
||||
if (!renamed.contains(key))
|
||||
renamed.insert(key, QUuid::createUuid().toString(QUuid::WithoutBraces));
|
||||
regenerated += xml.mid(last, match.capturedStart() - last) + renamed.value(key);
|
||||
last = match.capturedEnd();
|
||||
}
|
||||
regenerated += xml.mid(last);
|
||||
QVERIFY(renamed.size() > 10);
|
||||
const QString copy = m_dir.filePath(QStringLiteral("regenerated.qet"));
|
||||
QFile out(copy);
|
||||
QVERIFY(out.open(QIODevice::WriteOnly));
|
||||
out.write(regenerated.toUtf8());
|
||||
out.close();
|
||||
|
||||
const QByteArray first = exportPdf(project, "1700000000");
|
||||
QVERIFY(!first.isEmpty());
|
||||
QVERIFY2(!first.contains("6f1c2d4e-9a3b-4c5d-8e7f-0a1b2c3d4e5f"),
|
||||
"the placeholder document id was left in the file");
|
||||
QVERIFY2(xrefMatches(first), "the xref table does not match the file");
|
||||
QVERIFY2(exportPdf(copy, "1700000000") == first,
|
||||
"the same drawing with new uuids gives a different PDF");
|
||||
}
|
||||
|
||||
void sameBytesInAnyFolder()
|
||||
{
|
||||
// #1178: the links carry the output path, and rewriting them left
|
||||
// the old startxref in the file, with the size the file had before.
|
||||
const QString project =
|
||||
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/iso_sfc_example.qet");
|
||||
QVERIFY2(QFile::exists(project), "examples/iso_sfc_example.qet not found");
|
||||
|
||||
const QByteArray first = exportPdf(project, "1700000000", QStringLiteral("p"));
|
||||
QVERIFY(!first.isEmpty());
|
||||
QVERIFY(first.contains("/GoTo"));
|
||||
QCOMPARE(first.count("startxref"), 1);
|
||||
QVERIFY2(xrefMatches(first), "the xref table does not match the file");
|
||||
QVERIFY2(exportPdf(project, "1700000000", QStringLiteral("longer-folder-name")) == first,
|
||||
"the same project gives a different PDF in another folder");
|
||||
}
|
||||
|
||||
void nowWithoutTheVariable()
|
||||
{
|
||||
const QString project =
|
||||
QStringLiteral(QET_EXAMPLES_DIR) + QStringLiteral("/741.qet");
|
||||
const QByteArray pdf = exportPdf(project, QByteArray());
|
||||
QVERIFY(!pdf.isEmpty());
|
||||
const QByteArray year =
|
||||
"/CreationDate (D:" + QByteArray::number(QDate::currentDate().year());
|
||||
QVERIFY(pdf.contains(year));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_pdfreproducible)
|
||||
#include "tst_pdfreproducible.moc"
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "positionorder.h"
|
||||
|
||||
#include <QtTest>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
|
||||
/**
|
||||
PositionOrder -- the position comparisons behind renumbering elements
|
||||
(comparPos) and numbering terminals. std::sort needs a strict weak
|
||||
ordering; the old comparisons used "<=" and a 1 px tolerance, which
|
||||
are not one, so two elements at the same position (or three terminals
|
||||
a fraction apart) were undefined behaviour.
|
||||
*/
|
||||
class tst_positionorder : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
using Less = std::function<bool(const QPointF &, const QPointF &)>;
|
||||
|
||||
static QList<QPair<QString, Less>> orders()
|
||||
{
|
||||
return {{QStringLiteral("xThenY"), PositionOrder::xThenY},
|
||||
{QStringLiteral("yThenX"), PositionOrder::yThenX},
|
||||
{QStringLiteral("roundedXThenY"), PositionOrder::roundedXThenY},
|
||||
{QStringLiteral("roundedYThenX"), PositionOrder::roundedYThenX}};
|
||||
}
|
||||
|
||||
private slots:
|
||||
// Nothing is before itself, and equal points are before each other
|
||||
// in neither direction: what "<=" broke.
|
||||
void equalPointsAreNotOrdered()
|
||||
{
|
||||
const QPointF p(100, 200), q(100, 200);
|
||||
for (const auto &order : orders()) {
|
||||
QVERIFY2(!order.second(p, p), qPrintable(order.first));
|
||||
QVERIFY2(!order.second(p, q), qPrintable(order.first));
|
||||
QVERIFY2(!order.second(q, p), qPrintable(order.first));
|
||||
}
|
||||
}
|
||||
|
||||
void leftToRightThenTopToBottom()
|
||||
{
|
||||
QVERIFY(PositionOrder::xThenY(QPointF(10, 500), QPointF(20, 0)));
|
||||
QVERIFY(!PositionOrder::xThenY(QPointF(20, 0), QPointF(10, 500)));
|
||||
QVERIFY(PositionOrder::xThenY(QPointF(10, 0), QPointF(10, 5)));
|
||||
QVERIFY(!PositionOrder::xThenY(QPointF(10, 5), QPointF(10, 0)));
|
||||
}
|
||||
|
||||
void topToBottomThenLeftToRight()
|
||||
{
|
||||
QVERIFY(PositionOrder::yThenX(QPointF(500, 10), QPointF(0, 20)));
|
||||
QVERIFY(!PositionOrder::yThenX(QPointF(0, 20), QPointF(500, 10)));
|
||||
QVERIFY(PositionOrder::yThenX(QPointF(0, 10), QPointF(5, 10)));
|
||||
QVERIFY(!PositionOrder::yThenX(QPointF(5, 10), QPointF(0, 10)));
|
||||
}
|
||||
|
||||
// Items a fraction of a pixel apart (placed with Ctrl, or after a
|
||||
// rotation) count as aligned, and are then ordered on the other axis.
|
||||
void roundedTreatsFractionsAsAligned()
|
||||
{
|
||||
QVERIFY(PositionOrder::roundedXThenY(QPointF(10.2, 0), QPointF(9.8, 5)));
|
||||
QVERIFY(!PositionOrder::roundedXThenY(QPointF(9.8, 5), QPointF(10.2, 0)));
|
||||
QVERIFY(PositionOrder::roundedYThenX(QPointF(0, 10.2), QPointF(5, 9.8)));
|
||||
QVERIFY(!PositionOrder::roundedYThenX(QPointF(5, 9.8), QPointF(0, 10.2)));
|
||||
}
|
||||
|
||||
// The three terminals that broke the tolerance comparison: with
|
||||
// "within 1 px counts as aligned", a < b (by y), b < c (by y) and
|
||||
// c < a (by x), a cycle. Rounded, they are simply ordered by x.
|
||||
void roundedIsTransitiveWhereToleranceWasNot()
|
||||
{
|
||||
const QPointF a(2, 0), b(1.1, 1), c(0.2, 2);
|
||||
QVERIFY(PositionOrder::roundedXThenY(c, b));
|
||||
QVERIFY(PositionOrder::roundedXThenY(b, a));
|
||||
QVERIFY(PositionOrder::roundedXThenY(c, a));
|
||||
QVERIFY(!PositionOrder::roundedXThenY(a, c));
|
||||
}
|
||||
|
||||
// Every order is a strict weak ordering on a grid of awkward points:
|
||||
// irreflexive, asymmetric, transitive, and with transitive
|
||||
// equivalence. Checked by brute force on the whole set.
|
||||
void strictWeakOrdering_data()
|
||||
{
|
||||
QTest::addColumn<QString>("name");
|
||||
for (const auto &order : orders())
|
||||
QTest::newRow(qPrintable(order.first)) << order.first;
|
||||
}
|
||||
|
||||
void strictWeakOrdering()
|
||||
{
|
||||
QFETCH(QString, name);
|
||||
Less less;
|
||||
for (const auto &order : orders())
|
||||
if (order.first == name) less = order.second;
|
||||
QVERIFY(less);
|
||||
|
||||
QList<QPointF> points;
|
||||
const QList<qreal> values{-1, -0.6, -0.4, 0, 0.2, 0.5, 0.9, 1, 1.1, 2, 10.5, 61.3, 61.7};
|
||||
for (qreal x : values)
|
||||
for (qreal y : values)
|
||||
points << QPointF(x, y);
|
||||
points << points.first() << QPointF(2, 0) << QPointF(1.1, 1) << QPointF(0.2, 2);
|
||||
|
||||
auto equiv = [&](const QPointF &p, const QPointF &q) { return !less(p, q) && !less(q, p); };
|
||||
for (const QPointF &p : points) {
|
||||
QVERIFY(!less(p, p));
|
||||
for (const QPointF &q : points) {
|
||||
if (less(p, q)) QVERIFY(!less(q, p));
|
||||
for (const QPointF &r : points) {
|
||||
if (less(p, q) && less(q, r)) QVERIFY2(less(p, r), "transitive");
|
||||
if (equiv(p, q) && equiv(q, r)) QVERIFY2(equiv(p, r), "equivalence transitive");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// And std::sort on it ends sorted, with the list intact.
|
||||
QList<QPointF> sorted = points;
|
||||
std::sort(sorted.begin(), sorted.end(), less);
|
||||
QCOMPARE(sorted.size(), points.size());
|
||||
for (int i = 1; i < sorted.size(); ++i)
|
||||
QVERIFY(!less(sorted.at(i), sorted.at(i - 1)));
|
||||
}
|
||||
|
||||
// Twenty items at one position, as pasting at the origin leaves
|
||||
// them: a list std::sort could scan past the end of with "<=".
|
||||
void manyEqualPositionsSort()
|
||||
{
|
||||
QList<QPointF> points(20, QPointF(0, 0));
|
||||
points << QPointF(-10, 0) << QPointF(10, 0);
|
||||
std::sort(points.begin(), points.end(), PositionOrder::xThenY);
|
||||
QCOMPARE(points.size(), 22);
|
||||
QCOMPARE(points.first(), QPointF(-10, 0));
|
||||
QCOMPARE(points.last(), QPointF(10, 0));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_positionorder)
|
||||
#include "tst_positionorder.moc"
|
||||
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// With "one text per potential" on, the potential's number is drawn on its
|
||||
// longest conductor. fixtures/one_text_per_potential_tie.qet has two
|
||||
// conductors of the same, greatest length in one potential: which of them
|
||||
// carried the number used to follow pointer order, so the same file
|
||||
// exported twice could put it on either. Runs the real binary, in separate
|
||||
// processes, since that order only changes from one run to the next.
|
||||
class tst_potentialtextcarrier : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
// Export the fixture to SVG and return where the number is drawn: the
|
||||
// transform of the group holding it. Empty if it is not drawn exactly
|
||||
// once.
|
||||
QString carrier(int run)
|
||||
{
|
||||
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(run));
|
||||
const QString out = m_dir.filePath(QStringLiteral("svg%1").arg(run));
|
||||
QDir().mkpath(home);
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
env.insert(QStringLiteral("TMPDIR"), home);
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--export-svg"),
|
||||
QFINDTESTDATA("fixtures/one_text_per_potential_tie.qet"), out});
|
||||
if (!proc.waitForFinished(60000) || proc.exitCode() != 0)
|
||||
return {};
|
||||
|
||||
const QStringList svgs = QDir(out).entryList({QStringLiteral("*.svg")});
|
||||
if (svgs.size() != 1)
|
||||
return {};
|
||||
QFile f(QDir(out).filePath(svgs.first()));
|
||||
if (!f.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
const QString svg = QString::fromUtf8(f.readAll());
|
||||
|
||||
static const QRegularExpression re(
|
||||
QStringLiteral("transform=\"([^\"]*)\"[^<]*>\\s*<text[^>]*>42</text>"));
|
||||
QStringList found;
|
||||
auto it = re.globalMatch(svg);
|
||||
while (it.hasNext())
|
||||
found << it.next().captured(1);
|
||||
return found.size() == 1 ? found.first() : QString();
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
// Before the fix about half the runs differed from the first, so
|
||||
// eight agreeing runs leave a 1 in 128 chance of a false pass.
|
||||
void sameCarrierEveryRun()
|
||||
{
|
||||
const QString first = carrier(0);
|
||||
QVERIFY2(!first.isEmpty(), "the number is not drawn exactly once");
|
||||
for (int run = 1; run < 8; ++run)
|
||||
QCOMPARE(carrier(run), first);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_potentialtextcarrier)
|
||||
#include "tst_potentialtextcarrier.moc"
|
||||
@@ -271,7 +271,7 @@ void tst_qeticons::lightIconsStayLightInDarkTheme()
|
||||
}
|
||||
|
||||
/**
|
||||
QET's own vector icons live in ico/scalable/, the Breeze ones in
|
||||
QET's own vector icons live in ico/qet/scalable/, the Breeze ones in
|
||||
ico/breeze/: one file for every size from the toolbar up. Line art
|
||||
has a recolored copy in the dark theme; colored art has none and is
|
||||
inherited. Each must resolve in both themes at 22, 24, 32 and 64 px,
|
||||
@@ -415,7 +415,7 @@ void tst_qeticons::breezeIconsAreSharpAtEverySize()
|
||||
}
|
||||
|
||||
/**
|
||||
A traced icon (ico/traced/, misc/make_icon_themes.py) is the PNG copied
|
||||
A traced icon (ico/qet/traced/, misc/make_icon_themes.py) is the PNG copied
|
||||
pixel by pixel at 16 and 22 pixels, so those files and the 24 pixel
|
||||
canvas must be drawn unscaled. At every size, at 1x and at 2x, Qt must
|
||||
pick the folder's own file.
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
// - information values were trimmed on save but not on load, so a label
|
||||
// with stray spaces kept them in its displayed copy until the project
|
||||
// was opened again (m_000.qet).
|
||||
// Runs the real binary's --resave twice on every example, and on a
|
||||
// project whose title block holds a value that is a single space (#973).
|
||||
// Runs the real binary's --resave twice on every example, on a project
|
||||
// whose title block holds a value that is a single space (#973), and on
|
||||
// one that used to crash on opening (bugtracker #345).
|
||||
class tst_resaveunchanged : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -83,10 +84,47 @@ private slots:
|
||||
QVERIFY2(a == b, "the second save changed the file");
|
||||
}
|
||||
|
||||
// A contact not linked to a coil, carrying a text built from %{label}:
|
||||
// opening it dereferenced the missing coil and crashed (bugtracker #345).
|
||||
// The fixture is a blank project with one such contact.
|
||||
void unlinkedContactLabelTextOpens()
|
||||
{
|
||||
const QString fixture = QFINDTESTDATA("fixtures/unlinked_contact_label.qet");
|
||||
QVERIFY(!fixture.isEmpty());
|
||||
const QString first = resave(fixture);
|
||||
QVERIFY2(!first.isEmpty(), "--resave failed: QElectroTech crashed opening the project");
|
||||
const QString second = resave(first);
|
||||
QVERIFY2(!second.isEmpty(), "second --resave failed");
|
||||
QVERIFY2(read(first) == read(second), "the second save changed the file");
|
||||
}
|
||||
|
||||
// %{machine_manufacturer_reference_auxiliary1..4} resolve like their
|
||||
// neighbours: they were missing from AssignVariables::replaceVariable()
|
||||
// and printed as literal text. The fixture has one terminal whose
|
||||
// four texts are "[%{machine_..._auxiliaryN}|%{manufacturer_reference_auxiliaryN}]",
|
||||
// saved by a build without the fix, so the literal text is in the file.
|
||||
void auxiliaryMachineReferenceResolves()
|
||||
{
|
||||
const QString fixture = QFINDTESTDATA("fixtures/aux_machine_reference.qet");
|
||||
QVERIFY(!fixture.isEmpty());
|
||||
const QString saved = resave(fixture);
|
||||
QVERIFY2(!saved.isEmpty(), "--resave failed");
|
||||
const QString xml = QString::fromUtf8(read(saved));
|
||||
for (int n = 1; n <= 4; ++n) {
|
||||
const QString shown = QStringLiteral("<text>[MMR-AUX%1|MR-AUX%1]</text>").arg(n);
|
||||
QVERIFY2(xml.contains(shown), qPrintable(shown + QStringLiteral(" not in the saved file")));
|
||||
}
|
||||
QVERIFY2(!xml.contains(QStringLiteral("<text>[%{machine")),
|
||||
"a machine manufacturer reference variable was left unresolved");
|
||||
}
|
||||
|
||||
// A title-block value that is a single space is kept through two saves
|
||||
// (#973), and a value with accents comes back as it went in.
|
||||
void singleSpaceValueKept()
|
||||
{
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
|
||||
QSKIP("QDomDocument::PreserveSpacingOnlyNodes needs Qt 6.5 (see QETProject::openFile)");
|
||||
#endif
|
||||
QByteArray xml = read(QStringLiteral(QET_EXAMPLES_DIR "/Projet_vierge.qet"));
|
||||
QVERIFY(xml.contains("<properties>"));
|
||||
xml.replace("<properties>",
|
||||
|
||||
@@ -97,6 +97,28 @@ private slots:
|
||||
QVERIFY(r.value(QStringLiteral("junk")).toArray().isEmpty());
|
||||
QVERIFY(r.value(QStringLiteral("badFolio")).toArray().isEmpty());
|
||||
}
|
||||
|
||||
// qet.deleteElement() takes the wires on the element's terminals with
|
||||
// it, as the Delete key does; they used to stay on the folio, attached
|
||||
// to an element that was gone.
|
||||
void deleteElementTakesItsWires()
|
||||
{
|
||||
const QJsonObject r = run(QStringLiteral(
|
||||
"var p = 'embed://import/probe/v2_fuse.elmt';\n"
|
||||
"var a = qet.addElement(0, p, 400, 400);\n"
|
||||
"var b = qet.addElement(0, p, 470, 490);\n"
|
||||
"qet.addConductor(0, a, 0, b, 0);\n"
|
||||
"var before = qet.conductorUuids(0).length;\n"
|
||||
"var deleted = qet.deleteElement(0, a);\n"
|
||||
"var dangling = qet.conductorUuids(0).filter(function (u) {\n"
|
||||
" return qet.conductorEnds(0, u).some(function (e) { return e.indexOf(a) === 0; }); });\n"
|
||||
"qet.log('PROBE ' + JSON.stringify({deleted: deleted, before: before,\n"
|
||||
" after: qet.conductorUuids(0).length, dangling: dangling.length}));\n"));
|
||||
QVERIFY2(!r.isEmpty(), "the script logged nothing");
|
||||
QVERIFY(r.value(QStringLiteral("deleted")).toBool());
|
||||
QCOMPARE(r.value(QStringLiteral("dangling")).toInt(), 0);
|
||||
QCOMPARE(r.value(QStringLiteral("after")).toInt(), r.value(QStringLiteral("before")).toInt() - 1);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_scriptconductoruuid)
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// qet.currentFolio() is the folio on screen in the editor. Through --run
|
||||
// there is none, so it stands in with the first folio, or -1 when the
|
||||
// project has no folio. --run also keeps one undo step per call: only a run
|
||||
// started from the editor is grouped into one, and a headless script that
|
||||
// calls qet.undo() itself must go on working.
|
||||
class tst_scriptcurrentfolio : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
// Run @p script on @p project in a sandbox of its own and return the
|
||||
// JSON object it logged.
|
||||
QJsonObject run(const QString &script, const QString &project)
|
||||
{
|
||||
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QDir().mkpath(home);
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly)) return {};
|
||||
f.write(script.toUtf8());
|
||||
f.close();
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--run"), path, project});
|
||||
if (!proc.waitForFinished(120000)) return {};
|
||||
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
|
||||
+ proc.readAllStandardError());
|
||||
const QString mark = QStringLiteral("PROBE ");
|
||||
for (const QString &line : out.split(QLatin1Char('\n'))) {
|
||||
const int i = line.indexOf(mark);
|
||||
if (i >= 0)
|
||||
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void firstFolioWithoutAView()
|
||||
{
|
||||
const QJsonObject r = run(QStringLiteral(
|
||||
"qet.log('PROBE ' + JSON.stringify({current: qet.currentFolio(),\n"
|
||||
" folios: qet.folioCount()}));\n"),
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
|
||||
QVERIFY2(!r.isEmpty(), "the script logged nothing");
|
||||
QVERIFY(r.value(QStringLiteral("folios")).toInt() > 1);
|
||||
QCOMPARE(r.value(QStringLiteral("current")).toInt(), 0);
|
||||
}
|
||||
|
||||
void noFolioIsMinusOne()
|
||||
{
|
||||
const QString project = m_dir.filePath(QStringLiteral("empty.qet"));
|
||||
QFile out(project);
|
||||
QVERIFY(out.open(QIODevice::WriteOnly));
|
||||
out.write("<project version=\"0.100.0\" title=\"empty\">\n</project>\n");
|
||||
out.close();
|
||||
|
||||
const QJsonObject r = run(QStringLiteral(
|
||||
"qet.log('PROBE ' + JSON.stringify({current: qet.currentFolio(),\n"
|
||||
" folios: qet.folioCount()}));\n"), project);
|
||||
QVERIFY2(!r.isEmpty(), "the script logged nothing");
|
||||
QCOMPARE(r.value(QStringLiteral("folios")).toInt(), 0);
|
||||
QCOMPARE(r.value(QStringLiteral("current")).toInt(), -1);
|
||||
}
|
||||
|
||||
// The list is read from the build itself: the calls a script uses
|
||||
// are in it with their parameter names, each once.
|
||||
void apiSignaturesListed()
|
||||
{
|
||||
const QJsonObject r = run(QStringLiteral(
|
||||
"qet.log('PROBE ' + JSON.stringify({sigs: qet.apiSignatures()}));\n"),
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
|
||||
QVERIFY2(!r.isEmpty(), "the script logged nothing");
|
||||
QStringList sigs;
|
||||
for (const QJsonValue &v : r.value(QStringLiteral("sigs")).toArray())
|
||||
sigs << v.toString();
|
||||
QVERIFY(sigs.size() > 100);
|
||||
QVERIFY(sigs.contains(QStringLiteral("int currentFolio()")));
|
||||
QVERIFY(sigs.contains(QStringLiteral(
|
||||
"int addText(int folioIndex, QString text, double x, double y)")));
|
||||
QVERIFY(sigs.contains(QStringLiteral(
|
||||
"bool exportPdf(QString output, bool showTerminals)")));
|
||||
QCOMPARE(sigs.filter(QStringLiteral(" exportPdf(")).size(), 1);
|
||||
QVERIFY(sigs.filter(QStringLiteral("setUndoGrouped")).isEmpty());
|
||||
}
|
||||
|
||||
void headlessUndoStaysPerCall()
|
||||
{
|
||||
const QJsonObject r = run(QStringLiteral(
|
||||
"var f = qet.currentFolio();\n"
|
||||
"var before = qet.texts(f).length;\n"
|
||||
"qet.addText(f, 'one', 40, 40);\n"
|
||||
"qet.addText(f, 'two', 40, 80);\n"
|
||||
"var undone = qet.undo();\n"
|
||||
"qet.log('PROBE ' + JSON.stringify({undone: undone,\n"
|
||||
" added: qet.texts(f).length - before}));\n"),
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
|
||||
QVERIFY2(!r.isEmpty(), "the script logged nothing");
|
||||
QCOMPARE(r.value(QStringLiteral("undone")).toBool(), true);
|
||||
QCOMPARE(r.value(QStringLiteral("added")).toInt(), 1);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_scriptcurrentfolio)
|
||||
#include "tst_scriptcurrentfolio.moc"
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
/**
|
||||
The project database keeps the width of a free text (text_width in the
|
||||
independent_text table, NULL for the automatic width), read here with
|
||||
qet.query() from a script run on fixtures/free_text_width.qet.
|
||||
*/
|
||||
class tst_scriptfreetextwidth : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
/// The rows of the independent_text table, by the first word of the text.
|
||||
QHash<QString, QJsonObject> rows()
|
||||
{
|
||||
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QDir().mkpath(home);
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly)) return {};
|
||||
f.write("qet.log('PROBE ' + JSON.stringify(qet.query("
|
||||
"'SELECT text, text_width, width, height FROM independent_text')));\n");
|
||||
f.close();
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--run"), path, QFINDTESTDATA("fixtures/free_text_width.qet")});
|
||||
if (!proc.waitForFinished(60000)) return {};
|
||||
|
||||
const QString out = QString::fromUtf8(proc.readAllStandardOutput() + proc.readAllStandardError());
|
||||
const QString mark = QStringLiteral("PROBE ");
|
||||
QHash<QString, QJsonObject> result;
|
||||
for (const QString &line : out.split(QLatin1Char('\n'))) {
|
||||
const int i = line.indexOf(mark);
|
||||
if (i < 0) continue;
|
||||
const QJsonArray array = QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).array();
|
||||
for (const QJsonValue &row : array)
|
||||
result.insert(row.toObject().value(QStringLiteral("text")).toString().section(QLatin1Char(' '), 0, 0),
|
||||
row.toObject());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
QVERIFY(!QFINDTESTDATA("fixtures/free_text_width.qet").isEmpty());
|
||||
}
|
||||
|
||||
void widthIsInTheDatabase()
|
||||
{
|
||||
const QHash<QString, QJsonObject> r = rows();
|
||||
QCOMPARE(r.size(), 3);
|
||||
|
||||
QCOMPARE(r.value(QStringLiteral("FreeAlpha")).value(QStringLiteral("text_width")).toDouble(), 70.0);
|
||||
QCOMPARE(r.value(QStringLiteral("CentAlpha")).value(QStringLiteral("text_width")).toDouble(), 120.0);
|
||||
//The automatic width is NULL
|
||||
const QJsonValue open = r.value(QStringLiteral("OpenAlpha")).value(QStringLiteral("text_width"));
|
||||
QVERIFY2(open.isNull() || open.toString().isEmpty(), qPrintable(open.toVariant().toString()));
|
||||
|
||||
//The box of the row is the wrapped one: taller than one line
|
||||
QVERIFY(r.value(QStringLiteral("FreeAlpha")).value(QStringLiteral("height")).toDouble()
|
||||
> 2 * r.value(QStringLiteral("OpenAlpha")).value(QStringLiteral("height")).toDouble());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_scriptfreetextwidth)
|
||||
#include "tst_scriptfreetextwidth.moc"
|
||||
@@ -0,0 +1,164 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QAction>
|
||||
|
||||
#include "scripting/scriptheader.h"
|
||||
#include "shortcutmanager.h"
|
||||
|
||||
// The header that turns a stored script into a button, and taking a
|
||||
// command out of ShortcutManager again when its script is deleted.
|
||||
class tst_scriptheader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
static QString script(const QString &lines)
|
||||
{
|
||||
return QStringLiteral("// ==QETScript==\n") + lines
|
||||
+ QStringLiteral("// ==/QETScript==\nqet.log('x');\n");
|
||||
}
|
||||
|
||||
private slots:
|
||||
void fullHeader()
|
||||
{
|
||||
const ScriptHeader h = ScriptHeader::parse(script(QStringLiteral(
|
||||
"// @name Add revision note\n"
|
||||
"// @icon note.svg\n"
|
||||
"// @tooltip Puts a note on the folio\n"
|
||||
"// @shortcut Ctrl+Alt+R\n"
|
||||
"// @context selection\n"
|
||||
"// @api 1\n")), QStringLiteral("add-note"));
|
||||
QVERIFY2(h.isValid(), qPrintable(h.error));
|
||||
QCOMPARE(h.id, QStringLiteral("add-note"));
|
||||
QCOMPARE(h.name, QStringLiteral("Add revision note"));
|
||||
QCOMPARE(h.icon, QStringLiteral("note.svg"));
|
||||
QCOMPARE(h.tooltip, QStringLiteral("Puts a note on the folio"));
|
||||
QCOMPARE(h.shortcut, QStringLiteral("Ctrl+Alt+R"));
|
||||
QCOMPARE(h.context, QStringLiteral("selection"));
|
||||
QCOMPARE(h.api, 1);
|
||||
}
|
||||
|
||||
void nameAloneIsEnough()
|
||||
{
|
||||
const ScriptHeader h = ScriptHeader::parse(
|
||||
script(QStringLiteral("// @name Just a name\n")), QStringLiteral("a"));
|
||||
QVERIFY2(h.isValid(), qPrintable(h.error));
|
||||
QCOMPARE(h.context, QStringLiteral("canvas"));
|
||||
QVERIFY(h.icon.isEmpty());
|
||||
}
|
||||
|
||||
void refused_data()
|
||||
{
|
||||
QTest::addColumn<QString>("text");
|
||||
QTest::addColumn<QString>("error");
|
||||
QTest::newRow("no header") << QStringLiteral("qet.log('x');\n")
|
||||
<< QStringLiteral("no // ==QETScript== header");
|
||||
QTest::newRow("no name") << script(QStringLiteral("// @icon x.svg\n"))
|
||||
<< QStringLiteral("@name is required");
|
||||
QTest::newRow("typo") << script(QStringLiteral("// @name A\n// @shortcutt Ctrl+K\n"))
|
||||
<< QStringLiteral("unknown header key @shortcutt");
|
||||
QTest::newRow("context") << script(QStringLiteral("// @name A\n// @context wires\n"))
|
||||
<< QStringLiteral("@context must be one of: canvas, selection, conductor");
|
||||
QTest::newRow("api") << script(QStringLiteral("// @name A\n// @api 2\n"))
|
||||
<< QStringLiteral("@api 2 is not supported by this version (1 is)");
|
||||
}
|
||||
|
||||
void refused()
|
||||
{
|
||||
QFETCH(QString, text);
|
||||
QFETCH(QString, error);
|
||||
const ScriptHeader h = ScriptHeader::parse(text, QStringLiteral("a"));
|
||||
QVERIFY(!h.isValid());
|
||||
QCOMPARE(h.error, error);
|
||||
}
|
||||
|
||||
// Only the header block counts: a "// @name" further down is code.
|
||||
void onlyTheBlock()
|
||||
{
|
||||
const ScriptHeader h = ScriptHeader::parse(
|
||||
script(QStringLiteral("// @name Real\n"))
|
||||
+ QStringLiteral("// @name Not this one\n// @bogus key\n"),
|
||||
QStringLiteral("a"));
|
||||
QVERIFY2(h.isValid(), qPrintable(h.error));
|
||||
QCOMPARE(h.name, QStringLiteral("Real"));
|
||||
}
|
||||
|
||||
// What the script manager writes reads back as what was typed, and
|
||||
// leaves the script's code as it was.
|
||||
void composeReadsBack()
|
||||
{
|
||||
const QString body = QStringLiteral("var f = qet.currentFolio();\nqet.addText(f, 'x', 0, 0);\n");
|
||||
const QString text = ScriptHeader::compose(
|
||||
QStringLiteral(" Add note "), QStringLiteral("note.svg"),
|
||||
QStringLiteral("A tip"), QStringLiteral("Ctrl+Alt+N"),
|
||||
QStringLiteral("conductor"), body);
|
||||
const ScriptHeader h = ScriptHeader::parse(text, QStringLiteral("add-note"));
|
||||
QVERIFY2(h.isValid(), qPrintable(h.error));
|
||||
QCOMPARE(h.name, QStringLiteral("Add note"));
|
||||
QCOMPARE(h.icon, QStringLiteral("note.svg"));
|
||||
QCOMPARE(h.tooltip, QStringLiteral("A tip"));
|
||||
QCOMPARE(h.shortcut, QStringLiteral("Ctrl+Alt+N"));
|
||||
QCOMPARE(h.context, QStringLiteral("conductor"));
|
||||
QCOMPARE(ScriptHeader::bodyOf(text), body);
|
||||
|
||||
// empty fields and the default context are left out
|
||||
const QString bare = ScriptHeader::compose(QStringLiteral("A"), QString(), QString(),
|
||||
QString(), QStringLiteral("canvas"),
|
||||
QStringLiteral("qet.log(1);"));
|
||||
QCOMPARE(bare, QStringLiteral("// ==QETScript==\n// @name A\n// @api 1\n"
|
||||
"// ==/QETScript==\nqet.log(1);\n"));
|
||||
}
|
||||
|
||||
void bodyWithoutHeader()
|
||||
{
|
||||
QCOMPARE(ScriptHeader::bodyOf(QStringLiteral("qet.log(1);\n")),
|
||||
QStringLiteral("qet.log(1);\n"));
|
||||
}
|
||||
|
||||
void idFor()
|
||||
{
|
||||
QCOMPARE(ScriptHeader::idFor(QStringLiteral("Numéroter les fils !"), {}),
|
||||
QStringLiteral("numeroter-les-fils"));
|
||||
QCOMPARE(ScriptHeader::idFor(QStringLiteral("Add note"),
|
||||
{QStringLiteral("add-note"), QStringLiteral("add-note-2")}),
|
||||
QStringLiteral("add-note-3"));
|
||||
QCOMPARE(ScriptHeader::idFor(QStringLiteral("!!!"), {}), QStringLiteral("script"));
|
||||
}
|
||||
|
||||
// Two windows register one id; the id stays until both are gone, and a
|
||||
// script that comes back under a new name is listed by that name.
|
||||
void unregisterAction()
|
||||
{
|
||||
ShortcutManager &m = ShortcutManager::instance();
|
||||
const QString id = QStringLiteral("diagrameditor.script.tst");
|
||||
auto listed = [&m, &id]() -> QString {
|
||||
for (const ShortcutManager::ShortcutInfo &info : m.allShortcuts())
|
||||
if (info.id == id) return info.description;
|
||||
return QString();
|
||||
};
|
||||
|
||||
QAction a(QStringLiteral("Old name")), b(QStringLiteral("Old name"));
|
||||
m.registerAction(&a, id, QStringLiteral("Scripts"), QKeySequence());
|
||||
m.registerAction(&b, id, QStringLiteral("Scripts"), QKeySequence());
|
||||
QCOMPARE(listed(), QStringLiteral("Old name"));
|
||||
|
||||
m.unregisterAction(&a, id);
|
||||
QCOMPARE(listed(), QStringLiteral("Old name"));
|
||||
QCOMPARE(m.action(id, nullptr), &b);
|
||||
|
||||
m.unregisterAction(&b, id);
|
||||
QVERIFY(listed().isEmpty());
|
||||
QCOMPARE(m.action(id, nullptr), nullptr);
|
||||
|
||||
QAction c(QStringLiteral("New name"));
|
||||
m.registerAction(&c, id, QStringLiteral("Scripts"), QKeySequence());
|
||||
QCOMPARE(listed(), QStringLiteral("New name"));
|
||||
m.unregisterAction(&c, id);
|
||||
|
||||
// unknown id: nothing to do, nothing breaks
|
||||
m.unregisterAction(&c, QStringLiteral("diagrameditor.script.none"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_scriptheader)
|
||||
#include "tst_scriptheader.moc"
|
||||
@@ -0,0 +1,170 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTemporaryDir>
|
||||
#include <QUuid>
|
||||
|
||||
// qet.terminalPosition(folio, element, terminal) and qet.conductorPath(folio,
|
||||
// uuid): where a wire docks on a terminal and which way it leaves, and a
|
||||
// wire's drawn path by its uuid. Checked against each other on every
|
||||
// conductor of the fixture: a path starts and ends exactly where its two
|
||||
// terminals say a wire docks, and leaves each the way it faces. Runs a
|
||||
// script through the real binary's --run.
|
||||
class tst_scriptlayoutreads : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
// Run @p script on the fixture in a sandbox of its own and return the
|
||||
// JSON object it logged.
|
||||
QJsonObject run(const QString &script)
|
||||
{
|
||||
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QDir().mkpath(home);
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly)) return {};
|
||||
f.write(script.toUtf8());
|
||||
f.close();
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--run"), path,
|
||||
QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet")});
|
||||
if (!proc.waitForFinished(60000)) return {};
|
||||
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
|
||||
+ proc.readAllStandardError());
|
||||
const QString mark = QStringLiteral("PROBE ");
|
||||
for (const QString &line : out.split(QLatin1Char('\n'))) {
|
||||
const int i = line.indexOf(mark);
|
||||
if (i >= 0)
|
||||
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void pathsStartAndEndAtTheirTerminals()
|
||||
{
|
||||
const QJsonObject r = run(QStringLiteral(
|
||||
"var out = [];\n"
|
||||
"var uuids = qet.conductorUuids(0);\n"
|
||||
"for (var i = 0; i < uuids.length; i++) {\n"
|
||||
" var ends = qet.conductorEnds(0, uuids[i]);\n"
|
||||
" var t = ends.map(function (e) { var m = e.split(' terminal ');\n"
|
||||
" return qet.terminalPosition(0, m[0], parseInt(m[1], 10)); });\n"
|
||||
" out.push({path: qet.conductorPath(0, uuids[i]), t: t});\n"
|
||||
"}\n"
|
||||
"qet.log('PROBE ' + JSON.stringify({wires: out,\n"
|
||||
" unknown: qet.conductorPath(0, '{00000000-0000-0000-0000-000000000001}'),\n"
|
||||
" junk: qet.conductorPath(0, 'not a uuid'),\n"
|
||||
" badFolio: qet.conductorPath(99, uuids[0]),\n"
|
||||
" badTerminal: qet.terminalPosition(0, qet.elementUuids(0)[0], 999)}));\n"));
|
||||
QVERIFY2(!r.isEmpty(), "the script logged nothing");
|
||||
|
||||
const QJsonArray wires = r.value(QStringLiteral("wires")).toArray();
|
||||
QCOMPARE(wires.size(), 7); // the fixture's conductors
|
||||
const QHash<QString, QPointF> step{{QStringLiteral("n"), {0, -1}},
|
||||
{QStringLiteral("e"), {1, 0}},
|
||||
{QStringLiteral("s"), {0, 1}},
|
||||
{QStringLiteral("w"), {-1, 0}}};
|
||||
for (const QJsonValue &v : wires) {
|
||||
const QJsonArray path = v.toObject().value(QStringLiteral("path")).toArray();
|
||||
const QJsonArray t = v.toObject().value(QStringLiteral("t")).toArray();
|
||||
QVERIFY(path.size() >= 2);
|
||||
QCOMPARE(t.size(), 2);
|
||||
const QJsonObject ends[2] = {path.first().toObject(), path.last().toObject()};
|
||||
const QJsonObject next[2] = {path.at(1).toObject(), path.at(path.size() - 2).toObject()};
|
||||
for (int k = 0; k < 2; ++k) {
|
||||
const QJsonObject term = t.at(k).toObject();
|
||||
QCOMPARE(ends[k].value(QStringLiteral("x")).toDouble(),
|
||||
term.value(QStringLiteral("x")).toDouble());
|
||||
QCOMPARE(ends[k].value(QStringLiteral("y")).toDouble(),
|
||||
term.value(QStringLiteral("y")).toDouble());
|
||||
// the first step out of a terminal goes the way it faces
|
||||
const QString facing = term.value(QStringLiteral("facing")).toString();
|
||||
QVERIFY2(step.contains(facing), qPrintable(facing));
|
||||
const QPointF d(next[k].value(QStringLiteral("x")).toDouble()
|
||||
- ends[k].value(QStringLiteral("x")).toDouble(),
|
||||
next[k].value(QStringLiteral("y")).toDouble()
|
||||
- ends[k].value(QStringLiteral("y")).toDouble());
|
||||
if (d.isNull()) continue;
|
||||
QVERIFY2(d.x() * step[facing].x() + d.y() * step[facing].y() > 0,
|
||||
qPrintable(facing));
|
||||
}
|
||||
}
|
||||
QVERIFY(r.value(QStringLiteral("unknown")).toArray().isEmpty());
|
||||
QVERIFY(r.value(QStringLiteral("junk")).toArray().isEmpty());
|
||||
QVERIFY(r.value(QStringLiteral("badFolio")).toArray().isEmpty());
|
||||
QVERIFY(r.value(QStringLiteral("badTerminal")).toObject().isEmpty());
|
||||
}
|
||||
|
||||
void pathMatchesConductorSegments()
|
||||
{
|
||||
// Where conductorSegments() can name the wire, both give the
|
||||
// same points.
|
||||
const QJsonObject r = run(QStringLiteral(
|
||||
"var same = 0, compared = 0, lines = qet.conductors(0);\n"
|
||||
"var uuids = qet.conductorUuids(0);\n"
|
||||
"for (var i = 0; i < uuids.length; i++) {\n"
|
||||
" var e = qet.conductorEnds(0, uuids[i])[0], n = 0;\n"
|
||||
" for (var l = 0; l < lines.length; l++) {\n"
|
||||
" var p = lines[l].split(' : ')[0].split(' -- ');\n"
|
||||
" if (p[0] === e || p[1] === e) n++;\n"
|
||||
" }\n"
|
||||
" if (n !== 1) continue;\n"
|
||||
" var m = e.split(' terminal ');\n"
|
||||
" var segs = qet.conductorSegments(0, m[0], parseInt(m[1], 10));\n"
|
||||
" var pts = [];\n"
|
||||
" segs.forEach(function (s, k) {\n"
|
||||
" var c = s.match(/\\(([^,]+),([^)]+)\\)-\\(([^,]+),([^)]+)\\)/);\n"
|
||||
" if (k === 0) pts.push([+c[1], +c[2]]);\n"
|
||||
" pts.push([+c[3], +c[4]]); });\n"
|
||||
" var path = qet.conductorPath(0, uuids[i]).map(function (q) { return [q.x, q.y]; });\n"
|
||||
" compared++;\n"
|
||||
" if (JSON.stringify(pts) === JSON.stringify(path)) same++;\n"
|
||||
"}\n"
|
||||
"qet.log('PROBE ' + JSON.stringify({same: same, compared: compared}));\n"));
|
||||
QVERIFY(r.value(QStringLiteral("compared")).toInt() > 0);
|
||||
QCOMPARE(r.value(QStringLiteral("same")).toInt(), r.value(QStringLiteral("compared")).toInt());
|
||||
}
|
||||
|
||||
void facingTurnsWithTheElement()
|
||||
{
|
||||
const QJsonObject r = run(QStringLiteral(
|
||||
"var el = qet.elementUuids(0)[0];\n"
|
||||
"var before = qet.terminalPosition(0, el, 0).facing;\n"
|
||||
"qet.rotateElement(0, el, 90);\n"
|
||||
"var after = qet.terminalPosition(0, el, 0).facing;\n"
|
||||
"qet.log('PROBE ' + JSON.stringify({before: before, after: after}));\n"));
|
||||
const QString order = QStringLiteral("nesw");
|
||||
const int b = order.indexOf(r.value(QStringLiteral("before")).toString());
|
||||
const int a = order.indexOf(r.value(QStringLiteral("after")).toString());
|
||||
QVERIFY(b >= 0 && a >= 0);
|
||||
QCOMPARE(a, (b + 1) % 4); // a quarter turn clockwise
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_scriptlayoutreads)
|
||||
|
||||
#include "tst_scriptlayoutreads.moc"
|
||||
@@ -0,0 +1,239 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// Mirroring a symbol on its folio (#1335), through the real binary's --run
|
||||
// on perceuse.qet: qet.mirrorElement() pushes the same undo command as
|
||||
// Edit > Miroir horizontal / vertical. A horizontal mirror swaps the
|
||||
// terminals left and right and turns the ones facing east to face west; a
|
||||
// vertical one does the same top and bottom; mirroring twice the same way
|
||||
// puts the symbol back exactly; and a save keeps the mirror.
|
||||
class tst_scriptmirror : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
// Run @p script on @p project in a sandbox of its own and return the
|
||||
// JSON object it logged.
|
||||
QJsonObject run(const QString &script, const QString &project)
|
||||
{
|
||||
const QString path = m_dir.filePath(QStringLiteral("probe.js"));
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QDir().mkpath(home);
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly)) return {};
|
||||
f.write(script.toUtf8());
|
||||
f.close();
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH),
|
||||
{QStringLiteral("--run"), path, project});
|
||||
if (!proc.waitForFinished(120000)) return {};
|
||||
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
|
||||
+ proc.readAllStandardError());
|
||||
const QString mark = QStringLiteral("PROBE ");
|
||||
for (const QString &line : out.split(QLatin1Char('\n'))) {
|
||||
const int i = line.indexOf(mark);
|
||||
if (i >= 0)
|
||||
return QJsonDocument::fromJson(line.mid(i + mark.size()).toUtf8()).object();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static QString facing(const QJsonArray &terminals, int i)
|
||||
{
|
||||
return terminals.at(i).toObject().value(QStringLiteral("facing")).toString();
|
||||
}
|
||||
static double coord(const QJsonArray &terminals, int i, const char *axis)
|
||||
{
|
||||
return terminals.at(i).toObject().value(QLatin1String(axis)).toDouble();
|
||||
}
|
||||
|
||||
// The facing of a terminal once mirrored: across a vertical line
|
||||
// (horizontal mirror) east and west swap, across a horizontal one north
|
||||
// and south do.
|
||||
static QString mirrored(const QString &facing, bool vertical)
|
||||
{
|
||||
const QString swap = vertical ? QStringLiteral("ns") : QStringLiteral("ew");
|
||||
const int i = swap.indexOf(facing);
|
||||
return i < 0 ? facing : QString(swap.at(1 - i));
|
||||
}
|
||||
|
||||
// @p after is @p before mirrored horizontally (or vertically): the
|
||||
// terminals face the mirrored way, keep the other coordinate, and are
|
||||
// mirrored across one same line, so x + x' (or y + y') is the same for
|
||||
// all of them.
|
||||
static void checkMirrored(const QJsonArray &before, const QJsonArray &after, bool vertical)
|
||||
{
|
||||
QCOMPARE(after.size(), before.size());
|
||||
const char *across = vertical ? "y" : "x";
|
||||
const char *along = vertical ? "x" : "y";
|
||||
const double axis = coord(before, 0, across) + coord(after, 0, across);
|
||||
const double shift = coord(after, 0, along) - coord(before, 0, along);
|
||||
for (int i = 0; i < before.size(); ++i) {
|
||||
QCOMPARE(facing(after, i), mirrored(facing(before, i), vertical));
|
||||
QCOMPARE(coord(before, i, across) + coord(after, i, across), axis);
|
||||
QCOMPARE(coord(after, i, along) - coord(before, i, along), shift);
|
||||
}
|
||||
}
|
||||
|
||||
static QString script(const QString &body)
|
||||
{
|
||||
return QStringLiteral(
|
||||
"function terms(f, el) {\n"
|
||||
" var r = [], n = qet.elementTerminals(f, el).length;\n"
|
||||
" for (var i = 0; i < n; i++) r.push(qet.terminalPosition(f, el, i));\n"
|
||||
" return r;\n"
|
||||
"}\n") + body;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void mirrorBackAndSave()
|
||||
{
|
||||
const QString saved = m_dir.filePath(QStringLiteral("mirrored.qet"));
|
||||
const QJsonObject r = run(script(QStringLiteral(
|
||||
// a symbol with terminals at different x and different y, one
|
||||
// of them facing east or west
|
||||
"var pick = null;\n"
|
||||
"for (var f = 0; f < qet.folioCount() && !pick; f++) {\n"
|
||||
" var els = qet.elementUuids(f);\n"
|
||||
" for (var e = 0; e < els.length && !pick; e++) {\n"
|
||||
" var t = terms(f, els[e]);\n"
|
||||
" if (t.length < 2) continue;\n"
|
||||
" var side = t.some(function (p) { return p.facing === 'e' || p.facing === 'w'; });\n"
|
||||
" var xs = t.some(function (p) { return p.x !== t[0].x; });\n"
|
||||
" var ys = t.some(function (p) { return p.y !== t[0].y; });\n"
|
||||
" if (side && xs && ys) pick = {f: f, e: els[e]};\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"var f = pick.f, el = pick.e, out = {element: el};\n"
|
||||
"out.before = terms(f, el);\n"
|
||||
"qet.mirrorElement(f, el, false);\n"
|
||||
"out.h = terms(f, el); out.hMirror = qet.elementMirror(f, el);\n"
|
||||
"qet.mirrorElement(f, el, false);\n"
|
||||
"out.back = terms(f, el); out.backMirror = qet.elementMirror(f, el);\n"
|
||||
"qet.mirrorElement(f, el, true);\n"
|
||||
"out.v = terms(f, el); out.vMirror = qet.elementMirror(f, el);\n"
|
||||
"qet.undo();\n"
|
||||
"out.undone = terms(f, el);\n"
|
||||
"qet.mirrorElement(f, el, true);\n"
|
||||
"out.folio = f;\n"
|
||||
"out.saved = qet.save(%1);\n"
|
||||
"qet.log('PROBE ' + JSON.stringify(out));\n")
|
||||
.arg(QString::fromUtf8(QJsonDocument(QJsonArray{saved}).toJson(QJsonDocument::Compact)).mid(1).chopped(1))),
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
|
||||
|
||||
QVERIFY2(!r.isEmpty(), "the script logged nothing");
|
||||
const QJsonArray before = r.value(QStringLiteral("before")).toArray();
|
||||
QVERIFY(before.size() >= 2);
|
||||
|
||||
QVERIFY(!r.value(QStringLiteral("hMirror")).toString().isEmpty());
|
||||
checkMirrored(before, r.value(QStringLiteral("h")).toArray(), false);
|
||||
|
||||
QCOMPARE(r.value(QStringLiteral("backMirror")).toString(), QString());
|
||||
QCOMPARE(r.value(QStringLiteral("back")).toArray(), before);
|
||||
|
||||
checkMirrored(before, r.value(QStringLiteral("v")).toArray(), true);
|
||||
QVERIFY(r.value(QStringLiteral("vMirror")).toString()
|
||||
!= r.value(QStringLiteral("hMirror")).toString());
|
||||
QCOMPARE(r.value(QStringLiteral("undone")).toArray(), before);
|
||||
|
||||
//The save keeps the mirror, and only on that element
|
||||
QVERIFY(r.value(QStringLiteral("saved")).toBool());
|
||||
QFile file(saved);
|
||||
QVERIFY(file.open(QIODevice::ReadOnly));
|
||||
QDomDocument doc;
|
||||
QVERIFY(bool(doc.setContent(&file)));
|
||||
const QString element = r.value(QStringLiteral("element")).toString();
|
||||
const QDomNodeList elements = doc.elementsByTagName(QStringLiteral("element"));
|
||||
int mirrored_count = 0;
|
||||
for (int i = 0; i < elements.size(); ++i) {
|
||||
const QDomElement e = elements.at(i).toElement();
|
||||
if (e.hasAttribute(QStringLiteral("mirror"))) {
|
||||
++mirrored_count;
|
||||
QCOMPARE(e.attribute(QStringLiteral("uuid")), element);
|
||||
QCOMPARE(e.attribute(QStringLiteral("mirror")),
|
||||
r.value(QStringLiteral("vMirror")).toString());
|
||||
}
|
||||
}
|
||||
QCOMPARE(mirrored_count, 1);
|
||||
|
||||
const QJsonObject reloaded = run(script(QStringLiteral(
|
||||
"var f = %1, el = '%2';\n"
|
||||
"qet.log('PROBE ' + JSON.stringify({t: terms(f, el), m: qet.elementMirror(f, el)}));\n")
|
||||
.arg(r.value(QStringLiteral("folio")).toInt()).arg(element)), saved);
|
||||
QCOMPARE(reloaded.value(QStringLiteral("m")).toString(),
|
||||
r.value(QStringLiteral("vMirror")).toString());
|
||||
QCOMPARE(reloaded.value(QStringLiteral("t")).toArray(),
|
||||
r.value(QStringLiteral("v")).toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
On a turned symbol a mirror of the folio is the other mirror of the
|
||||
symbol itself, and its rotation does not change: a label kept
|
||||
upright does not swing round.
|
||||
*/
|
||||
void turnedElement_data()
|
||||
{
|
||||
QTest::addColumn<bool>("vertical");
|
||||
QTest::newRow("horizontal") << false;
|
||||
QTest::newRow("vertical") << true;
|
||||
}
|
||||
|
||||
void turnedElement()
|
||||
{
|
||||
QFETCH(bool, vertical);
|
||||
const QJsonObject r = run(script(QStringLiteral(
|
||||
"var pick = null;\n"
|
||||
"for (var f = 0; f < qet.folioCount() && !pick; f++) {\n"
|
||||
" var els = qet.elementUuids(f);\n"
|
||||
" for (var e = 0; e < els.length && !pick; e++) {\n"
|
||||
" var t = terms(f, els[e]);\n"
|
||||
" if (t.length >= 2 && t.some(function (p) { return p.x !== t[0].x; })\n"
|
||||
" && t.some(function (p) { return p.y !== t[0].y; })) pick = {f: f, e: els[e]};\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
"var f = pick.f, el = pick.e, out = {};\n"
|
||||
"qet.rotateElement(f, el, 90);\n"
|
||||
"out.before = terms(f, el);\n"
|
||||
"qet.mirrorElement(f, el, %1);\n"
|
||||
"out.after = terms(f, el); out.mirror = qet.elementMirror(f, el);\n"
|
||||
"qet.log('PROBE ' + JSON.stringify(out));\n")
|
||||
.arg(vertical ? QStringLiteral("true") : QStringLiteral("false"))),
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/perceuse.qet"));
|
||||
|
||||
QVERIFY2(!r.isEmpty(), "the script logged nothing");
|
||||
checkMirrored(r.value(QStringLiteral("before")).toArray(),
|
||||
r.value(QStringLiteral("after")).toArray(), vertical);
|
||||
//turned by 90 degrees, the symbol's own axes are the other way
|
||||
QCOMPARE(r.value(QStringLiteral("mirror")).toString(),
|
||||
vertical ? QStringLiteral("horizontal") : QStringLiteral("vertical"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_scriptmirror)
|
||||
|
||||
#include "tst_scriptmirror.moc"
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "shortcutmanager.h"
|
||||
#include "ui/configpage/shortcutsconfigpage.h"
|
||||
|
||||
#include <QAction>
|
||||
#include <QComboBox>
|
||||
#include <QKeySequenceEdit>
|
||||
#include <QMainWindow>
|
||||
#include <QMenuBar>
|
||||
#include <QPixmap>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <QTest>
|
||||
#include <QTreeWidget>
|
||||
|
||||
/**
|
||||
The Shortcuts configuration page: the icon and menu columns, the
|
||||
category filter, finding a command by pressing its key, and the
|
||||
copied list.
|
||||
*/
|
||||
class tst_shortcutspage : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QMainWindow m_window;
|
||||
QAction *m_save = nullptr;
|
||||
QAction *m_run = nullptr;
|
||||
QAction *m_tool = nullptr;
|
||||
|
||||
QTreeWidgetItem *row(QTreeWidget *tree, const QString &text)
|
||||
{
|
||||
const auto found = tree->findItems(text, Qt::MatchExactly | Qt::MatchRecursive, 0);
|
||||
return found.isEmpty() ? nullptr : found.first();
|
||||
}
|
||||
|
||||
QStringList visible(QTreeWidget *tree)
|
||||
{
|
||||
QStringList texts;
|
||||
for (const QString &text : {QStringLiteral("Enregistrer"), QStringLiteral("Exécuter"), QStringLiteral("Outil")}) {
|
||||
if (QTreeWidgetItem *item = row(tree, text); item && !item->isHidden()) {
|
||||
texts << text;
|
||||
}
|
||||
}
|
||||
return texts;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
QSettings().remove(QStringLiteral("shortcuts"));
|
||||
|
||||
QPixmap pixmap(16, 16);
|
||||
pixmap.fill(Qt::red);
|
||||
|
||||
QMenu *file = m_window.menuBar()->addMenu(QStringLiteral("&Fichier"));
|
||||
QMenu *scripts = file->addMenu(QStringLiteral("&Scripts"));
|
||||
m_save = file->addAction(QIcon(pixmap), QStringLiteral("&Enregistrer"));
|
||||
m_run = scripts->addAction(QStringLiteral("Exécuter"));
|
||||
//On a toolbar only, in no menu
|
||||
m_tool = new QAction(QStringLiteral("Outil"), &m_window);
|
||||
|
||||
ShortcutManager &manager = ShortcutManager::instance();
|
||||
manager.registerAction(m_save, QStringLiteral("t.save"), QStringLiteral("Catégorie A"), QKeySequence(Qt::CTRL | Qt::Key_S));
|
||||
manager.registerAction(m_run, QStringLiteral("t.run"), QStringLiteral("Catégorie A"), QKeySequence(Qt::CTRL | Qt::Key_R));
|
||||
manager.registerAction(m_tool, QStringLiteral("t.tool"), QStringLiteral("Catégorie B"), QKeySequence());
|
||||
}
|
||||
|
||||
void menuPath()
|
||||
{
|
||||
QCOMPARE(ShortcutsConfigPage::menuPath(m_save), QStringLiteral("Fichier"));
|
||||
QCOMPARE(ShortcutsConfigPage::menuPath(m_run), QStringLiteral("Fichier › Scripts"));
|
||||
QCOMPARE(ShortcutsConfigPage::menuPath(m_tool), QString());
|
||||
QCOMPARE(ShortcutsConfigPage::menuPath(nullptr), QString());
|
||||
}
|
||||
|
||||
void iconAndMenuColumns()
|
||||
{
|
||||
ShortcutsConfigPage page(nullptr);
|
||||
auto *tree = page.findChild<QTreeWidget *>();
|
||||
QVERIFY(row(tree, QStringLiteral("Enregistrer")));
|
||||
QVERIFY(!row(tree, QStringLiteral("Enregistrer"))->icon(0).isNull());
|
||||
QVERIFY(row(tree, QStringLiteral("Outil"))->icon(0).isNull());
|
||||
QCOMPARE(row(tree, QStringLiteral("Exécuter"))->text(1), QStringLiteral("Fichier › Scripts"));
|
||||
QCOMPARE(row(tree, QStringLiteral("Outil"))->text(1), QString());
|
||||
}
|
||||
|
||||
void categoryFilter()
|
||||
{
|
||||
ShortcutsConfigPage page(nullptr);
|
||||
auto *tree = page.findChild<QTreeWidget *>();
|
||||
auto *combo = page.findChild<QComboBox *>(QStringLiteral("categoryFilterCombo"));
|
||||
QCOMPARE(visible(tree).size(), 3);
|
||||
combo->setCurrentIndex(combo->findText(QStringLiteral("Catégorie B")));
|
||||
QCOMPARE(visible(tree), QStringList{QStringLiteral("Outil")});
|
||||
combo->setCurrentIndex(0);
|
||||
QCOMPARE(visible(tree).size(), 3);
|
||||
}
|
||||
|
||||
void findByKey()
|
||||
{
|
||||
ShortcutsConfigPage page(nullptr);
|
||||
auto *tree = page.findChild<QTreeWidget *>();
|
||||
auto *key = page.findChild<QKeySequenceEdit *>(QStringLiteral("keySearchEdit"));
|
||||
key->setKeySequence(QKeySequence(Qt::CTRL | Qt::Key_S));
|
||||
QCOMPARE(visible(tree), QStringList{QStringLiteral("Enregistrer")});
|
||||
key->setKeySequence(QKeySequence(Qt::CTRL | Qt::Key_J));
|
||||
QVERIFY(visible(tree).isEmpty());
|
||||
key->clear();
|
||||
QCOMPARE(visible(tree).size(), 3);
|
||||
}
|
||||
|
||||
void copiedList()
|
||||
{
|
||||
ShortcutsConfigPage page(nullptr);
|
||||
auto *combo = page.findChild<QComboBox *>(QStringLiteral("categoryFilterCombo"));
|
||||
combo->setCurrentIndex(combo->findText(QStringLiteral("Catégorie A")));
|
||||
const QStringList lines = page.listAsText().split(QLatin1Char('\n'), Qt::SkipEmptyParts);
|
||||
QCOMPARE(lines.size(), 3);
|
||||
QCOMPARE(lines.at(0).count(QLatin1Char('\t')), 3);
|
||||
QVERIFY(lines.contains(QStringLiteral("Catégorie A\tFichier\tEnregistrer\t")
|
||||
+ QKeySequence(Qt::CTRL | Qt::Key_S).toString(QKeySequence::NativeText)));
|
||||
QVERIFY(lines.contains(QStringLiteral("Catégorie A\tFichier › Scripts\tExécuter\t")
|
||||
+ QKeySequence(Qt::CTRL | Qt::Key_R).toString(QKeySequence::NativeText)));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_shortcutspage)
|
||||
#include "tst_shortcutspage.moc"
|
||||
@@ -0,0 +1,208 @@
|
||||
#include <QtTest>
|
||||
#include <QGraphicsRectItem>
|
||||
#include <QGraphicsScene>
|
||||
|
||||
#include "shownkinds.h"
|
||||
|
||||
class tst_shownkinds : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QGraphicsScene *scene = nullptr;
|
||||
|
||||
QGraphicsRectItem *add(QGraphicsItem *parent = nullptr)
|
||||
{
|
||||
auto item = new QGraphicsRectItem(0, 0, 10, 10, parent);
|
||||
item->setFlag(QGraphicsItem::ItemIsSelectable);
|
||||
if (!parent) {
|
||||
scene->addItem(item);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void init()
|
||||
{
|
||||
scene = new QGraphicsScene;
|
||||
for (int k = 0; k < ShownKinds::KindCount; ++k) {
|
||||
ShownKinds::setShown(ShownKinds::Kind(k), true);
|
||||
}
|
||||
}
|
||||
|
||||
void cleanup()
|
||||
{
|
||||
delete scene;
|
||||
scene = nullptr;
|
||||
}
|
||||
|
||||
void hideAndShowAgain()
|
||||
{
|
||||
auto shape = add();
|
||||
auto text = add();
|
||||
ShownKinds::tag(shape, ShownKinds::Shapes);
|
||||
ShownKinds::tag(text, ShownKinds::FreeTexts);
|
||||
|
||||
ShownKinds::setShown(ShownKinds::Shapes, false);
|
||||
ShownKinds::apply(scene, ShownKinds::Shapes);
|
||||
QVERIFY(!shape->isVisible());
|
||||
QVERIFY(text->isVisible());
|
||||
QCOMPARE(ShownKinds::hiddenCount(), 1);
|
||||
|
||||
ShownKinds::setShown(ShownKinds::Shapes, true);
|
||||
ShownKinds::apply(scene, ShownKinds::Shapes);
|
||||
QVERIFY(shape->isVisible());
|
||||
QCOMPARE(ShownKinds::hiddenCount(), 0);
|
||||
}
|
||||
|
||||
// Created while its kind is hidden: starts hidden, whatever created it.
|
||||
void createdWhileHidden()
|
||||
{
|
||||
ShownKinds::setShown(ShownKinds::Pictures, false);
|
||||
auto picture = add();
|
||||
ShownKinds::tag(picture, ShownKinds::Pictures);
|
||||
QVERIFY(!picture->isVisible());
|
||||
}
|
||||
|
||||
// Untagged items (symbols, wires) are never touched.
|
||||
void untaggedUntouched()
|
||||
{
|
||||
auto symbol = add();
|
||||
auto hidden_symbol = add();
|
||||
hidden_symbol->setVisible(false);
|
||||
for (int k = 0; k < ShownKinds::KindCount; ++k) {
|
||||
ShownKinds::setShown(ShownKinds::Kind(k), false);
|
||||
ShownKinds::apply(scene, ShownKinds::Kind(k));
|
||||
}
|
||||
QVERIFY(symbol->isVisible());
|
||||
for (int k = 0; k < ShownKinds::KindCount; ++k) {
|
||||
ShownKinds::setShown(ShownKinds::Kind(k), true);
|
||||
ShownKinds::apply(scene, ShownKinds::Kind(k));
|
||||
}
|
||||
QVERIFY(!hidden_symbol->isVisible());
|
||||
}
|
||||
|
||||
// apply() touches only the kind it is given.
|
||||
void applyOnlyThatKind()
|
||||
{
|
||||
auto shape = add();
|
||||
ShownKinds::tag(shape, ShownKinds::Shapes);
|
||||
ShownKinds::setShown(ShownKinds::Shapes, false);
|
||||
ShownKinds::apply(scene, ShownKinds::Tables);
|
||||
QVERIFY(shape->isVisible());
|
||||
}
|
||||
|
||||
// An item that hid itself (a wire number switched off) stays hidden
|
||||
// when its kind is hidden and shown again.
|
||||
void ownHiddenStaysHidden()
|
||||
{
|
||||
auto number = add();
|
||||
ShownKinds::tag(number, ShownKinds::WireNumbers);
|
||||
ShownKinds::setVisible(number, false);
|
||||
ShownKinds::setShown(ShownKinds::WireNumbers, false);
|
||||
ShownKinds::apply(scene, ShownKinds::WireNumbers);
|
||||
ShownKinds::setShown(ShownKinds::WireNumbers, true);
|
||||
ShownKinds::apply(scene, ShownKinds::WireNumbers);
|
||||
QVERIFY(!number->isVisible());
|
||||
}
|
||||
|
||||
// An item asking to be shown while its kind is hidden stays hidden,
|
||||
// and appears when the kind is shown (one text per potential moving
|
||||
// to another wire, a cross-reference changing its snap).
|
||||
void showVetoedThenRestored()
|
||||
{
|
||||
auto number = add();
|
||||
ShownKinds::tag(number, ShownKinds::WireNumbers);
|
||||
ShownKinds::setShown(ShownKinds::WireNumbers, false);
|
||||
ShownKinds::apply(scene, ShownKinds::WireNumbers);
|
||||
ShownKinds::setVisible(number, true);
|
||||
QVERIFY(!number->isVisible());
|
||||
ShownKinds::setShown(ShownKinds::WireNumbers, true);
|
||||
ShownKinds::apply(scene, ShownKinds::WireNumbers);
|
||||
QVERIFY(number->isVisible());
|
||||
}
|
||||
|
||||
// Hidden while its kind is hidden: not shown again with the kind.
|
||||
void hiddenWhileKindHidden()
|
||||
{
|
||||
auto number = add();
|
||||
ShownKinds::tag(number, ShownKinds::WireNumbers);
|
||||
ShownKinds::setShown(ShownKinds::WireNumbers, false);
|
||||
ShownKinds::apply(scene, ShownKinds::WireNumbers);
|
||||
ShownKinds::setVisible(number, false);
|
||||
ShownKinds::setShown(ShownKinds::WireNumbers, true);
|
||||
ShownKinds::apply(scene, ShownKinds::WireNumbers);
|
||||
QVERIFY(!number->isVisible());
|
||||
}
|
||||
|
||||
// wantsVisible() tells the wire carrying a potential's text from the
|
||||
// others while wire texts are hidden, so deleting it moves the text.
|
||||
void wantsVisibleWhileKindHidden()
|
||||
{
|
||||
auto carrier = add();
|
||||
auto other = add();
|
||||
ShownKinds::tag(carrier, ShownKinds::WireNumbers);
|
||||
ShownKinds::tag(other, ShownKinds::WireNumbers);
|
||||
ShownKinds::setVisible(other, false);
|
||||
ShownKinds::setShown(ShownKinds::WireNumbers, false);
|
||||
ShownKinds::apply(scene, ShownKinds::WireNumbers);
|
||||
QVERIFY(!carrier->isVisible());
|
||||
QVERIFY(ShownKinds::wantsVisible(carrier));
|
||||
QVERIFY(!ShownKinds::wantsVisible(other));
|
||||
ShownKinds::setShown(ShownKinds::WireNumbers, true);
|
||||
ShownKinds::apply(scene, ShownKinds::WireNumbers);
|
||||
QVERIFY(ShownKinds::wantsVisible(carrier));
|
||||
QVERIFY(!ShownKinds::wantsVisible(other));
|
||||
}
|
||||
|
||||
// A text under a hidden parent still counts as wanting to be visible.
|
||||
void childOfHiddenParent()
|
||||
{
|
||||
auto group = add();
|
||||
auto text = add(group);
|
||||
ShownKinds::tag(group, ShownKinds::SymbolTexts);
|
||||
ShownKinds::tag(text, ShownKinds::CrossReferences);
|
||||
ShownKinds::setShown(ShownKinds::SymbolTexts, false);
|
||||
ShownKinds::apply(scene, ShownKinds::SymbolTexts);
|
||||
ShownKinds::setShown(ShownKinds::CrossReferences, false);
|
||||
ShownKinds::apply(scene, ShownKinds::CrossReferences);
|
||||
ShownKinds::setShown(ShownKinds::SymbolTexts, true);
|
||||
ShownKinds::apply(scene, ShownKinds::SymbolTexts);
|
||||
QVERIFY(!text->isVisible());
|
||||
ShownKinds::setShown(ShownKinds::CrossReferences, true);
|
||||
ShownKinds::apply(scene, ShownKinds::CrossReferences);
|
||||
QVERIFY(text->isVisible());
|
||||
}
|
||||
|
||||
// isHidden() follows the parents: a cross-reference under a symbol text.
|
||||
void hiddenThroughParent()
|
||||
{
|
||||
auto symbol = add();
|
||||
auto label = add(symbol);
|
||||
auto xref = add(label);
|
||||
ShownKinds::tag(label, ShownKinds::SymbolTexts);
|
||||
ShownKinds::tag(xref, ShownKinds::CrossReferences);
|
||||
QVERIFY(!ShownKinds::isHidden(xref));
|
||||
|
||||
ShownKinds::setShown(ShownKinds::SymbolTexts, false);
|
||||
QVERIFY(ShownKinds::isHidden(xref));
|
||||
QVERIFY(!ShownKinds::isHidden(symbol));
|
||||
}
|
||||
|
||||
// Hidden means safe from Select All, copy and delete: Qt will not
|
||||
// select it, and drops a selection it had.
|
||||
void hiddenIsNotSelectable()
|
||||
{
|
||||
auto text = add();
|
||||
ShownKinds::tag(text, ShownKinds::FreeTexts);
|
||||
text->setSelected(true);
|
||||
ShownKinds::setShown(ShownKinds::FreeTexts, false);
|
||||
ShownKinds::apply(scene, ShownKinds::FreeTexts);
|
||||
QVERIFY(!text->isSelected());
|
||||
text->setSelected(true);
|
||||
QVERIFY(scene->selectedItems().isEmpty());
|
||||
QVERIFY(scene->items(QRectF(-5, -5, 20, 20)).isEmpty());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_shownkinds)
|
||||
#include "tst_shownkinds.moc"
|
||||
@@ -120,6 +120,34 @@ void SmartDeviceTest::defaultQueryAndCsv()
|
||||
QVERIFY(csv.contains("Terminal block"));
|
||||
QVERIFY(!csv.contains("Must not be exported"));
|
||||
QVERIFY(csv.indexOf("-K1") < csv.indexOf("-K2"));
|
||||
|
||||
//#1178: a junction -- a terminal-type symbol drawn where wires
|
||||
//branch -- has no label and no part data. It can be left out, and
|
||||
//so can the contact blocks; a terminal block with only a part
|
||||
//number stays.
|
||||
QSqlQuery raw(db);
|
||||
QVERIFY(raw.exec(QStringLiteral(
|
||||
"INSERT INTO element_nomenclature_view "
|
||||
"(label, element_type, diagram_position, position) "
|
||||
"VALUES (NULL, 'terminal', 1, 6)")));
|
||||
QVERIFY(raw.exec(QStringLiteral(
|
||||
"INSERT INTO element_nomenclature_view "
|
||||
"(label, designation, manufacturer, manufacturer_reference, "
|
||||
"element_type, diagram_position, position) "
|
||||
"VALUES ('', '', '', 'PT 2,5', 'terminal', 1, 7)")));
|
||||
auto count = [&db](const QString &sql) {
|
||||
QSqlQuery q(db);
|
||||
if (!q.exec(sql))
|
||||
return -1;
|
||||
int n = 0;
|
||||
while (q.next())
|
||||
++n;
|
||||
return n;
|
||||
};
|
||||
QCOMPARE(count(BomExport::defaultQuery()), 6);
|
||||
QCOMPARE(count(BomExport::defaultQuery(true, false)), 5);
|
||||
QCOMPARE(count(BomExport::defaultQuery(false, true)), 5);
|
||||
QCOMPARE(count(BomExport::defaultQuery(false, false)), 4);
|
||||
}
|
||||
QSqlDatabase::removeDatabase(connection);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
|
||||
#include "editor/symbolscale.h"
|
||||
|
||||
class tst_symbolscale : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
// Terminals 10 px apart: whole factors are safe, halving is not.
|
||||
void tenPitchTakesWholeFactors()
|
||||
{
|
||||
const QList<QPointF> terminals{{0, -20}, {0, 20}, {10, 0}};
|
||||
QCOMPARE(SymbolScale::safeFactors(terminals),
|
||||
(QList<qreal>{2.0, 3.0, 4.0}));
|
||||
}
|
||||
|
||||
// Terminals 20 px apart: every candidate is safe.
|
||||
void twentyPitchTakesEverything()
|
||||
{
|
||||
const QList<QPointF> terminals{{-20, 0}, {20, 40}};
|
||||
QCOMPARE(SymbolScale::safeFactors(terminals), SymbolScale::candidates());
|
||||
}
|
||||
|
||||
// Off the grid by 5 px today: only even factors bring it on.
|
||||
void fivePxOffIsRescuedByEvenFactors()
|
||||
{
|
||||
const QList<QPointF> terminals{{-5, 0}, {5, 30}};
|
||||
QVERIFY(!SymbolScale::allOnGrid(terminals));
|
||||
QCOMPARE(SymbolScale::offGridCount(terminals), 2);
|
||||
QCOMPARE(SymbolScale::safeFactors(terminals),
|
||||
(QList<qreal>{2.0, 4.0}));
|
||||
}
|
||||
|
||||
// Off by an odd amount: nothing helps.
|
||||
void oddOffsetHasNoSafeFactor()
|
||||
{
|
||||
QVERIFY(SymbolScale::safeFactors({{-7, 0}, {0, 20}}).isEmpty());
|
||||
}
|
||||
|
||||
void noTerminalTakesEverything()
|
||||
{
|
||||
QCOMPARE(SymbolScale::safeFactors({}), SymbolScale::candidates());
|
||||
}
|
||||
|
||||
// Saved with two decimals: 19.999 still counts as 20.
|
||||
void savedRoundingIsTolerated()
|
||||
{
|
||||
QVERIFY(SymbolScale::onGrid(19.999));
|
||||
QVERIFY(SymbolScale::onGrid(-30.001));
|
||||
QVERIFY(!SymbolScale::onGrid(19.9));
|
||||
}
|
||||
|
||||
void fontSizeIsWholeAndReadable()
|
||||
{
|
||||
QCOMPARE(SymbolScale::scaledFontSize(9, 2.0), 18);
|
||||
QCOMPARE(SymbolScale::scaledFontSize(9, 1.5), 14);
|
||||
QCOMPARE(SymbolScale::scaledFontSize(6, 0.5), 4);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_symbolscale)
|
||||
#include "tst_symbolscale.moc"
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QProcess>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
/**
|
||||
A label formula with ten or more sequential numbers: %sequ_10 must get
|
||||
the tenth value, not the first value followed by a "0". The values are
|
||||
put in by AssignVariables::assignSequence(), which the bill of
|
||||
materials uses for every label, so the real binary's --export-bom on
|
||||
fixtures/ten_sequential_numbers.qet shows the result: its element has
|
||||
the formula %sequ_10-%sequ_1 and the unit values A to J.
|
||||
*/
|
||||
class tst_tensequentialnumbers : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
static bool runQet(const QStringList &arguments)
|
||||
{
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
|
||||
return proc.waitForFinished(60000)
|
||||
&& proc.exitStatus() == QProcess::NormalExit
|
||||
&& proc.exitCode() == 0;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY2(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)), "qelectrotech binary not found");
|
||||
QVERIFY(m_dir.isValid());
|
||||
}
|
||||
|
||||
void tenthValueIsUsed()
|
||||
{
|
||||
const QString fixture = QFINDTESTDATA("fixtures/ten_sequential_numbers.qet");
|
||||
QVERIFY2(!fixture.isEmpty(), "fixture project not found");
|
||||
const QString out = m_dir.filePath(QStringLiteral("bom.csv"));
|
||||
QVERIFY2(runQet({QStringLiteral("--export-bom"), fixture, out}), "--export-bom failed");
|
||||
|
||||
QFile file(out);
|
||||
QVERIFY(file.open(QIODevice::ReadOnly | QIODevice::Text));
|
||||
const QStringList lines = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'));
|
||||
QStringList labels;
|
||||
for (const QString &line : lines) {
|
||||
if (line.startsWith(QLatin1Char('"')))
|
||||
labels << line.section(QLatin1Char(';'), 0, 0);
|
||||
}
|
||||
QVERIFY2(labels.contains(QStringLiteral("\"J-A\"")),
|
||||
qPrintable(QStringLiteral("labels: %1").arg(labels.join(QLatin1Char(' ')))));
|
||||
QVERIFY2(!labels.contains(QStringLiteral("\"A0-A\"")), "the first value with a 0 appended");
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_tensequentialnumbers)
|
||||
#include "tst_tensequentialnumbers.moc"
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QTextDocument>
|
||||
#include <QTextOption>
|
||||
|
||||
#include "textlines.h"
|
||||
|
||||
class tst_textlines : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
// A text without a width gives its own lines.
|
||||
void unwrappedTextGivesItsLines()
|
||||
{
|
||||
QTextDocument document(QStringLiteral("Motor protection\nswitch Q12"));
|
||||
QCOMPARE(TextLines::layoutLines(&document),
|
||||
QStringList({QStringLiteral("Motor protection"), QStringLiteral("switch Q12")}));
|
||||
}
|
||||
|
||||
// A text without a width gives exactly the lines the DXF export wrote
|
||||
// before, toPlainText().split('\n'): trailing spaces and tabs kept,
|
||||
// non-breaking spaces as plain spaces, <br> as a line break.
|
||||
void unwrappedTextIsUnchanged_data()
|
||||
{
|
||||
QTest::addColumn<QString>("html");
|
||||
QTest::newRow("trailing spaces") << QStringLiteral("K1 \nswitch\t");
|
||||
QTest::newRow("non-breaking spaces") << QStringLiteral("<p>K1 24V DC</p>");
|
||||
QTest::newRow("line break") << QStringLiteral("<p>Wiper to 6<br>contacts</p>");
|
||||
QTest::newRow("paragraphs") << QStringLiteral("<p>relais voyant </p><p> preventa</p>");
|
||||
QTest::newRow("empty") << QString();
|
||||
}
|
||||
|
||||
void unwrappedTextIsUnchanged()
|
||||
{
|
||||
QFETCH(QString, html);
|
||||
QTextDocument document;
|
||||
if (Qt::mightBeRichText(html))
|
||||
document.setHtml(html);
|
||||
else
|
||||
document.setPlainText(html);
|
||||
|
||||
QCOMPARE(TextLines::layoutLines(&document),
|
||||
document.toPlainText().split(QLatin1Char('\n')));
|
||||
}
|
||||
|
||||
// A text wrapped to its width gives one line per drawn line, without
|
||||
// the space the line was broken at.
|
||||
void wrappedTextGivesTheDrawnLines()
|
||||
{
|
||||
QTextDocument document(QStringLiteral("Motor protection switch Q12"));
|
||||
QTextOption option = document.defaultTextOption();
|
||||
option.setWrapMode(QTextOption::WordWrap);
|
||||
document.setDefaultTextOption(option);
|
||||
document.setTextWidth(1);
|
||||
|
||||
QCOMPARE(TextLines::layoutLines(&document),
|
||||
QStringList({QStringLiteral("Motor"), QStringLiteral("protection"),
|
||||
QStringLiteral("switch"), QStringLiteral("Q12")}));
|
||||
}
|
||||
|
||||
// A wrapped paragraph has no trailing spaces left, even on its last
|
||||
// line: tabs and spaces that wrap to a line of their own (as in the
|
||||
// affuteuse_250h example) give an empty line, which is not written but
|
||||
// still keeps the line spacing.
|
||||
void wrappedTextHasNoTrailingSpaces()
|
||||
{
|
||||
QTextDocument document(QStringLiteral("\tMotor\t\t "));
|
||||
QTextOption option = document.defaultTextOption();
|
||||
option.setWrapMode(QTextOption::WordWrap);
|
||||
document.setDefaultTextOption(option);
|
||||
document.setTextWidth(80);
|
||||
|
||||
const QStringList lines = TextLines::layoutLines(&document);
|
||||
QVERIFY(lines.size() > 1);
|
||||
QVERIFY(lines.contains(QStringLiteral("Motor")));
|
||||
for (const QString &line : lines)
|
||||
QVERIFY2(line.isEmpty() || !line.back().isSpace(), qPrintable(line));
|
||||
}
|
||||
|
||||
// Non-breaking spaces also become plain spaces in a wrapped text.
|
||||
void wrappedTextHasPlainSpaces()
|
||||
{
|
||||
QTextDocument document;
|
||||
document.setHtml(QStringLiteral("<p>K1 24V and more words</p>"));
|
||||
QTextOption option = document.defaultTextOption();
|
||||
option.setWrapMode(QTextOption::WordWrap);
|
||||
document.setDefaultTextOption(option);
|
||||
document.setTextWidth(1);
|
||||
|
||||
const QStringList lines = TextLines::layoutLines(&document);
|
||||
QCOMPARE(lines.first(), QStringLiteral("K1 24V"));
|
||||
for (const QString &line : lines)
|
||||
QVERIFY(!line.contains(QChar::Nbsp));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_textlines)
|
||||
#include "tst_textlines.moc"
|
||||
@@ -0,0 +1,329 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <QtTest>
|
||||
#include <QGraphicsScene>
|
||||
#include <QGraphicsSceneMouseEvent>
|
||||
#include <QGraphicsTextItem>
|
||||
#include <QSignalSpy>
|
||||
#include <QTextBlock>
|
||||
#include <QTextLayout>
|
||||
#include <QTextOption>
|
||||
#include <QUndoStack>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "QetGraphicsItemModeler/qetgraphicshandleritem.h"
|
||||
#include "QetGraphicsItemModeler/textresizehandles.h"
|
||||
|
||||
using TextResize::Corner;
|
||||
|
||||
/**
|
||||
A text with the "textWidth" property the texts of QElectroTech have.
|
||||
*/
|
||||
class WidthText : public QGraphicsTextItem
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(qreal textWidth READ textWidth WRITE setWidth)
|
||||
|
||||
public:
|
||||
WidthText(const QString &text) : QGraphicsTextItem(text)
|
||||
{
|
||||
QTextOption option = document()->defaultTextOption();
|
||||
option.setWrapMode(QTextOption::WordWrap);
|
||||
document()->setDefaultTextOption(option);
|
||||
}
|
||||
void setWidth(qreal width) {setTextWidth(width);}
|
||||
};
|
||||
|
||||
/// The number of lines the text is laid out on.
|
||||
static int lineCount(const QGraphicsTextItem &text)
|
||||
{
|
||||
text.document()->size();
|
||||
int lines = 0;
|
||||
for (QTextBlock block = text.document()->begin() ; block.isValid() ; block = block.next())
|
||||
lines += block.layout()->lineCount();
|
||||
return lines;
|
||||
}
|
||||
|
||||
static bool samePoint(const QPointF &a, const QPointF &b)
|
||||
{
|
||||
return qAbs(a.x() - b.x()) < 1e-6 && qAbs(a.y() - b.y()) < 1e-6;
|
||||
}
|
||||
|
||||
static QPointF sceneCorner(const QGraphicsTextItem &text, Corner corner)
|
||||
{
|
||||
return text.mapToScene(TextResize::cornerOf(text.boundingRect(), corner));
|
||||
}
|
||||
|
||||
/// Send a mouse event of type to the handle at corner, as the scene would.
|
||||
static void sendMouse(QGraphicsScene &scene, TextResizeHandles &handles,
|
||||
Corner corner, QEvent::Type type, const QPointF &scene_pos)
|
||||
{
|
||||
QGraphicsSceneMouseEvent event(type);
|
||||
event.setButton(Qt::LeftButton);
|
||||
event.setButtons(type == QEvent::GraphicsSceneMouseRelease ? Qt::NoButton : Qt::LeftButton);
|
||||
event.setScenePos(scene_pos);
|
||||
scene.sendEvent(handles.handle(corner), &event);
|
||||
}
|
||||
|
||||
/// Drag the handle at corner from where it is by offset, in scene coordinates.
|
||||
static void drag(QGraphicsScene &scene, TextResizeHandles &handles,
|
||||
WidthText &text, Corner corner, const QPointF &offset)
|
||||
{
|
||||
const QPointF start = sceneCorner(text, corner);
|
||||
sendMouse(scene, handles, corner, QEvent::GraphicsSceneMousePress, start);
|
||||
sendMouse(scene, handles, corner, QEvent::GraphicsSceneMouseMove, start + offset / 2);
|
||||
sendMouse(scene, handles, corner, QEvent::GraphicsSceneMouseMove, start + offset);
|
||||
sendMouse(scene, handles, corner, QEvent::GraphicsSceneMouseRelease, start + offset);
|
||||
}
|
||||
|
||||
class tst_textresizehandles : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
// Pure geometry: the width given by a drag does not change while the
|
||||
// text moves to keep its opposite corner in place.
|
||||
void widthForDragUsesThePressTransform()
|
||||
{
|
||||
WidthText text(QStringLiteral("Motor protection switch"));
|
||||
text.setRotation(37);
|
||||
text.setPos(10, 10);
|
||||
const QTransform press_inverse = text.sceneTransform().inverted();
|
||||
const QPointF fixed = TextResize::cornerOf(text.boundingRect(), TextResize::TopLeft);
|
||||
const QPointF mouse = text.mapToScene(QPointF(80, 3));
|
||||
|
||||
const qreal width = TextResize::widthForDrag(press_inverse, fixed, mouse, TextResize::BottomRight, 0);
|
||||
QCOMPARE(qRound(width * 1000), 80000);
|
||||
|
||||
text.setPos(500, -300);
|
||||
QCOMPARE(TextResize::widthForDrag(press_inverse, fixed, mouse, TextResize::BottomRight, 0), width);
|
||||
}
|
||||
|
||||
// The minimum width is the longest word: a text that narrow does not
|
||||
// break any word, one narrower would.
|
||||
void minimumWidthIsTheLongestWord()
|
||||
{
|
||||
WidthText text(QStringLiteral("a Motorschutzschalter b"));
|
||||
const qreal minimum = TextResize::minimumWidth(text.document());
|
||||
|
||||
WidthText word(QStringLiteral("Motorschutzschalter"));
|
||||
QVERIFY(qAbs(minimum - word.boundingRect().width()) < 0.5);
|
||||
|
||||
text.setWidth(minimum);
|
||||
QVERIFY(qAbs(text.boundingRect().width() - minimum) < 0.5);
|
||||
QCOMPARE(lineCount(text), 3);
|
||||
}
|
||||
|
||||
// The minimum width never cuts a word, even for a document that would
|
||||
// otherwise break a word anywhere.
|
||||
void minimumWidthKeepsWords()
|
||||
{
|
||||
QGraphicsTextItem text(QStringLiteral("a Motorschutzschalter"));
|
||||
QTextOption option = text.document()->defaultTextOption();
|
||||
option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
|
||||
text.document()->setDefaultTextOption(option);
|
||||
|
||||
QGraphicsTextItem word(QStringLiteral("Motorschutzschalter"));
|
||||
QVERIFY(qAbs(TextResize::minimumWidth(text.document()) - word.boundingRect().width()) < 0.5);
|
||||
}
|
||||
|
||||
// pinCorner() puts a corner where it was, for each corner, rotation,
|
||||
// rotation point and inside a rotated parent.
|
||||
void pinCornerKeepsTheCorner()
|
||||
{
|
||||
QGraphicsRectItem parent(0, 0, 10, 10);
|
||||
parent.setRotation(30);
|
||||
for (bool with_parent : {false, true})
|
||||
for (int c = TextResize::TopLeft ; c <= TextResize::BottomLeft ; ++c)
|
||||
for (qreal angle : {0.0, 90.0, 37.0})
|
||||
for (bool centre : {false, true}) {
|
||||
WidthText text(QStringLiteral("Motor protection switch"));
|
||||
if (with_parent)
|
||||
text.setParentItem(&parent);
|
||||
text.setRotation(angle);
|
||||
text.setTransformOriginPoint(centre ? text.boundingRect().center() : QPointF());
|
||||
const Corner corner = Corner(c);
|
||||
const QPointF before = sceneCorner(text, corner);
|
||||
|
||||
text.setWidth(70);
|
||||
if (centre)
|
||||
text.setTransformOriginPoint(text.boundingRect().center());
|
||||
TextResize::pinCorner(&text, corner, before);
|
||||
QVERIFY2(samePoint(sceneCorner(text, corner), before),
|
||||
qPrintable(QStringLiteral("corner %1 angle %2 centre %3 parent %4")
|
||||
.arg(c).arg(angle).arg(centre).arg(with_parent)));
|
||||
text.setParentItem(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Dragging a handle keeps the opposite corner in place, wraps the text
|
||||
// and reports the change once, on release.
|
||||
void dragKeepsTheOppositeCorner()
|
||||
{
|
||||
for (int c = TextResize::TopLeft ; c <= TextResize::BottomLeft ; ++c)
|
||||
for (qreal angle : {0.0, 90.0, 37.0}) {
|
||||
QGraphicsScene scene;
|
||||
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
|
||||
scene.addItem(text);
|
||||
text->setPos(100, 100);
|
||||
text->setRotation(angle);
|
||||
auto *handles = new TextResizeHandles(text);
|
||||
QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
|
||||
|
||||
const Corner corner = Corner(c);
|
||||
const QPointF fixed = sceneCorner(*text, TextResize::opposite(corner));
|
||||
const qreal natural = text->boundingRect().width();
|
||||
const QPointF start_pos = text->pos();
|
||||
|
||||
//Narrower by 40 along the text, whatever its rotation;
|
||||
//a vertical move of the mouse is ignored.
|
||||
QTransform turn;
|
||||
turn.rotate(angle);
|
||||
const qreal sign = TextResize::isLeft(corner) ? 1 : -1;
|
||||
drag(scene, *handles, *text, corner, turn.map(QPointF(sign * 40, 25)));
|
||||
|
||||
const QString what = QStringLiteral("corner %1 angle %2").arg(c).arg(angle);
|
||||
QCOMPARE(text->textWidth(), qreal(qRound(natural - 40)));
|
||||
QVERIFY2(lineCount(*text) > 1, qPrintable(what));
|
||||
QVERIFY2(samePoint(sceneCorner(*text, TextResize::opposite(corner)), fixed), qPrintable(what));
|
||||
|
||||
QCOMPARE(finished.count(), 1);
|
||||
const QList<QVariant> args = finished.first();
|
||||
QCOMPARE(args.at(0).toReal(), qreal(-1));
|
||||
QCOMPARE(args.at(1).toReal(), text->textWidth());
|
||||
QCOMPARE(args.at(2).toPointF(), start_pos);
|
||||
QCOMPARE(args.at(3).toPointF(), text->pos());
|
||||
|
||||
//The handles follow the new box
|
||||
QVERIFY(samePoint(handles->handle(corner)->scenePos(), sceneCorner(*text, corner)));
|
||||
}
|
||||
}
|
||||
|
||||
// The handles still work after the text left its scene and came back,
|
||||
// as undoing a delete does: Qt drops scene event filters on removal.
|
||||
void dragAfterLeavingTheScene()
|
||||
{
|
||||
QGraphicsScene scene;
|
||||
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
|
||||
scene.addItem(text);
|
||||
auto *handles = new TextResizeHandles(text);
|
||||
|
||||
scene.removeItem(text);
|
||||
scene.addItem(text);
|
||||
|
||||
const QPointF start_pos = text->pos();
|
||||
drag(scene, *handles, *text, TextResize::BottomRight, QPointF(-40, 0));
|
||||
QVERIFY(text->textWidth() > 0);
|
||||
QCOMPARE(text->pos(), start_pos);
|
||||
delete text;
|
||||
}
|
||||
|
||||
// The handles also work when they are created before the text is in a
|
||||
// scene.
|
||||
void dragWhenCreatedOutsideAScene()
|
||||
{
|
||||
QGraphicsScene scene;
|
||||
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
|
||||
auto *handles = new TextResizeHandles(text);
|
||||
scene.addItem(text);
|
||||
|
||||
drag(scene, *handles, *text, TextResize::BottomRight, QPointF(-40, 0));
|
||||
QVERIFY(text->textWidth() > 0);
|
||||
}
|
||||
|
||||
// A text is never made narrower than its longest word.
|
||||
void dragStopsAtTheLongestWord()
|
||||
{
|
||||
QGraphicsScene scene;
|
||||
auto *text = new WidthText(QStringLiteral("a Motorschutzschalter b"));
|
||||
scene.addItem(text);
|
||||
auto *handles = new TextResizeHandles(text);
|
||||
drag(scene, *handles, *text, TextResize::BottomRight, QPointF(-1000, 0));
|
||||
QCOMPARE(text->textWidth(), std::ceil(TextResize::minimumWidth(text->document())));
|
||||
}
|
||||
|
||||
// A press and release without moving changes nothing.
|
||||
void clickChangesNothing()
|
||||
{
|
||||
QGraphicsScene scene;
|
||||
auto *text = new WidthText(QStringLiteral("Motor protection switch"));
|
||||
scene.addItem(text);
|
||||
auto *handles = new TextResizeHandles(text);
|
||||
QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
|
||||
drag(scene, *handles, *text, TextResize::TopRight, QPointF());
|
||||
QCOMPARE(finished.count(), 0);
|
||||
QCOMPARE(text->textWidth(), qreal(-1));
|
||||
}
|
||||
|
||||
// Double-clicking a handle goes back to the automatic width.
|
||||
void doubleClickResetsToAutomaticWidth()
|
||||
{
|
||||
QGraphicsScene scene;
|
||||
auto *text = new WidthText(QStringLiteral("Motor protection switch"));
|
||||
scene.addItem(text);
|
||||
text->setWidth(60);
|
||||
auto *handles = new TextResizeHandles(text);
|
||||
QSignalSpy finished(handles, &TextResizeHandles::resizeFinished);
|
||||
|
||||
sendMouse(scene, *handles, TextResize::BottomLeft, QEvent::GraphicsSceneMouseDoubleClick,
|
||||
sceneCorner(*text, TextResize::BottomLeft));
|
||||
QCOMPARE(text->textWidth(), qreal(-1));
|
||||
QCOMPARE(finished.count(), 1);
|
||||
QCOMPARE(finished.first().at(0).toReal(), qreal(60));
|
||||
QCOMPARE(finished.first().at(1).toReal(), qreal(-1));
|
||||
|
||||
//Already automatic: nothing to undo
|
||||
sendMouse(scene, *handles, TextResize::BottomLeft, QEvent::GraphicsSceneMouseDoubleClick,
|
||||
sceneCorner(*text, TextResize::BottomLeft));
|
||||
QCOMPARE(finished.count(), 1);
|
||||
}
|
||||
|
||||
// One undo step gives back the width and the position, one redo
|
||||
// applies them again.
|
||||
void undoRestoresWidthAndPosition()
|
||||
{
|
||||
QGraphicsScene scene;
|
||||
auto *text = new WidthText(QStringLiteral("Motor protection switch Q12"));
|
||||
scene.addItem(text);
|
||||
text->setPos(100, 100);
|
||||
text->setRotation(90);
|
||||
auto *handles = new TextResizeHandles(text);
|
||||
QUndoStack stack;
|
||||
connect(handles, &TextResizeHandles::resizeFinished, &stack,
|
||||
[&stack, text](qreal ow, qreal nw, QPointF op, QPointF np) {
|
||||
stack.push(new TextResizeCommand(text, ow, nw, op, np));
|
||||
});
|
||||
|
||||
drag(scene, *handles, *text, TextResize::TopLeft, QPointF(0, 40));
|
||||
QCOMPARE(stack.count(), 1);
|
||||
const qreal new_width = text->textWidth();
|
||||
const QPointF new_pos = text->pos();
|
||||
QVERIFY(new_pos != QPointF(100, 100));
|
||||
|
||||
stack.undo();
|
||||
QCOMPARE(text->textWidth(), qreal(-1));
|
||||
QCOMPARE(text->pos(), QPointF(100, 100));
|
||||
stack.redo();
|
||||
QCOMPARE(text->textWidth(), new_width);
|
||||
QCOMPARE(text->pos(), new_pos);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_textresizehandles)
|
||||
#include "tst_textresizehandles.moc"
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "toolbarsettings.h"
|
||||
#include "ui/configpage/toolbarsconfigpage.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDockWidget>
|
||||
#include <QMainWindow>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <QTest>
|
||||
#include <QToolBar>
|
||||
|
||||
/**
|
||||
ToolbarSettings and its configuration page: defaults change nothing,
|
||||
saved values reach every open window that took them, and a toolbar
|
||||
inside a panel or a window that never took them is left alone.
|
||||
*/
|
||||
class tst_toolbarsettings : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QMainWindow *m_window = nullptr;
|
||||
QToolBar *m_top = nullptr;
|
||||
QToolBar *m_left = nullptr;
|
||||
QToolBar *m_in_dock = nullptr;
|
||||
QMainWindow *m_other = nullptr;
|
||||
QToolBar *m_other_bar = nullptr;
|
||||
QSize m_default_size;
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
QSettings().remove(QStringLiteral("toolbars"));
|
||||
|
||||
m_window = new QMainWindow();
|
||||
m_default_size = m_window->iconSize();
|
||||
m_top = new QToolBar(QStringLiteral("top"), m_window);
|
||||
m_window->addToolBar(Qt::TopToolBarArea, m_top);
|
||||
//Created without a parent, as some QET toolbars are
|
||||
m_left = new QToolBar(QStringLiteral("left"));
|
||||
m_window->addToolBar(Qt::LeftToolBarArea, m_left);
|
||||
auto *dock = new QDockWidget(m_window);
|
||||
m_in_dock = new QToolBar(dock);
|
||||
dock->setWidget(m_in_dock);
|
||||
m_window->addDockWidget(Qt::LeftDockWidgetArea, dock);
|
||||
m_window->show();
|
||||
|
||||
//A window that never takes the settings, like the print window
|
||||
m_other = new QMainWindow();
|
||||
m_other_bar = new QToolBar(QStringLiteral("other"), m_other);
|
||||
m_other->addToolBar(Qt::TopToolBarArea, m_other_bar);
|
||||
m_other->show();
|
||||
}
|
||||
|
||||
void cleanupTestCase()
|
||||
{
|
||||
delete m_window;
|
||||
delete m_other;
|
||||
}
|
||||
|
||||
void defaultsChangeNothing()
|
||||
{
|
||||
QCOMPARE(ToolbarSettings::iconSize(), 0);
|
||||
QCOMPARE(ToolbarSettings::buttonStyle(), Qt::ToolButtonIconOnly);
|
||||
QCOMPARE(ToolbarSettings::locked(), false);
|
||||
ToolbarSettings::applyTo(m_window);
|
||||
QCOMPARE(m_window->iconSize(), m_default_size);
|
||||
QCOMPARE(m_top->iconSize(), m_default_size);
|
||||
QCOMPARE(m_top->toolButtonStyle(), Qt::ToolButtonIconOnly);
|
||||
QVERIFY(m_top->isMovable());
|
||||
QVERIFY(m_left->isMovable());
|
||||
}
|
||||
|
||||
void pageSavesAndAppliesToOpenWindows()
|
||||
{
|
||||
{
|
||||
ToolbarsConfigPage page;
|
||||
auto *size = page.findChild<QComboBox *>(QStringLiteral("iconSizeCombo"));
|
||||
auto *style = page.findChild<QComboBox *>(QStringLiteral("buttonStyleCombo"));
|
||||
size->setCurrentIndex(size->findData(32));
|
||||
style->setCurrentIndex(style->findData(int(Qt::ToolButtonTextUnderIcon)));
|
||||
page.findChild<QCheckBox *>(QStringLiteral("lockedCheck"))->setChecked(true);
|
||||
page.applyConf();
|
||||
}
|
||||
QCOMPARE(ToolbarSettings::iconSize(), 32);
|
||||
QCOMPARE(ToolbarSettings::buttonStyle(), Qt::ToolButtonTextUnderIcon);
|
||||
QVERIFY(ToolbarSettings::locked());
|
||||
|
||||
QCOMPARE(m_top->iconSize(), QSize(32, 32));
|
||||
QCOMPARE(m_left->iconSize(), QSize(32, 32));
|
||||
QCOMPARE(m_top->toolButtonStyle(), Qt::ToolButtonTextUnderIcon);
|
||||
QVERIFY(!m_top->isMovable());
|
||||
QVERIFY(!m_left->isMovable());
|
||||
//A toolbar inside a panel is not one of the window's toolbars
|
||||
QVERIFY(m_in_dock->isMovable());
|
||||
//Nor is a window that did not take the settings when it opened
|
||||
QCOMPARE(m_other_bar->iconSize(), m_default_size);
|
||||
QCOMPARE(m_other_bar->toolButtonStyle(), Qt::ToolButtonIconOnly);
|
||||
QVERIFY(m_other_bar->isMovable());
|
||||
|
||||
//A page opened now shows what was saved
|
||||
ToolbarsConfigPage page;
|
||||
QCOMPARE(page.findChild<QComboBox *>(QStringLiteral("iconSizeCombo"))->currentData().toInt(), 32);
|
||||
QVERIFY(page.findChild<QCheckBox *>(QStringLiteral("lockedCheck"))->isChecked());
|
||||
}
|
||||
|
||||
void backToDefaultsRemovesTheKeys()
|
||||
{
|
||||
ToolbarSettings::save(0, Qt::ToolButtonIconOnly, false);
|
||||
QVERIFY(!QSettings().contains(QStringLiteral("toolbars/icon_size")));
|
||||
QVERIFY(!QSettings().contains(QStringLiteral("toolbars/button_style")));
|
||||
QVERIFY(!QSettings().contains(QStringLiteral("toolbars/locked")));
|
||||
ToolbarSettings::applyToAll();
|
||||
QCOMPARE(m_top->iconSize(), m_default_size);
|
||||
QCOMPARE(m_top->toolButtonStyle(), Qt::ToolButtonIconOnly);
|
||||
QVERIFY(m_top->isMovable());
|
||||
}
|
||||
|
||||
void unknownStyleFallsBack()
|
||||
{
|
||||
QSettings().setValue(QStringLiteral("toolbars/button_style"), 99);
|
||||
QCOMPARE(ToolbarSettings::buttonStyle(), Qt::ToolButtonIconOnly);
|
||||
QSettings().remove(QStringLiteral("toolbars/button_style"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(tst_toolbarsettings)
|
||||
#include "tst_toolbarsettings.moc"
|
||||
@@ -0,0 +1,121 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// qet.addElement() compares the uuid of the symbol it is given with the copy
|
||||
// already embedded under the same name. A symbol file that exists but cannot
|
||||
// be read has no uuid, and was reported as "would collide with a different
|
||||
// element" (#1178 follow-up: on Windows, any symbol whose full path reaches
|
||||
// 260 characters). Made unreadable here with the file's permissions, which
|
||||
// gives the same null uuid on Linux.
|
||||
class tst_unreadableelement : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
QString m_collection;
|
||||
QString m_symbol;
|
||||
|
||||
// Runs a script placing common://custom/my_siren.elmt into project and
|
||||
// saving it to output; returns everything the run printed.
|
||||
QString placeTheSymbol(const QString &project, const QString &output)
|
||||
{
|
||||
const QString root = m_dir.path();
|
||||
const QString home = root + QStringLiteral("/home");
|
||||
const QString settings = root + QStringLiteral("/settings");
|
||||
QDir().mkpath(settings + QStringLiteral("/QElectroTech"));
|
||||
QDir().mkpath(root + QStringLiteral("/tmp"));
|
||||
QFile ini(settings + QStringLiteral("/QElectroTech/QElectroTech.ini"));
|
||||
if (!ini.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
return QString();
|
||||
ini.write("[elements-collections]\ncommon-collection-path=");
|
||||
ini.write(m_collection.toUtf8());
|
||||
ini.write("\n");
|
||||
ini.close();
|
||||
|
||||
const QString script = root + QStringLiteral("/add.js");
|
||||
QFile js(script);
|
||||
if (!js.open(QIODevice::WriteOnly))
|
||||
return QString();
|
||||
js.write("var u = qet.addElement(0, 'common://custom/my_siren.elmt', 100, 100);\n"
|
||||
"qet.log('RESULT ' + (u ? 'placed' : 'refused'));\n"
|
||||
"if (u) qet.save('");
|
||||
js.write(output.toUtf8());
|
||||
js.write("');\n");
|
||||
js.close();
|
||||
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/.config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/.local/share"));
|
||||
env.insert(QStringLiteral("TMPDIR"), root + QStringLiteral("/tmp"));
|
||||
env.insert(QStringLiteral("QET_SETTINGS_DIR"), settings);
|
||||
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(env);
|
||||
proc.setProcessChannelMode(QProcess::MergedChannels);
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), script, project});
|
||||
if (!proc.waitForFinished(60000))
|
||||
return QString();
|
||||
return QString::fromUtf8(proc.readAll());
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
m_collection = m_dir.filePath(QStringLiteral("collection"));
|
||||
m_symbol = m_collection + QStringLiteral("/custom/my_siren.elmt");
|
||||
QVERIFY(QDir().mkpath(m_collection + QStringLiteral("/custom")));
|
||||
QVERIFY(QFile::copy(QStringLiteral(QET_ELEMENTS_DIR "/10_electric/10_allpole/380_signaling_operating/12_acoustic_signaling/sirene.elmt"),
|
||||
m_symbol));
|
||||
QVERIFY(QFile::copy(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"),
|
||||
m_dir.filePath(QStringLiteral("p.qet"))));
|
||||
}
|
||||
|
||||
void anUnreadableSymbolIsNotACollision()
|
||||
{
|
||||
// Placed once while readable: the project now embeds it.
|
||||
const QString embedded = m_dir.filePath(QStringLiteral("embedded.qet"));
|
||||
QVERIFY(placeTheSymbol(m_dir.filePath(QStringLiteral("p.qet")), embedded)
|
||||
.contains(QStringLiteral("RESULT placed")));
|
||||
QVERIFY(QFile::exists(embedded));
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
// Qt cannot take read permission away on Windows: it only knows the
|
||||
// read-only flag there, so this returns false. The check needs a
|
||||
// file the user cannot read, which Windows cannot give it.
|
||||
QSKIP("Windows cannot make the file unreadable through QFile");
|
||||
#endif
|
||||
QVERIFY(QFile::setPermissions(m_symbol, QFileDevice::Permissions()));
|
||||
QFile probe(m_symbol);
|
||||
if (probe.open(QIODevice::ReadOnly))
|
||||
QSKIP("permissions do not stop this user reading the file (root?)");
|
||||
|
||||
const QString out = placeTheSymbol(embedded, m_dir.filePath(QStringLiteral("again.qet")));
|
||||
QFile::setPermissions(m_symbol, QFileDevice::ReadOwner | QFileDevice::WriteOwner);
|
||||
QVERIFY2(out.contains(QStringLiteral("RESULT refused")), qPrintable(out));
|
||||
QVERIFY2(out.contains(QStringLiteral("could not read element")), qPrintable(out));
|
||||
QVERIFY2(!out.contains(QStringLiteral("would collide")), qPrintable(out));
|
||||
}
|
||||
|
||||
// Readable again, the same symbol is placed a second time as before.
|
||||
void theSameSymbolIsPlacedAgain()
|
||||
{
|
||||
const QString out = placeTheSymbol(m_dir.filePath(QStringLiteral("embedded.qet")),
|
||||
m_dir.filePath(QStringLiteral("twice.qet")));
|
||||
QVERIFY2(out.contains(QStringLiteral("RESULT placed")), qPrintable(out));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_unreadableelement)
|
||||
|
||||
#include "tst_unreadableelement.moc"
|
||||
@@ -0,0 +1,138 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
// A project can keep the texts drawn in its turned symbols horizontal
|
||||
// (<symbol_texts upright="true"/>, Project properties). Through the real
|
||||
// binary on tremie_vibrante.qet: the symbols of the first folio are turned
|
||||
// by 90 degrees, then the folio is exported to DXF with the setting off and
|
||||
// on. The "M" of each motor turns with its symbol when it is off, and stays
|
||||
// horizontal when it is on.
|
||||
class tst_uprightsymboltexts : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
|
||||
QProcessEnvironment environment()
|
||||
{
|
||||
const QString home = m_dir.filePath(QStringLiteral("home"));
|
||||
QDir().mkpath(home);
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/data"));
|
||||
env.insert(QStringLiteral("TMPDIR"), m_dir.path());
|
||||
return env;
|
||||
}
|
||||
|
||||
bool runQet(const QStringList &arguments)
|
||||
{
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(environment());
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), arguments);
|
||||
return proc.waitForFinished(120000) && proc.exitCode() == 0;
|
||||
}
|
||||
|
||||
// The rotation (DXF group code 50) of every TEXT entity of @p dxf whose
|
||||
// text (group code 1) is @p text
|
||||
static QList<double> textRotations(const QString &dxf, const QString &text)
|
||||
{
|
||||
QFile file(dxf);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return {};
|
||||
const QStringList lines = QString::fromUtf8(file.readAll())
|
||||
.split(QLatin1Char('\n'));
|
||||
QList<double> rotations;
|
||||
bool in_text = false, match = false;
|
||||
double rotation = 0;
|
||||
for (int i = 0; i + 1 < lines.size(); i += 2) {
|
||||
const QString code = lines.at(i).trimmed();
|
||||
const QString value = lines.at(i + 1).trimmed();
|
||||
if (code == QLatin1String("0")) {
|
||||
if (in_text && match) rotations << rotation;
|
||||
in_text = value == QLatin1String("TEXT");
|
||||
match = false;
|
||||
rotation = 0;
|
||||
} else if (in_text && code == QLatin1String("1")) {
|
||||
match = value == text;
|
||||
} else if (in_text && code == QLatin1String("50")) {
|
||||
rotation = value.toDouble();
|
||||
}
|
||||
}
|
||||
return rotations;
|
||||
}
|
||||
|
||||
// The DXF of the first folio of @p project, exported into @p name
|
||||
QString exportDxf(const QString &project, const QString &name)
|
||||
{
|
||||
const QString out = m_dir.filePath(name);
|
||||
QDir().mkpath(out);
|
||||
if (!runQet({QStringLiteral("--export-dxf"), project, out})) return {};
|
||||
const QStringList files = QDir(out).entryList({QStringLiteral("01*.dxf")});
|
||||
return files.isEmpty() ? QString() : QDir(out).filePath(files.first());
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void motorTextStaysHorizontal()
|
||||
{
|
||||
//The symbols of the first folio, turned by 90 degrees
|
||||
const QString off = m_dir.filePath(QStringLiteral("off.qet"));
|
||||
const QString script = m_dir.filePath(QStringLiteral("turn.js"));
|
||||
QFile js(script);
|
||||
QVERIFY(js.open(QIODevice::WriteOnly));
|
||||
js.write(QStringLiteral(
|
||||
"var els = qet.elementUuids(0);\n"
|
||||
"for (var e = 0; e < els.length; e++) qet.rotateElement(0, els[e], 90);\n"
|
||||
"qet.save(%1);\n").arg(QLatin1Char('"') + off + QLatin1Char('"')).toUtf8());
|
||||
js.close();
|
||||
QVERIFY(runQet({QStringLiteral("--run"), script,
|
||||
QStringLiteral(QET_EXAMPLES_DIR "/tremie_vibrante.qet")}));
|
||||
|
||||
//An existing project is read with the setting off, and saved so
|
||||
QFile off_file(off);
|
||||
QVERIFY(off_file.open(QIODevice::ReadOnly));
|
||||
QString xml = QString::fromUtf8(off_file.readAll());
|
||||
off_file.close();
|
||||
QVERIFY(!xml.contains(QStringLiteral("symbol_texts")));
|
||||
|
||||
//The same project with the setting on
|
||||
const QString on = m_dir.filePath(QStringLiteral("on.qet"));
|
||||
const int root_end = xml.indexOf(QLatin1Char('>'), xml.indexOf(QStringLiteral("<project"))) + 1;
|
||||
QVERIFY(root_end > 0);
|
||||
xml.insert(root_end, QStringLiteral("\n<symbol_texts upright=\"true\"/>"));
|
||||
QFile on_file(on);
|
||||
QVERIFY(on_file.open(QIODevice::WriteOnly));
|
||||
on_file.write(xml.toUtf8());
|
||||
on_file.close();
|
||||
|
||||
const QList<double> turned = textRotations(exportDxf(off, QStringLiteral("dxf-off")),
|
||||
QStringLiteral("M"));
|
||||
const QList<double> upright = textRotations(exportDxf(on, QStringLiteral("dxf-on")),
|
||||
QStringLiteral("M"));
|
||||
//The two motors of the folio
|
||||
QCOMPARE(turned.size(), 2);
|
||||
QCOMPARE(upright.size(), 2);
|
||||
for (double rotation : turned)
|
||||
QCOMPARE(std::fmod(rotation, 360.0), 270.0);
|
||||
for (double rotation : upright)
|
||||
QCOMPARE(std::fmod(rotation, 360.0), 0.0);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_APPLESS_MAIN(tst_uprightsymboltexts)
|
||||
|
||||
#include "tst_uprightsymboltexts.moc"
|
||||
@@ -0,0 +1,388 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include <QDir>
|
||||
#include <QDomDocument>
|
||||
#include <QFile>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
#include "wiringrules.h"
|
||||
|
||||
// The checks that drive QElectroTech through a script (--run) need a
|
||||
// build with scripting; without it --run is not an option and the
|
||||
// process never exits (QET_HAS_SCRIPTING, top-level CMakeLists.txt).
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
# define SKIP_WITHOUT_SCRIPTING
|
||||
#else
|
||||
# define SKIP_WITHOUT_SCRIPTING QSKIP("built without scripting: --run is not available")
|
||||
#endif
|
||||
|
||||
// How many wires a terminal may take (discussion #1158). The rules are
|
||||
// tested on their own; the project setting and the refusal are tested
|
||||
// through the real binary: --resave keeps a <wiring_rules> element and
|
||||
// adds none to a project without it, and qet.addConductor() is refused
|
||||
// past the limit unless the master switch is off.
|
||||
class tst_wiringrules : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
QTemporaryDir m_dir;
|
||||
int m_run = 0;
|
||||
|
||||
// Environment of one sandboxed run; @p master_off writes the master
|
||||
// switch off into that run's own settings, @p application_max_wires
|
||||
// the limit every project follows unless it sets its own.
|
||||
QProcessEnvironment sandbox(bool master_off = false, int application_max_wires = 0)
|
||||
{
|
||||
const QString home = m_dir.filePath(QStringLiteral("home%1").arg(m_run));
|
||||
const QString tmp = m_dir.filePath(QStringLiteral("tmp%1").arg(m_run));
|
||||
const QString settings = m_dir.filePath(QStringLiteral("settings%1").arg(m_run));
|
||||
++m_run;
|
||||
QDir().mkpath(home);
|
||||
QDir().mkpath(tmp);
|
||||
QDir().mkpath(settings + QStringLiteral("/QElectroTech"));
|
||||
if (master_off || application_max_wires) {
|
||||
QFile ini(settings + QStringLiteral("/QElectroTech/QElectroTech.ini"));
|
||||
if (ini.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
ini.write(QStringLiteral("[diagrameditor]\nwiring_rules_enabled=%1\nwiring_rules_max_wires=%2\n")
|
||||
.arg(master_off ? QStringLiteral("false") : QStringLiteral("true"))
|
||||
.arg(application_max_wires).toUtf8());
|
||||
}
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
env.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("offscreen"));
|
||||
env.insert(QStringLiteral("QET_ENABLE_SCRIPTING"), QStringLiteral("1"));
|
||||
env.insert(QStringLiteral("HOME"), home);
|
||||
env.insert(QStringLiteral("XDG_CONFIG_HOME"), home + QStringLiteral("/.config"));
|
||||
env.insert(QStringLiteral("XDG_DATA_HOME"), home + QStringLiteral("/.local/share"));
|
||||
env.insert(QStringLiteral("QET_SETTINGS_DIR"), settings);
|
||||
env.insert(QStringLiteral("TMPDIR"), tmp);
|
||||
return env;
|
||||
}
|
||||
|
||||
// The fixture with @p rules inserted before <newdiagrams>, as a new file
|
||||
QString fixtureWith(const QString &rules)
|
||||
{
|
||||
QFile source(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"));
|
||||
if (!source.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
return {};
|
||||
QString text = QString::fromUtf8(source.readAll());
|
||||
const int newdiagrams = text.indexOf(QLatin1String("<newdiagrams"));
|
||||
if (newdiagrams < 0)
|
||||
return {};
|
||||
text.insert(newdiagrams, rules + QStringLiteral("\n "));
|
||||
const QString path = m_dir.filePath(QStringLiteral("fixture%1.qet").arg(m_run));
|
||||
QFile out(path);
|
||||
if (!out.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
return {};
|
||||
out.write(text.toUtf8());
|
||||
return path;
|
||||
}
|
||||
|
||||
// Runs --resave on @p in, returns the saved file's text (empty on failure)
|
||||
QString resave(const QString &in)
|
||||
{
|
||||
const QString out = m_dir.filePath(QStringLiteral("out%1.qet").arg(m_run));
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(sandbox());
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--resave"), in, out});
|
||||
if (!proc.waitForFinished(120000) || proc.exitCode() != 0)
|
||||
return {};
|
||||
QFile file(out);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
return {};
|
||||
return QString::fromUtf8(file.readAll());
|
||||
}
|
||||
|
||||
// Wires a free terminal to a terminal that already has a wire, through
|
||||
// qet.addConductor(); returns what the script printed after "PROBE ".
|
||||
QString addWireToWiredTerminal(const QString &project, bool master_off,
|
||||
int application_max_wires = 0)
|
||||
{
|
||||
const QString script_path = m_dir.filePath(QStringLiteral("probe%1.js").arg(m_run));
|
||||
QFile script(script_path);
|
||||
if (!script.open(QIODevice::WriteOnly))
|
||||
return {};
|
||||
// The first end of the first conductor is a terminal with a wire.
|
||||
// Any terminal of another element with no wire is the other end.
|
||||
script.write(
|
||||
"var end = qet.conductorEnds(0, qet.conductorUuids(0)[0])[0].split(' terminal ');\n"
|
||||
"var wired = {};\n"
|
||||
"qet.conductorUuids(0).forEach(function (u) {\n"
|
||||
" qet.conductorEnds(0, u).forEach(function (e) { wired[e] = true; }); });\n"
|
||||
"var free = null;\n"
|
||||
"qet.elementUuids(0).forEach(function (el) {\n"
|
||||
" if (free || el == end[0]) return;\n"
|
||||
" var n = qet.elementTerminals(0, el).length;\n"
|
||||
" for (var i = 0; i < n && !free; ++i)\n"
|
||||
" if (!wired[el + ' terminal ' + i]) free = [el, i]; });\n"
|
||||
"qet.log('PROBE ' + (free ? qet.addConductor(0, end[0], parseInt(end[1]), free[0], free[1])\n"
|
||||
" : 'nofree'));\n");
|
||||
script.close();
|
||||
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(sandbox(master_off, application_max_wires));
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), script_path, project});
|
||||
if (!proc.waitForFinished(60000))
|
||||
return {};
|
||||
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
|
||||
+ proc.readAllStandardError());
|
||||
const QString mark = QStringLiteral("PROBE ");
|
||||
for (const QString &line : out.split(QLatin1Char('\n'))) {
|
||||
const int i = line.indexOf(mark);
|
||||
if (i >= 0)
|
||||
return line.mid(i + mark.size()).trimmed();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// On a fixture with @p rules: a symbol wired to four others is
|
||||
// deleted, so QElectroTech rewires the four to keep the potential.
|
||||
// Returns the most wires any one of them ends with.
|
||||
QString mostWiresAfterDeletingTheHub(const QString &rules)
|
||||
{
|
||||
const QString project = fixtureWith(rules);
|
||||
const QString script_path = m_dir.filePath(QStringLiteral("hub%1.js").arg(m_run));
|
||||
QFile script(script_path);
|
||||
if (project.isEmpty() || !script.open(QIODevice::WriteOnly))
|
||||
return {};
|
||||
// Placed on a diagonal, so no two terminals line up and nothing
|
||||
// is auto-connected.
|
||||
script.write(
|
||||
"var p = 'embed://import/probe/v2_fuse.elmt';\n"
|
||||
"var hub = qet.addElement(0, p, 400, 400);\n"
|
||||
"var others = [];\n"
|
||||
"for (var i = 1; i <= 4; ++i) others.push(qet.addElement(0, p, 400 + 70 * i, 400 + 90 * i));\n"
|
||||
"others.forEach(function (o) { qet.addConductor(0, hub, 0, o, 0); });\n"
|
||||
"qet.deleteElement(0, hub);\n"
|
||||
"var count = {};\n"
|
||||
"qet.conductorUuids(0).forEach(function (u) {\n"
|
||||
" qet.conductorEnds(0, u).forEach(function (e) { count[e] = (count[e] || 0) + 1; }); });\n"
|
||||
"var most = 0;\n"
|
||||
"others.forEach(function (o) { most = Math.max(most, count[o + ' terminal 0'] || 0); });\n"
|
||||
"qet.log('PROBE ' + most);\n");
|
||||
script.close();
|
||||
|
||||
QProcess proc;
|
||||
proc.setProcessEnvironment(sandbox());
|
||||
proc.start(QStringLiteral(QET_TEST_BINARY_PATH), {QStringLiteral("--run"), script_path, project});
|
||||
if (!proc.waitForFinished(60000))
|
||||
return {};
|
||||
const QString out = QString::fromUtf8(proc.readAllStandardOutput()
|
||||
+ proc.readAllStandardError());
|
||||
const QString mark = QStringLiteral("PROBE ");
|
||||
for (const QString &line : out.split(QLatin1Char('\n'))) {
|
||||
const int i = line.indexOf(mark);
|
||||
if (i >= 0)
|
||||
return line.mid(i + mark.size()).trimmed();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private slots:
|
||||
void initTestCase()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QVERIFY(QFile::exists(QStringLiteral(QET_TEST_BINARY_PATH)));
|
||||
}
|
||||
|
||||
void limit()
|
||||
{
|
||||
WiringRules::Settings rules;
|
||||
QCOMPARE(WiringRules::limit(rules, true, false), 0);
|
||||
QCOMPARE(WiringRules::limit(rules, true, true), 0);
|
||||
|
||||
rules.max_wires = 4;
|
||||
QCOMPARE(WiringRules::limit(rules, true, false), 4);
|
||||
QCOMPARE(WiringRules::limit(rules, true, true), 4);
|
||||
|
||||
rules.one_wire_per_report = true;
|
||||
QCOMPARE(WiringRules::limit(rules, true, true), 1);
|
||||
QCOMPARE(WiringRules::limit(rules, true, false), 4);
|
||||
|
||||
// The master switch turns every rule off
|
||||
QCOMPARE(WiringRules::limit(rules, false, false), 0);
|
||||
QCOMPARE(WiringRules::limit(rules, false, true), 0);
|
||||
}
|
||||
|
||||
void hasRoom()
|
||||
{
|
||||
QVERIFY(WiringRules::hasRoom(0, 0));
|
||||
QVERIFY(WiringRules::hasRoom(0, 19));
|
||||
QVERIFY(WiringRules::hasRoom(2, 1));
|
||||
QVERIFY(!WiringRules::hasRoom(2, 2));
|
||||
QVERIFY(!WiringRules::hasRoom(2, 3));
|
||||
QVERIFY(!WiringRules::hasRoom(1, 1));
|
||||
}
|
||||
|
||||
void chainsOnlyUnderALimit()
|
||||
{
|
||||
WiringRules::Settings rules;
|
||||
QVERIFY(!WiringRules::chainsWires(rules, true));
|
||||
rules.one_wire_per_report = true;
|
||||
QVERIFY(!WiringRules::chainsWires(rules, true));
|
||||
rules.max_wires = 2;
|
||||
QVERIFY(WiringRules::chainsWires(rules, true));
|
||||
QVERIFY(!WiringRules::chainsWires(rules, false));
|
||||
}
|
||||
|
||||
void chainOrder()
|
||||
{
|
||||
QCOMPARE(WiringRules::chainOrder({}), QList<int>());
|
||||
QCOMPARE(WiringRules::chainOrder({QPointF(5, 5)}), QList<int>({0}));
|
||||
|
||||
// Starts top left, then always the nearest along the grid
|
||||
const QList<QPointF> row {QPointF(300, 0), QPointF(0, 0), QPointF(200, 0), QPointF(100, 0)};
|
||||
QCOMPARE(WiringRules::chainOrder(row), QList<int>({1, 3, 2, 0}));
|
||||
|
||||
// Same x: the higher one starts
|
||||
const QList<QPointF> column {QPointF(0, 100), QPointF(0, 0), QPointF(0, 50)};
|
||||
QCOMPARE(WiringRules::chainOrder(column), QList<int>({1, 2, 0}));
|
||||
|
||||
// Each index exactly once
|
||||
const QList<QPointF> scattered {QPointF(40, 90), QPointF(10, 300), QPointF(220, 10),
|
||||
QPointF(10, 10), QPointF(220, 300)};
|
||||
QList<int> order = WiringRules::chainOrder(scattered);
|
||||
QCOMPARE(order.first(), 3);
|
||||
std::sort(order.begin(), order.end());
|
||||
QCOMPARE(order, QList<int>({0, 1, 2, 3, 4}));
|
||||
}
|
||||
|
||||
void projectOverridesApplication()
|
||||
{
|
||||
WiringRules::Settings application;
|
||||
application.max_wires = 2;
|
||||
application.one_wire_per_report = true;
|
||||
|
||||
// A project that follows the application gets its rules
|
||||
WiringRules::Settings follows;
|
||||
QCOMPARE(WiringRules::effective(follows, application).max_wires, 2);
|
||||
QVERIFY(WiringRules::effective(follows, application).one_wire_per_report);
|
||||
QVERIFY(!WiringRules::effective(follows, application).own);
|
||||
|
||||
// Its own rules win, "no limit" included
|
||||
WiringRules::Settings own;
|
||||
own.own = true;
|
||||
QCOMPARE(WiringRules::effective(own, application).max_wires, 0);
|
||||
QVERIFY(!WiringRules::effective(own, application).one_wire_per_report);
|
||||
QVERIFY(WiringRules::effective(own, application).own);
|
||||
own.max_wires = 6;
|
||||
QCOMPARE(WiringRules::effective(own, application).max_wires, 6);
|
||||
}
|
||||
|
||||
void warnsWhenARuleIsTurnedOn()
|
||||
{
|
||||
WiringRules::Settings off;
|
||||
WiringRules::Settings limit;
|
||||
limit.max_wires = 4;
|
||||
WiringRules::Settings report;
|
||||
report.one_wire_per_report = true;
|
||||
|
||||
QVERIFY(WiringRules::turnsRuleOn(off, limit));
|
||||
QVERIFY(WiringRules::turnsRuleOn(off, report));
|
||||
QVERIFY(WiringRules::turnsRuleOn(limit, [&]{ auto s = limit; s.one_wire_per_report = true; return s; }()));
|
||||
// Changing a limit that was already on, or turning rules off: no warning
|
||||
QVERIFY(!WiringRules::turnsRuleOn(limit, [&]{ auto s = limit; s.max_wires = 2; return s; }()));
|
||||
QVERIFY(!WiringRules::turnsRuleOn(limit, off));
|
||||
QVERIFY(!WiringRules::turnsRuleOn(off, off));
|
||||
}
|
||||
|
||||
void xmlRoundTrip()
|
||||
{
|
||||
QDomDocument doc;
|
||||
QDomElement root = doc.createElement(QStringLiteral("project"));
|
||||
doc.appendChild(root);
|
||||
|
||||
// Following the application: nothing written, and reading
|
||||
// nothing follows the application
|
||||
WiringRules::Settings follows;
|
||||
follows.max_wires = 3; // not the project's: not written
|
||||
WiringRules::toXml(follows, root);
|
||||
QVERIFY(root.firstChildElement().isNull());
|
||||
QVERIFY(WiringRules::fromXml(root).isDefault());
|
||||
|
||||
// Its own rules, even "none", are written and read back as its own
|
||||
WiringRules::Settings none;
|
||||
none.own = true;
|
||||
WiringRules::toXml(none, root);
|
||||
QCOMPARE(root.firstChildElement().tagName(), QStringLiteral("wiring_rules"));
|
||||
QVERIFY(WiringRules::fromXml(root) == none);
|
||||
root.removeChild(root.firstChildElement());
|
||||
|
||||
WiringRules::Settings rules;
|
||||
rules.own = true;
|
||||
rules.max_wires = 2;
|
||||
rules.one_wire_per_report = true;
|
||||
WiringRules::toXml(rules, root);
|
||||
QCOMPARE(root.firstChildElement().tagName(), QStringLiteral("wiring_rules"));
|
||||
QVERIFY(WiringRules::fromXml(root) == rules);
|
||||
}
|
||||
|
||||
void savedSettingSurvivesResave()
|
||||
{
|
||||
const QString plain = resave(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"));
|
||||
QVERIFY2(!plain.isEmpty(), "--resave failed");
|
||||
QVERIFY(!plain.contains(QLatin1String("wiring_rules")));
|
||||
|
||||
const QString with_rules = fixtureWith(
|
||||
QStringLiteral("<wiring_rules max_wires_per_terminal=\"2\" one_wire_per_report=\"true\"/>"));
|
||||
QVERIFY(!with_rules.isEmpty());
|
||||
const QString saved = resave(with_rules);
|
||||
QVERIFY2(!saved.isEmpty(), "--resave failed");
|
||||
QVERIFY2(saved.contains(QLatin1String("max_wires_per_terminal=\"2\""))
|
||||
&& saved.contains(QLatin1String("one_wire_per_report=\"true\"")),
|
||||
"the setting was lost on save");
|
||||
|
||||
// A project's own "no rule" is kept too: it overrides the application's
|
||||
const QString own_none = fixtureWith(QStringLiteral("<wiring_rules/>"));
|
||||
QVERIFY(!own_none.isEmpty());
|
||||
const QString saved_none = resave(own_none);
|
||||
QVERIFY2(!saved_none.isEmpty(), "--resave failed");
|
||||
QVERIFY2(saved_none.contains(QLatin1String("<wiring_rules/>")),
|
||||
"a project's own rules were lost on save");
|
||||
}
|
||||
|
||||
void wirePastTheLimitIsRefused()
|
||||
{
|
||||
SKIP_WITHOUT_SCRIPTING;
|
||||
// Without a rule the wire is drawn, as on master
|
||||
QCOMPARE(addWireToWiredTerminal(QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet"), false),
|
||||
QStringLiteral("true"));
|
||||
|
||||
// One wire per terminal: the terminal is full
|
||||
const QString limited = fixtureWith(
|
||||
QStringLiteral("<wiring_rules max_wires_per_terminal=\"1\"/>"));
|
||||
QVERIFY(!limited.isEmpty());
|
||||
QCOMPARE(addWireToWiredTerminal(limited, false), QStringLiteral("false"));
|
||||
|
||||
// The master switch off: the project's rule does nothing
|
||||
QCOMPARE(addWireToWiredTerminal(limited, true), QStringLiteral("true"));
|
||||
|
||||
// The application's limit applies to a project that sets none...
|
||||
const QString plain = QFINDTESTDATA("fixtures/qet_bug_repro_resaved.qet");
|
||||
QCOMPARE(addWireToWiredTerminal(plain, false, 1), QStringLiteral("false"));
|
||||
// ...not to one that sets its own, here "no limit"
|
||||
const QString own_none = fixtureWith(QStringLiteral("<wiring_rules/>"));
|
||||
QVERIFY(!own_none.isEmpty());
|
||||
QCOMPARE(addWireToWiredTerminal(own_none, false, 1), QStringLiteral("true"));
|
||||
// ...and the master switch still turns it off
|
||||
QCOMPARE(addWireToWiredTerminal(plain, true, 1), QStringLiteral("true"));
|
||||
}
|
||||
|
||||
void deletingASymbolChainsTheWires()
|
||||
{
|
||||
SKIP_WITHOUT_SCRIPTING;
|
||||
// No rule: the four are wired to one of them, as on master
|
||||
QCOMPARE(mostWiresAfterDeletingTheHub(QString()), QStringLiteral("3"));
|
||||
|
||||
// A limit: one after another, none gets more than two
|
||||
QCOMPARE(mostWiresAfterDeletingTheHub(
|
||||
QStringLiteral("<wiring_rules max_wires_per_terminal=\"4\"/>")),
|
||||
QStringLiteral("2"));
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_wiringrules)
|
||||
|
||||
#include "tst_wiringrules.moc"
|
||||
@@ -0,0 +1,94 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#include <QtTest>
|
||||
|
||||
#include "properties/xrefproperties.h"
|
||||
|
||||
/**
|
||||
XRefProperties reads the cross-reference position from a project's
|
||||
<xref xrefpos="..."> and from the settings file. Older versions saved an
|
||||
empty value; read as-is it drew the cross-reference over the element's
|
||||
label (issue #1238). An empty or unknown value must fall back to the
|
||||
default, AlignBottom, and a valid one must be kept.
|
||||
*/
|
||||
class tst_xrefpos : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
static Qt::AlignmentFlag fromXml(const QString &attribute, bool present = true)
|
||||
{
|
||||
QDomDocument doc;
|
||||
QDomElement e = doc.createElement("xref");
|
||||
e.setAttribute("type", "protection");
|
||||
if (present)
|
||||
e.setAttribute("xrefpos", attribute);
|
||||
XRefProperties xrp;
|
||||
xrp.fromXml(e);
|
||||
return xrp.getXrefPos();
|
||||
}
|
||||
|
||||
private slots:
|
||||
void xml_data()
|
||||
{
|
||||
QTest::addColumn<QString>("stored");
|
||||
QTest::addColumn<int>("expected");
|
||||
QTest::newRow("empty") << "" << int(Qt::AlignBottom);
|
||||
QTest::newRow("garbage") << "Sideways" << int(Qt::AlignBottom);
|
||||
QTest::newRow("not offered") << "AlignJustify" << int(Qt::AlignBottom);
|
||||
QTest::newRow("bottom") << "AlignBottom" << int(Qt::AlignBottom);
|
||||
QTest::newRow("top") << "AlignTop" << int(Qt::AlignTop);
|
||||
QTest::newRow("left") << "AlignLeft" << int(Qt::AlignLeft);
|
||||
QTest::newRow("right") << "AlignRight" << int(Qt::AlignRight);
|
||||
QTest::newRow("baseline") << "AlignBaseline" << int(Qt::AlignBaseline);
|
||||
QTest::newRow("hcenter") << "AlignHCenter" << int(Qt::AlignHCenter);
|
||||
}
|
||||
|
||||
void xml()
|
||||
{
|
||||
QFETCH(QString, stored);
|
||||
QFETCH(int, expected);
|
||||
QCOMPARE(int(fromXml(stored)), expected);
|
||||
}
|
||||
|
||||
void xmlMissing()
|
||||
{
|
||||
QCOMPARE(fromXml(QString(), false), Qt::AlignBottom);
|
||||
}
|
||||
|
||||
/**
|
||||
An empty value is written back as "AlignBottom" on the next save,
|
||||
so the file heals instead of carrying the empty value forward.
|
||||
*/
|
||||
void emptyIsRewritten()
|
||||
{
|
||||
QDomDocument doc;
|
||||
QDomElement e = doc.createElement("xref");
|
||||
e.setAttribute("xrefpos", "");
|
||||
XRefProperties xrp;
|
||||
xrp.fromXml(e);
|
||||
QCOMPARE(xrp.toXml(doc).attribute("xrefpos"), QStringLiteral("AlignBottom"));
|
||||
}
|
||||
|
||||
void settings()
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
QSettings s(dir.filePath("qet.ini"), QSettings::IniFormat);
|
||||
XRefProperties xrp;
|
||||
|
||||
s.setValue("xrefprotectionxrefpos", "");
|
||||
xrp.fromSettings(s, "xrefprotection");
|
||||
QCOMPARE(xrp.getXrefPos(), Qt::AlignBottom);
|
||||
|
||||
s.remove("xrefprotectionxrefpos");
|
||||
xrp.setXrefPos(Qt::AlignTop);
|
||||
xrp.fromSettings(s, "xrefprotection");
|
||||
QCOMPARE(xrp.getXrefPos(), Qt::AlignBottom);
|
||||
|
||||
s.setValue("xrefprotectionxrefpos", "AlignRight");
|
||||
xrp.fromSettings(s, "xrefprotection");
|
||||
QCOMPARE(xrp.getXrefPos(), Qt::AlignRight);
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(tst_xrefpos)
|
||||
#include "tst_xrefpos.moc"
|
||||
Reference in New Issue
Block a user