Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffbbd62355 | ||
|
|
a5413efc37 | ||
|
|
8d69fa3e5a | ||
|
|
97700c5ab5 | ||
|
|
ff2f42016a | ||
|
|
94266c2819 | ||
|
|
dc0e269186 | ||
|
|
da0611a301 |
@@ -35,6 +35,7 @@ jobs:
|
|||||||
// kind of change
|
// kind of change
|
||||||
'crash',
|
'crash',
|
||||||
'bug-fix',
|
'bug-fix',
|
||||||
|
'SECURITY',
|
||||||
'enhancement',
|
'enhancement',
|
||||||
'QoL',
|
'QoL',
|
||||||
'optimization',
|
'optimization',
|
||||||
@@ -193,6 +194,7 @@ jobs:
|
|||||||
// kind of change
|
// kind of change
|
||||||
'crash',
|
'crash',
|
||||||
'bug-fix',
|
'bug-fix',
|
||||||
|
'SECURITY',
|
||||||
'enhancement',
|
'enhancement',
|
||||||
'QoL',
|
'QoL',
|
||||||
'optimization',
|
'optimization',
|
||||||
|
|||||||
@@ -834,6 +834,37 @@ find_package(OpenSSL REQUIRED)
|
|||||||
find_package(CURL REQUIRED)
|
find_package(CURL REQUIRED)
|
||||||
find_package(Freetype REQUIRED)
|
find_package(Freetype REQUIRED)
|
||||||
|
|
||||||
|
if (SLIC3R_GUI)
|
||||||
|
# LibDataChannel's installed export references its bundled dependencies,
|
||||||
|
# but does not install their CMake targets. Recreate those targets from
|
||||||
|
# the same dependency prefix before loading the LibDataChannel config.
|
||||||
|
if (NOT TARGET Usrsctp::usrsctp)
|
||||||
|
find_library(_ORCA_USRSCTP_LIBRARY NAMES usrsctp
|
||||||
|
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
|
||||||
|
if (_ORCA_USRSCTP_LIBRARY)
|
||||||
|
add_library(Usrsctp::usrsctp UNKNOWN IMPORTED GLOBAL)
|
||||||
|
set_target_properties(Usrsctp::usrsctp PROPERTIES
|
||||||
|
IMPORTED_LOCATION "${_ORCA_USRSCTP_LIBRARY}"
|
||||||
|
IMPORTED_LINK_INTERFACE_LANGUAGES C
|
||||||
|
INTERFACE_LINK_LIBRARIES "Threads::Threads")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT TARGET LibJuice::LibJuice)
|
||||||
|
find_library(_ORCA_LIBJUICE_LIBRARY NAMES juice
|
||||||
|
PATHS "${CMAKE_PREFIX_PATH}/lib" NO_DEFAULT_PATH)
|
||||||
|
if (_ORCA_LIBJUICE_LIBRARY)
|
||||||
|
add_library(LibJuice::LibJuice UNKNOWN IMPORTED GLOBAL)
|
||||||
|
set_target_properties(LibJuice::LibJuice PROPERTIES
|
||||||
|
IMPORTED_LOCATION "${_ORCA_LIBJUICE_LIBRARY}"
|
||||||
|
IMPORTED_LINK_INTERFACE_LANGUAGES C
|
||||||
|
INTERFACE_LINK_LIBRARIES "Threads::Threads")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_package(LibDataChannel CONFIG REQUIRED)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
add_library(libcurl INTERFACE)
|
add_library(libcurl INTERFACE)
|
||||||
target_link_libraries(libcurl INTERFACE CURL::libcurl)
|
target_link_libraries(libcurl INTERFACE CURL::libcurl)
|
||||||
@@ -1114,6 +1145,7 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
|
|||||||
endif ()
|
endif ()
|
||||||
file(COPY ${_occt_dlls}
|
file(COPY ${_occt_dlls}
|
||||||
${CMAKE_PREFIX_PATH}/bin/freetype.dll
|
${CMAKE_PREFIX_PATH}/bin/freetype.dll
|
||||||
|
${CMAKE_PREFIX_PATH}/bin/avformat-61.dll
|
||||||
${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll
|
${CMAKE_PREFIX_PATH}/bin/avcodec-61.dll
|
||||||
${CMAKE_PREFIX_PATH}/bin/swresample-5.dll
|
${CMAKE_PREFIX_PATH}/bin/swresample-5.dll
|
||||||
${CMAKE_PREFIX_PATH}/bin/swscale-8.dll
|
${CMAKE_PREFIX_PATH}/bin/swscale-8.dll
|
||||||
@@ -1126,6 +1158,7 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
|
|||||||
${_out_dir}/WebView2Loader.dll
|
${_out_dir}/WebView2Loader.dll
|
||||||
|
|
||||||
${_out_dir}/freetype.dll
|
${_out_dir}/freetype.dll
|
||||||
|
${_out_dir}/avformat-61.dll
|
||||||
${_out_dir}/avcodec-61.dll
|
${_out_dir}/avcodec-61.dll
|
||||||
${_out_dir}/swresample-5.dll
|
${_out_dir}/swresample-5.dll
|
||||||
${_out_dir}/swscale-8.dll
|
${_out_dir}/swscale-8.dll
|
||||||
@@ -1149,7 +1182,10 @@ function(orcaslicer_copy_sos target config postfix output_sos)
|
|||||||
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}")
|
set(_out_dir "${CMAKE_CURRENT_BINARY_DIR}")
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
file(COPY ${CMAKE_PREFIX_PATH}/lib/libavcodec.so
|
file(COPY ${CMAKE_PREFIX_PATH}/lib/libavformat.so
|
||||||
|
${CMAKE_PREFIX_PATH}/lib/libavformat.so.61
|
||||||
|
${CMAKE_PREFIX_PATH}/lib/libavformat.so.61.1.100
|
||||||
|
${CMAKE_PREFIX_PATH}/lib/libavcodec.so
|
||||||
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61
|
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61
|
||||||
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100
|
${CMAKE_PREFIX_PATH}/lib/libavcodec.so.61.3.100
|
||||||
${CMAKE_PREFIX_PATH}/lib/libavutil.so
|
${CMAKE_PREFIX_PATH}/lib/libavutil.so
|
||||||
@@ -1164,6 +1200,9 @@ function(orcaslicer_copy_sos target config postfix output_sos)
|
|||||||
DESTINATION ${_out_dir})
|
DESTINATION ${_out_dir})
|
||||||
|
|
||||||
set(${output_sos}
|
set(${output_sos}
|
||||||
|
${_out_dir}/libavformat.so
|
||||||
|
${_out_dir}/libavformat.so.61
|
||||||
|
${_out_dir}/libavformat.so.61.1.100
|
||||||
${_out_dir}/libavcodec.so
|
${_out_dir}/libavcodec.so
|
||||||
${_out_dir}/libavcodec.so.61
|
${_out_dir}/libavcodec.so.61
|
||||||
${_out_dir}/libavcodec.so.61.3.100
|
${_out_dir}/libavcodec.so.61.3.100
|
||||||
@@ -1293,6 +1332,8 @@ endif ()
|
|||||||
|
|
||||||
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
set(LIBRARY_FILES
|
set(LIBRARY_FILES
|
||||||
|
${LIBDIR_BIN}/libavformat.so.61
|
||||||
|
${LIBDIR_BIN}/libavformat.so.61.1.100
|
||||||
${LIBDIR_BIN}/libavcodec.so.61
|
${LIBDIR_BIN}/libavcodec.so.61
|
||||||
${LIBDIR_BIN}/libavcodec.so.61.3.100
|
${LIBDIR_BIN}/libavcodec.so.61.3.100
|
||||||
${LIBDIR_BIN}/libavutil.so.59
|
${LIBDIR_BIN}/libavutil.so.59
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ if (NOT _is_multi AND NOT CMAKE_BUILD_TYPE)
|
|||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
function(orcaslicer_add_cmake_project projectname)
|
function(orcaslicer_add_cmake_project projectname)
|
||||||
cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND" "CMAKE_ARGS" ${ARGN})
|
cmake_parse_arguments(P_ARGS "FORWARD_CONFIG" "INSTALL_DIR;BUILD_COMMAND;INSTALL_COMMAND;SOURCE_DIR" "CMAKE_ARGS" ${ARGN})
|
||||||
|
|
||||||
# MSVC is true for clang-cl as well, so the sub-build toolchain has to key on the
|
# MSVC is true for clang-cl as well, so the sub-build toolchain has to key on the
|
||||||
# generator. A non-Visual-Studio superbuild passes its own generator down, and with
|
# generator. A non-Visual-Studio superbuild passes its own generator down, and with
|
||||||
@@ -210,12 +210,18 @@ function(orcaslicer_add_cmake_project projectname)
|
|||||||
set(_build_j "-j${NPROC}")
|
set(_build_j "-j${NPROC}")
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
set(_source_dir_arg "")
|
||||||
|
if (P_ARGS_SOURCE_DIR)
|
||||||
|
set(_source_dir_arg SOURCE_DIR ${P_ARGS_SOURCE_DIR})
|
||||||
|
endif ()
|
||||||
|
|
||||||
if (NOT IS_CROSS_COMPILE OR NOT APPLE)
|
if (NOT IS_CROSS_COMPILE OR NOT APPLE)
|
||||||
ExternalProject_Add(
|
ExternalProject_Add(
|
||||||
dep_${projectname}
|
dep_${projectname}
|
||||||
EXCLUDE_FROM_ALL ON
|
EXCLUDE_FROM_ALL ON
|
||||||
INSTALL_DIR ${DESTDIR}
|
INSTALL_DIR ${DESTDIR}
|
||||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname}
|
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname}
|
||||||
|
${_source_dir_arg}
|
||||||
${_gen}
|
${_gen}
|
||||||
CMAKE_ARGS
|
CMAKE_ARGS
|
||||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||||
@@ -249,12 +255,14 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
|
|||||||
# note for future devs: shared libs may actually create a size reduction
|
# note for future devs: shared libs may actually create a size reduction
|
||||||
# but orcaslicer_deps tends to get really funny regarding linking after that (notably boost)
|
# but orcaslicer_deps tends to get really funny regarding linking after that (notably boost)
|
||||||
# so, as much as I would like to use that, it's not happening
|
# so, as much as I would like to use that, it's not happening
|
||||||
|
if (NOT P_ARGS_SOURCE_DIR)
|
||||||
ExternalProject_Add_Step(dep_${projectname} free_download_space
|
ExternalProject_Add_Step(dep_${projectname} free_download_space
|
||||||
DEPENDEES download # do after download
|
DEPENDEES download # do after download
|
||||||
COMMENT "Freeing Space: Removing source archive"
|
COMMENT "Freeing Space: Removing source archive"
|
||||||
WORKING_DIRECTORY ${DEP_DOWNLOAD_DIR}
|
WORKING_DIRECTORY ${DEP_DOWNLOAD_DIR}
|
||||||
COMMAND ${CMAKE_COMMAND} -E rm -r ${projectname}
|
COMMAND ${CMAKE_COMMAND} -E rm -rf ${projectname}
|
||||||
)
|
)
|
||||||
|
endif ()
|
||||||
ExternalProject_Add_Step(dep_${projectname} free_build_space
|
ExternalProject_Add_Step(dep_${projectname} free_build_space
|
||||||
DEPENDEES install # do after install
|
DEPENDEES install # do after install
|
||||||
COMMENT "Freeing Space: Removing source and build files"
|
COMMENT "Freeing Space: Removing source and build files"
|
||||||
@@ -268,6 +276,7 @@ else()
|
|||||||
EXCLUDE_FROM_ALL ON
|
EXCLUDE_FROM_ALL ON
|
||||||
INSTALL_DIR ${DESTDIR}
|
INSTALL_DIR ${DESTDIR}
|
||||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname}
|
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/${projectname}
|
||||||
|
${_source_dir_arg}
|
||||||
${_gen}
|
${_gen}
|
||||||
CMAKE_ARGS
|
CMAKE_ARGS
|
||||||
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||||
@@ -396,10 +405,6 @@ include(libnoise/libnoise.cmake)
|
|||||||
|
|
||||||
include(Draco/Draco.cmake)
|
include(Draco/Draco.cmake)
|
||||||
|
|
||||||
include(FFMPEG/FFMPEG.cmake)
|
|
||||||
include(Assimp/Assimp.cmake)
|
|
||||||
|
|
||||||
|
|
||||||
# I *think* 1.1 is used for *just* md5 hashing?
|
# I *think* 1.1 is used for *just* md5 hashing?
|
||||||
# 3.1 has everything in the right place, but the md5 funcs used are deprecated
|
# 3.1 has everything in the right place, but the md5 funcs used are deprecated
|
||||||
# a grep across the repo shows it is used for other things
|
# a grep across the repo shows it is used for other things
|
||||||
@@ -410,6 +415,12 @@ if(NOT OPENSSL_FOUND)
|
|||||||
set(OPENSSL_PKG dep_OpenSSL)
|
set(OPENSSL_PKG dep_OpenSSL)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
include(FFMPEG/FFMPEG.cmake)
|
||||||
|
include(Assimp/Assimp.cmake)
|
||||||
|
|
||||||
|
include(DataChannel/DataChannel.cmake)
|
||||||
|
set(DATACHANNEL_PKG dep_DataChannel)
|
||||||
|
|
||||||
# we don't want to load a "wrong" openssl when loading curl
|
# we don't want to load a "wrong" openssl when loading curl
|
||||||
# so, just don't even bother
|
# so, just don't even bother
|
||||||
# ...i think this is how it works? change if wrong
|
# ...i think this is how it works? change if wrong
|
||||||
@@ -483,6 +494,7 @@ set(_dep_list
|
|||||||
dep_wxInspector
|
dep_wxInspector
|
||||||
dep_FFMPEG
|
dep_FFMPEG
|
||||||
dep_Assimp
|
dep_Assimp
|
||||||
|
${DATACHANNEL_PKG}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (MSVC)
|
if (MSVC)
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# libdatachannel is the native ICE/DTLS/SCTP implementation used by the
|
||||||
|
# GUI WebRTC camera controller. Keep the source revision fixed: the signaling
|
||||||
|
# protocol is evolving independently of this transport dependency.
|
||||||
|
#
|
||||||
|
# It vendors plog, usrsctp and libjuice as git submodules, which a plain
|
||||||
|
# GitHub tag tarball does not include. The flatpak sandbox has no network
|
||||||
|
# access during the build, so there the manifest itself clones the repo
|
||||||
|
# (submodules and all) into the dependency download directory before the
|
||||||
|
# sandbox closes. ExternalProject_Add is pointed at that existing checkout
|
||||||
|
# instead of being given its own network-dependent download method.
|
||||||
|
if (FLATPAK)
|
||||||
|
set(_datachannel_source
|
||||||
|
SOURCE_DIR ${DEP_DOWNLOAD_DIR}/DataChannel
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
set(_datachannel_source
|
||||||
|
GIT_REPOSITORY https://github.com/paullouisageneau/libdatachannel.git
|
||||||
|
GIT_TAG v0.24.5
|
||||||
|
GIT_SHALLOW ON
|
||||||
|
GIT_SUBMODULES_RECURSE ON
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
orcaslicer_add_cmake_project(DataChannel
|
||||||
|
DEPENDS ${OPENSSL_PKG}
|
||||||
|
CMAKE_ARGS
|
||||||
|
-DNO_EXAMPLES=ON
|
||||||
|
-DNO_TESTS=ON
|
||||||
|
-DNO_WEBSOCKET=ON
|
||||||
|
-DNO_MEDIA=ON
|
||||||
|
-DUSE_NICE=OFF
|
||||||
|
-DUSE_SYSTEM_JUICE=OFF
|
||||||
|
-DUSE_SYSTEM_USRSCTP=OFF
|
||||||
|
-DOPENSSL_ROOT_DIR:PATH=${DESTDIR}
|
||||||
|
-DOPENSSL_USE_STATIC_LIBS=ON
|
||||||
|
${_datachannel_source}
|
||||||
|
)
|
||||||
@@ -1,14 +1,26 @@
|
|||||||
set(_conf_cmd ./configure)
|
set(_conf_cmd ./configure)
|
||||||
|
|
||||||
|
set(_ffmpeg_depends)
|
||||||
|
set(_ffmpeg_configure_command ${_conf_cmd})
|
||||||
|
if (TARGET dep_OpenSSL)
|
||||||
|
set(_ffmpeg_depends DEPENDS dep_OpenSSL)
|
||||||
|
set(_ffmpeg_configure_command
|
||||||
|
${CMAKE_COMMAND} -E env
|
||||||
|
"PKG_CONFIG_PATH=${DESTDIR}/lib/pkgconfig:$ENV{PKG_CONFIG_PATH}"
|
||||||
|
${_conf_cmd}
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (MSVC)
|
if (MSVC)
|
||||||
set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG")
|
set(_source_dir "${CMAKE_BINARY_DIR}/dep_FFMPEG-prefix/src/dep_FFMPEG")
|
||||||
|
|
||||||
set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-winarm64-orca-shared-7.0.zip")
|
set(PREBUILD_URL_arm64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-09-18-16-50/ffmpeg-n7.0.3-33-g887d4b4919-winarm64-orca-shared-7.0.zip")
|
||||||
set(PREBUILD_HASH_arm64 "12f4140279f2f8469885e1b5b2e8be9d788882914c21523cacd56989f3548054")
|
set(PREBUILD_HASH_arm64 "da480cbb39680056de824c57ec4dc3bd577b479ebbc310ff1f9dc55cf014b4c1")
|
||||||
set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-07-17-14-28/ffmpeg-n7.0.3-31-g9b6ffd74b5-win64-orca-shared-7.0.zip")
|
set(PREBUILD_URL_x64 "https://github.com/Noisyfox/FFmpeg-Builds-Orca/releases/download/autobuild-2026-09-18-16-50/ffmpeg-n7.0.3-33-g887d4b4919-win64-orca-shared-7.0.zip")
|
||||||
set(PREBUILD_HASH_x64 "e65916020ddb9ef84b2666dfbcbfc9b1d67f69d15b4a66db53754637bf2d498c")
|
set(PREBUILD_HASH_x64 "85da19daf198f5548259d8aabb349db84997a3f6e6886d8d7764114add9c6dae")
|
||||||
|
|
||||||
ExternalProject_Add(dep_FFMPEG
|
ExternalProject_Add(dep_FFMPEG
|
||||||
|
${_ffmpeg_depends}
|
||||||
URL ${PREBUILD_URL_${DEPS_ARCH}}
|
URL ${PREBUILD_URL_${DEPS_ARCH}}
|
||||||
URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}}
|
URL_HASH SHA256=${PREBUILD_HASH_${DEPS_ARCH}}
|
||||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
|
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
|
||||||
@@ -21,6 +33,8 @@ if (MSVC)
|
|||||||
)
|
)
|
||||||
|
|
||||||
else ()
|
else ()
|
||||||
|
set(_openssl_cmd --enable-openssl)
|
||||||
|
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
set(_minos_cmd
|
set(_minos_cmd
|
||||||
"--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
|
"--extra-cflags=-mmacosx-version-min=${DEP_OSX_TARGET}"
|
||||||
@@ -52,10 +66,11 @@ else ()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
ExternalProject_Add(dep_FFMPEG
|
ExternalProject_Add(dep_FFMPEG
|
||||||
|
${_ffmpeg_depends}
|
||||||
URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz
|
URL https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.0.3.tar.gz
|
||||||
URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC
|
URL_HASH SHA256=DEEDCABE339165214A3637DF4C86A507AEF0D793CF8774FF68735F4737E8DDBC
|
||||||
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
|
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/FFMPEG
|
||||||
CONFIGURE_COMMAND ${_conf_cmd}
|
CONFIGURE_COMMAND ${_ffmpeg_configure_command}
|
||||||
${_cross_cmd}
|
${_cross_cmd}
|
||||||
${_pic_cmd}
|
${_pic_cmd}
|
||||||
${_arch_cmd}
|
${_arch_cmd}
|
||||||
@@ -63,20 +78,21 @@ else ()
|
|||||||
"--prefix=${DESTDIR}"
|
"--prefix=${DESTDIR}"
|
||||||
${_link_cmd}
|
${_link_cmd}
|
||||||
${_minos_cmd}
|
${_minos_cmd}
|
||||||
|
${_openssl_cmd}
|
||||||
--disable-doc
|
--disable-doc
|
||||||
--enable-small
|
--enable-small
|
||||||
--disable-outdevs
|
--disable-outdevs
|
||||||
--disable-filters
|
--disable-filters
|
||||||
--enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test*
|
--enable-filter=*null*,afade,*fifo,*format,*resample,aeval,allrgb,allyuv,atempo,pan,*bars,color,*key,crop,draw*,eq*,framerate,*_qsv,*_vaapi,*v4l2*,hw*,scale,volume,test*
|
||||||
--disable-protocols
|
--disable-protocols
|
||||||
--enable-protocol=file,fd,pipe,rtp,udp
|
--enable-protocol=file,fd,pipe,http,https,rtp,tcp,udp
|
||||||
--disable-muxers
|
--disable-muxers
|
||||||
--enable-muxer=rtp
|
--enable-muxer=rtp
|
||||||
--disable-encoders
|
--disable-encoders
|
||||||
--disable-decoders
|
--disable-decoders
|
||||||
--enable-decoder=*aac*,h264*,mp3*,mjpeg,rv*
|
--enable-decoder=*aac*,h264*,mp3*,mjpeg,rv*
|
||||||
--disable-demuxers
|
--disable-demuxers
|
||||||
--enable-demuxer=h264,mp3,mov
|
--enable-demuxer=h264,mp3,mov,mpjpeg,rtsp,sdp
|
||||||
--disable-zlib
|
--disable-zlib
|
||||||
--disable-avdevice
|
--disable-avdevice
|
||||||
BUILD_IN_SOURCE ON
|
BUILD_IN_SOURCE ON
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ endif()
|
|||||||
# On macOS/Linux OCCT links statically, so an unreferenced toolkit costs build time and no
|
# On macOS/Linux OCCT links statically, so an unreferenced toolkit costs build time and no
|
||||||
# shipped bytes. The Windows figure is a real DLL cost and has NOT been measured -- an
|
# shipped bytes. The Windows figure is a real DLL cost and has NOT been measured -- an
|
||||||
# earlier "3.77 MiB, Windows only" note here covered only two of the three toolkits and is
|
# earlier "3.77 MiB, Windows only" note here covered only two of the three toolkits and is
|
||||||
# not a number to quote. See docs/cad_dependency_weight.md.
|
# not a number to quote. See docs/HLSD/design-tab.md.
|
||||||
|
|
||||||
if (IN_GIT_REPO)
|
if (IN_GIT_REPO)
|
||||||
set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT)
|
set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT)
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
# Orca-CAD vs Onshape — capability gap analysis
|
|
||||||
|
|
||||||
Generated 2026-07-22 by enumerating the source, not from recollection:
|
|
||||||
`CadFeatureType` and `add_*` in `src/libslic3r/CAD/CadDocument.hpp`, `Tool` in
|
|
||||||
`src/slic3r/GUI/CAD/DesignPanel.hpp`, `Mode` in `src/slic3r/GUI/CAD/DesignSketchTool.hpp`,
|
|
||||||
`SketchConstraintType` + `SketchEntity::Type` in `src/libslic3r/CAD/SketchEngine.hpp`,
|
|
||||||
and the JSON-RPC dispatch in `src/slic3r/GUI/CAD/McpControl.cpp`.
|
|
||||||
|
|
||||||
**Scope note.** Onshape is a cloud PLM platform; Orca is a Design tab inside a
|
|
||||||
slicer. A large share of Onshape's surface (release management, branching, real-time
|
|
||||||
collaboration, FEA, rendering, PDM) is out of scope by construction and is listed
|
|
||||||
separately at the bottom rather than counted as a "missing tool".
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. What Orca already has
|
|
||||||
|
|
||||||
### 2D sketcher — near parity with Onshape
|
|
||||||
This is the strongest area. Very little is missing.
|
|
||||||
|
|
||||||
| Category | Orca |
|
|
||||||
|---|---|
|
|
||||||
| Entities | Line, Polyline, Arc (3-point / tangent / center), Circle (center / 2-point / 3-point), Point, Ellipse, Elliptical arc, B-spline |
|
|
||||||
| Shapes | Rectangle (corner / center / oblique / rounded), Slot, Arc-slot, Polygon |
|
|
||||||
| Edit ops | Fillet, Chamfer, Offset, Mirror, Trim, Extend |
|
|
||||||
| Transforms | Move, Rotate, Scale, Linear array, Polar array |
|
|
||||||
| Constraints (19) | Fix, Coincident, Horizontal, Vertical, Distance, LockX, LockY, EqualLength, Parallel, Perpendicular, Concentric, Tangent, Midpoint, Symmetric, Angle, Radius, Diameter, PointOnLine, PointOnObject |
|
|
||||||
| Dimensions | Length, Diameter, Radius, Angle, Distance, Distance-to-line |
|
|
||||||
|
|
||||||
Solver: vendored SolveSpace (`libslvs`, GPL-3.0) — the same solver lineage as a
|
|
||||||
commercial-grade sketcher.
|
|
||||||
|
|
||||||
### Part features
|
|
||||||
|
|
||||||
| Present | Notes |
|
|
||||||
|---|---|
|
|
||||||
| Extrude | + up-to-face / up-to-point, taper, flip |
|
|
||||||
| Revolve | angle-arc gizmo |
|
|
||||||
| Sweep | along a path |
|
|
||||||
| Loft | multi-profile |
|
|
||||||
| Fillet / Chamfer | edge-level |
|
|
||||||
| Draft | face taper |
|
|
||||||
| Shell | wall thickness + open face |
|
|
||||||
| Hole / Thread | face-aware placement |
|
|
||||||
| Pattern | linear + circular |
|
|
||||||
| Boolean | New / Add / Cut / Intersect, with face-mating |
|
|
||||||
| Cut | plane-based, signed offset |
|
|
||||||
| Datum plane | offset / 2-face / 2-edge derived |
|
|
||||||
| Import | STEP (B-rep) + mesh→B-rep (native mesh2step port) |
|
|
||||||
| Export | STEP (native B-rep, not tessellated) |
|
|
||||||
| Multi-body | + per-body colour |
|
|
||||||
| Section view | with flip |
|
|
||||||
| Undo/redo | full feature-tree recompute |
|
|
||||||
| 3MF persistence | parametric recipe survives save/load |
|
|
||||||
|
|
||||||
### Automation
|
|
||||||
9 MCP JSON-RPC methods: `describe_tools`, `describe_scene`, `query_topology`,
|
|
||||||
`measure`, `slice_body`, `import_step`, `import_mesh`, `validate_against`, plus
|
|
||||||
build actions `extrude`, `revolve`, `fillet`, `chamfer`, `hole`, `boolean`, `pattern`.
|
|
||||||
Onshape's equivalent is its REST API + FeatureScript.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Missing tools — ranked by impact
|
|
||||||
|
|
||||||
### Tier 1 — structural absences (whole subsystems)
|
|
||||||
|
|
||||||
**1. Assemblies and mates.** Entirely absent. No assembly document, no mate
|
|
||||||
connectors, no fastened / revolute / slider / cylindrical / planar / ball / pin-slot
|
|
||||||
mates, no assembly patterns, no interference detection, no exploded views.
|
|
||||||
`bool_target_face` / `bool_tool_face` do face-to-face *mating* for a boolean, which
|
|
||||||
is geometric alignment, not a kinematic joint.
|
|
||||||
*Impact:* multi-part products cannot be positioned or validated as a mechanism.
|
|
||||||
*Note:* an MCP-side `align_instance_to_face` / `create_*_mate` vocabulary already
|
|
||||||
exists on the Onshape bridge in this workspace, so the target semantics are known.
|
|
||||||
|
|
||||||
**2. Drawings / 2D documentation.** Absent. No drawing sheets, dimensioned views,
|
|
||||||
section/detail views, GD&T, title blocks, or BOM.
|
|
||||||
*Impact:* nothing manufacturable-by-a-third-party leaves the tool. For 3D printing
|
|
||||||
this matters less than for machining, which is the honest reason it is Tier 1 by
|
|
||||||
CAD convention but arguably Tier 3 for this product.
|
|
||||||
|
|
||||||
**3. Variables, equations, configurations.** Absent — no `add_variable`, no
|
|
||||||
expression evaluation, no configuration table. Every dimension is a literal double.
|
|
||||||
*Impact:* this is the biggest *parametric* gap. "Make this bracket for an M4 vs M5
|
|
||||||
bolt" requires re-editing every dependent feature by hand. Onshape's Variable
|
|
||||||
Studio + configurations are a core differentiator, and this is the cheapest Tier 1
|
|
||||||
item to close for the size of the payoff.
|
|
||||||
|
|
||||||
**4. Surface modelling.** Absent. No surface extrude/revolve/loft/sweep, no fill,
|
|
||||||
knit, trim/extend surface, offset surface, or thicken. Orca is solid-only.
|
|
||||||
*Impact:* organic/complex shapes and repair of imported junk geometry are impossible.
|
|
||||||
OCCT already provides all of it (`TKOffset`, `TKBRep`), so the kernel is not the
|
|
||||||
blocker — only UI and feature plumbing.
|
|
||||||
|
|
||||||
**5. Sheet metal.** Absent. No flange, bend, tab, relief, or flat-pattern unfold.
|
|
||||||
*Impact:* arguably out of scope for an FDM slicer; listed for completeness.
|
|
||||||
|
|
||||||
### Tier 2 — individual features with clear demand
|
|
||||||
|
|
||||||
| Missing | Why it matters | Cheap? |
|
|
||||||
|---|---|---|
|
|
||||||
| **Mirror body** (part-level) | Sketch mirror exists; mirroring a *solid* about a plane does not. Extremely common. | Yes — OCCT `gp_Trsf` mirror + fuse |
|
|
||||||
| **Helix / spiral curve** | No helix ⇒ no springs, no custom threads, no spiral vase geometry. Sweep exists but has no helical path to sweep along. | Yes |
|
|
||||||
| **Move / rotate body as a real feature** | `m_body_xform` exists but is **display-only** (memory #1655) — it never enters the B-rep. Export/boolean see the original position. | Medium |
|
|
||||||
| **Split body** | Cut removes material; splitting one body into two independently-usable bodies is absent. Very relevant for print-in-parts. | Medium |
|
|
||||||
| **Thicken** | Solid from a surface/face offset. | Needs surfaces |
|
|
||||||
| **Rib** | Standard structural feature. | Medium |
|
|
||||||
| **Delete face / move face / replace face** | Direct/dumb-solid editing — the main tool for fixing imported STEP. Given Orca imports STEP *and* meshes, its absence is felt. | Medium |
|
|
||||||
| **Datum axis, coordinate system** | Only datum *planes* exist. Axes are needed for revolve/pattern references. | Yes |
|
|
||||||
| **Mass properties** | `GeometryEngine` computes a volume internally, but there is no volume/mass/COM/inertia readout. For print cost/time estimation this is nearly free to expose. | Yes — trivial |
|
|
||||||
| **Measure tool in the GUI** | `measure` exists over MCP but there is no interactive measure in the UI. | Yes |
|
|
||||||
| **Hole standards library** | Hole exists, but no counterbore/countersink/tapped standards (ISO/ANSI) with callouts. | Medium |
|
|
||||||
| **Project / convert edges into a sketch** | Cannot reference existing solid edges as sketch geometry ("Use" in SolidWorks). A significant sketcher gap given everything else is present. | Medium |
|
|
||||||
| **Construction geometry** | Could not confirm a construction/reference-line flag on sketch entities. | Yes if absent |
|
|
||||||
| **Curve tools** | Projected curve, bridging curve, composite curve, 3D fit spline. | Medium |
|
|
||||||
| **Pattern on curve / pattern faces** | Pattern is linear + circular of whole bodies only; no curve-driven pattern, no feature/face pattern. | Medium |
|
|
||||||
| **Wrap / emboss** | Text or sketch wrapped onto a curved face. | Hard |
|
|
||||||
| **Enclose** | Solid from bounded void regions. | Medium |
|
|
||||||
|
|
||||||
### Tier 3 — platform capabilities (out of scope by construction)
|
|
||||||
|
|
||||||
Version control with branching/merging, release management, real-time multi-user
|
|
||||||
collaboration, cloud PDM, FeatureScript custom-feature authoring, simulation/FEA,
|
|
||||||
photorealistic rendering, app store/integrations. These are Onshape-the-platform,
|
|
||||||
not Onshape-the-modeller. Not defects in Orca.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Recommended priority
|
|
||||||
|
|
||||||
If the goal is "credible parametric CAD inside a slicer", the ordering that buys
|
|
||||||
the most capability per unit of work:
|
|
||||||
|
|
||||||
1. **Variables + expressions** — unlocks genuine parametric reuse; no new kernel work.
|
|
||||||
2. **Mass properties + GUI measure** — nearly free, immediately useful for printing.
|
|
||||||
3. **Mirror body, datum axis, helix** — small, self-contained, high-frequency features.
|
|
||||||
4. **Promote move/rotate body from display-only to a real B-rep feature** — closes a
|
|
||||||
correctness gap, not just a missing tool (exports currently disagree with the view).
|
|
||||||
5. **Split body** — high value for print-in-parts workflows.
|
|
||||||
6. **Project edges into sketch** — the sketcher's most conspicuous hole.
|
|
||||||
7. **Surface modelling** — large, but OCCT already ships the algorithms.
|
|
||||||
8. **Assemblies** — largest effort; only worth it if Orca targets multi-part products.
|
|
||||||
|
|
||||||
Deliberately last: drawings and sheet metal — high cost, low relevance to an
|
|
||||||
FDM-oriented tool.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Honest summary
|
|
||||||
|
|
||||||
Orca's **sketcher is at or near Onshape parity**, and its **solid feature set
|
|
||||||
covers the mainstream modelling path** (sketch → extrude/revolve/sweep/loft →
|
|
||||||
dress-up → boolean/pattern). What is absent is *breadth*: assemblies, surfaces,
|
|
||||||
sheet metal, drawings, and — most importantly for a tool calling itself parametric —
|
|
||||||
**variables and configurations**.
|
|
||||||
|
|
||||||
The single most defensible criticism is #3: without variables, the feature tree is
|
|
||||||
parametric in *structure* but not in *value*, so the promise of "change one number
|
|
||||||
and the model updates" is only half delivered.
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# Dependency weight of the Design/CAD subsystem
|
|
||||||
|
|
||||||
What the Design tab actually costs a maintainer who merges it. Written to be checkable:
|
|
||||||
every number below is reproducible with the command that produced it, and the places where
|
|
||||||
a number is still missing say so instead of guessing.
|
|
||||||
|
|
||||||
Measured on Linux x86_64, OCCT V7_6_0, in the `snapmaker-deps` build image.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
| | Cost |
|
|
||||||
|---|---|
|
|
||||||
| New third-party dependencies | **none** |
|
|
||||||
| OCCT build flag | `BUILD_MODULE_ModelingAlgorithms=ON` |
|
|
||||||
| Extra OCCT toolkits *built* | 3 (TKFillet, TKOffset, TKFeat) |
|
|
||||||
| Extra OCCT toolkits *linked* | 2 (TKFillet, TKOffset) |
|
|
||||||
| Vendored code | `src/libslic3r/slvs`, 9,339 lines, 380 KiB, GPLv3 |
|
|
||||||
| Own object code | 6.79 MiB unstripped `.o` (7.13 MiB with the solver) |
|
|
||||||
|
|
||||||
OCCT is **already** an upstream dependency — Orca uses it for STEP import. The Design tab
|
|
||||||
does not add a library; it turns on one more OCCT module.
|
|
||||||
|
|
||||||
## The OCCT module flag
|
|
||||||
|
|
||||||
`deps/OCCT/OCCT.cmake` gates the module on `SLIC3R_CAD`:
|
|
||||||
|
|
||||||
```cmake
|
|
||||||
-DBUILD_MODULE_ModelingAlgorithms=${SLIC3R_CAD} # was hard-coded OFF
|
|
||||||
```
|
|
||||||
|
|
||||||
With `SLIC3R_CAD=OFF` the deps prefix matches upstream exactly.
|
|
||||||
|
|
||||||
`ModelingAlgorithms` contains 12 toolkits, but **most were already being built**, because
|
|
||||||
`DataExchange` — the STEP path upstream already ships — depends on them. The honest delta is
|
|
||||||
only the toolkits that DataExchange's dependency closure does *not* reach:
|
|
||||||
|
|
||||||
```
|
|
||||||
ModelingAlgorithms = TKGeomAlgo TKTopAlgo TKPrim TKBO TKBool TKHLR
|
|
||||||
TKFillet TKOffset TKFeat TKMesh TKXMesh TKShHealing
|
|
||||||
|
|
||||||
already required by DataExchange: TKBO TKBool TKGeomAlgo TKHLR TKMesh
|
|
||||||
TKPrim TKShHealing TKTopAlgo
|
|
||||||
true delta: TKFeat TKFillet TKOffset TKXMesh
|
|
||||||
```
|
|
||||||
|
|
||||||
Reproduce by walking `adm/MODULES` and each toolkit's `src/<TK>/EXTERNLIB` in the OCCT
|
|
||||||
source tree.
|
|
||||||
|
|
||||||
### Sizes of the delta toolkits
|
|
||||||
|
|
||||||
Static archives in the deps prefix. These are *build artifacts*, not shipped bytes — a
|
|
||||||
static link pulls in only the objects it references:
|
|
||||||
|
|
||||||
| Toolkit | Archive | Referenced by the Design tab? |
|
|
||||||
|---|---|---|
|
|
||||||
| TKFillet | 7.40 MiB | yes — `BRepFilletAPI` |
|
|
||||||
| TKOffset | 5.38 MiB | yes — `BRepOffsetAPI`, `BRepOffset_` |
|
|
||||||
| TKFeat | 4.42 MiB | **no** |
|
|
||||||
| TKXMesh | — | not produced at all |
|
|
||||||
|
|
||||||
TKFeat is worth calling out: nothing in the Design tab references it, and it is absent from
|
|
||||||
the `TKFillet`/`TKOffset` dependency closure, so it is built for nothing. OCCT's module flag
|
|
||||||
is all-or-nothing per module, which is why it comes along. It costs build time and zero
|
|
||||||
shipped bytes on any platform that links OCCT statically.
|
|
||||||
|
|
||||||
**A correction to the record.** The comment in `deps/OCCT/OCCT.cmake` and the earlier
|
|
||||||
summary both said the delta was "TKFillet + TKOffset — 3.77 MiB, Windows only". The toolkit
|
|
||||||
list was incomplete: TKFeat is built too. The 3.77 MiB figure covers 2 of the 3 built
|
|
||||||
toolkits and has not been re-derived here — see the gap below.
|
|
||||||
|
|
||||||
## What is not measured yet
|
|
||||||
|
|
||||||
Two numbers a maintainer may reasonably ask for are **not** in this document, because
|
|
||||||
producing them honestly needs a build this machine cannot do:
|
|
||||||
|
|
||||||
1. **Windows DLL delta.** OCCT builds shared on Windows, so the shipped cost there is real
|
|
||||||
DLL bytes rather than linker-selected objects. That needs a Windows build to size —
|
|
||||||
tracked as the cross-platform build proof (`gix`).
|
|
||||||
2. **Clean-build time delta.** Measuring it means building the deps prefix twice, with the
|
|
||||||
flag ON and OFF, on the same machine. The incremental figures from day-to-day work do not
|
|
||||||
answer the question and are not offered as if they did.
|
|
||||||
|
|
||||||
Do not quote a number for either until it has been measured.
|
|
||||||
|
|
||||||
## Vendored solver
|
|
||||||
|
|
||||||
`src/libslic3r/slvs` — the 2D sketch constraint solver extracted from SolveSpace.
|
|
||||||
|
|
||||||
- 19 files: 8 `.cpp`, 11 `.h`, plus `LICENSE`
|
|
||||||
- 9,339 lines, 380 KiB of source, 0.34 MiB of object code
|
|
||||||
- **GPLv3**, `LICENSE` preserved verbatim in the vendored directory
|
|
||||||
|
|
||||||
The fork is **AGPLv3**. GPLv3 code combines into an AGPLv3 work without difficulty: AGPLv3
|
|
||||||
§13 provides explicit compatibility in that direction. No licence question to resolve.
|
|
||||||
|
|
||||||
It is live code, not a carried corpse — `SketchSolver.cpp` is its only consumer and drives
|
|
||||||
every sketch constraint in the Design tab.
|
|
||||||
|
|
||||||
## Own code
|
|
||||||
|
|
||||||
Object sizes from the release build (unstripped, so these include debug information and
|
|
||||||
overstate the shipped contribution):
|
|
||||||
|
|
||||||
| Object | Size |
|
|
||||||
|---|---|
|
|
||||||
| DesignPanel.o | 2.22 MiB |
|
|
||||||
| McpControl.o | 1.69 MiB |
|
|
||||||
| DesignSketchTool.o | 0.88 MiB |
|
|
||||||
| CadDocument.o | 0.76 MiB |
|
|
||||||
| SketchEngine.o | 0.40 MiB |
|
|
||||||
| DesignCanvas.o | 0.37 MiB |
|
|
||||||
| GeometryEngine.o | 0.32 MiB |
|
|
||||||
| SketchSolver.o | 0.15 MiB |
|
|
||||||
| slvs (all objects) | 0.34 MiB |
|
|
||||||
| **total** | **7.13 MiB** |
|
|
||||||
|
|
||||||
For scale, the linked binary is 137.1 MiB.
|
|
||||||
|
|
||||||
## Reproducing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# toolkit membership and dependency closure
|
|
||||||
R=<occt-source>
|
|
||||||
cat $R/adm/MODULES # module -> toolkits
|
|
||||||
cat $R/src/<TK>/EXTERNLIB # toolkit -> its dependencies
|
|
||||||
|
|
||||||
# archive sizes
|
|
||||||
ls -l <deps-prefix>/lib/libTK{Fillet,Offset,Feat}.a
|
|
||||||
|
|
||||||
# what the Design tab actually references
|
|
||||||
grep -rE 'BRepFilletAPI|BRepOffsetAPI|BRepOffset_|BRepFeat' src/libslic3r/
|
|
||||||
|
|
||||||
# vendored solver
|
|
||||||
wc -l src/libslic3r/slvs/*.cpp src/libslic3r/slvs/**/*.h
|
|
||||||
head -3 src/libslic3r/slvs/LICENSE
|
|
||||||
```
|
|
||||||
@@ -1,704 +0,0 @@
|
|||||||
# Orca-CAD — UX guidelines and design charter
|
|
||||||
|
|
||||||
Status: proposed, v1. Owner: design working group. Applies to the Design tab —
|
|
||||||
the parametric CAD environment inside OrcaSlicer.
|
|
||||||
|
|
||||||
This document is a **review instrument**, not an essay. Sections 3–9 are written
|
|
||||||
so that a reviewer can hold a pull request against them and get a yes or a no.
|
|
||||||
If a rule here cannot be failed, it is badly written and should be rewritten.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Why this exists
|
|
||||||
|
|
||||||
A CAD tool acquires its interface by accretion. Every feature arrives needing
|
|
||||||
"just one more field", the side panel is the cheapest place to put it, and after
|
|
||||||
forty features the product is FreeCAD: complete, respected, and abandoned by
|
|
||||||
almost everyone who opens it once. That end state is not a failure of any single
|
|
||||||
decision. It is the sum of forty locally reasonable ones taken without a written
|
|
||||||
rule to violate.
|
|
||||||
|
|
||||||
So we write the rule down first, and we make additions argue against it.
|
|
||||||
|
|
||||||
## 2. Product thesis
|
|
||||||
|
|
||||||
**Orca-CAD is a modelling space for people who want a part, inside the tool that
|
|
||||||
prints it.**
|
|
||||||
|
|
||||||
Three audiences, one interface:
|
|
||||||
|
|
||||||
- **The fourteen-year-old on a school laptop.** Free software, on the machine
|
|
||||||
they already have, with no account, no subscription, no licence and no
|
|
||||||
tutorial. They open the tab because they want a bracket for a bike light, and
|
|
||||||
an hour later it is printing. This is not the charity case at the bottom of
|
|
||||||
the list — it is the reason the project is worth doing. A CAD tool that only
|
|
||||||
the equipped can run is a tool for people who were already going to design
|
|
||||||
something; this one has to be a creative instrument in the hands of someone
|
|
||||||
who did not yet know they could make things. Everything in §6.1 exists to
|
|
||||||
keep that door open, and nothing gets to close it for the convenience of the
|
|
||||||
other two audiences.
|
|
||||||
- **The maker** who has an idea and a printer, and who has bounced off FreeCAD.
|
|
||||||
They should be modelling something real within ten minutes of first opening
|
|
||||||
the tab, without a tutorial, without knowing the word "constraint".
|
|
||||||
- **The mechanical designer** who needs assemblies, mates, exploded views,
|
|
||||||
variables, and a feature history they can edit six months later. They should
|
|
||||||
not have to leave for SolidWorks the moment the work gets serious.
|
|
||||||
|
|
||||||
The order matters. When a decision helps one audience and hurts another, the
|
|
||||||
earlier one wins unless there is a written argument for why not.
|
|
||||||
|
|
||||||
The reference for *how it feels* is Shapr3D: direct, gestural, quiet, almost no
|
|
||||||
chrome, depth revealed by what you touch rather than by what is on screen. The
|
|
||||||
anti-references are Blender (a modal keyboard language you must learn before the
|
|
||||||
first success) and FreeCAD (a workbench-and-dialog architecture where the
|
|
||||||
geometry is a preview of a form you fill in elsewhere).
|
|
||||||
|
|
||||||
We are not cloning Shapr3D's feature set. We are adopting its *interaction
|
|
||||||
economy*: the smallest number of visible controls that still makes an expert
|
|
||||||
fast.
|
|
||||||
|
|
||||||
**And one thing neither reference has:** Orca-CAD lives inside a slicer. The
|
|
||||||
plate, the nozzle, the material and the print constraints are known to the
|
|
||||||
application at design time. Designing for print is not a plugin here, it is the
|
|
||||||
home advantage. Where a rule below trades generality for print-awareness, it
|
|
||||||
trades in favour of print-awareness.
|
|
||||||
|
|
||||||
## 3. The laws
|
|
||||||
|
|
||||||
Non-negotiable. A change that breaks one of these does not get merged on the
|
|
||||||
grounds that it was easier, that the alternative is more work, or that another
|
|
||||||
CAD does it that way. Each law carries a test — the question a reviewer asks.
|
|
||||||
|
|
||||||
### L1 — Geometry first: you point, then you act
|
|
||||||
|
|
||||||
Controls live **on the geometry**: handles, arrows, points, small circles and
|
|
||||||
boxes, with an inline label tab for typed values. Not in a side panel of combos
|
|
||||||
and spin fields.
|
|
||||||
|
|
||||||
The canonical gesture: **select a face or plane in the viewport, then click the
|
|
||||||
sketch tool.** Never: click the sketch tool, then choose a plane from a list.
|
|
||||||
The tool consumes what you pointed at — and, better still, the thing you pointed
|
|
||||||
at offers the tool itself (§4).
|
|
||||||
|
|
||||||
> **Test.** Can the operation be performed start to finish without the pointer
|
|
||||||
> leaving the viewport, except to press the tool itself? If a control had to be
|
|
||||||
> added to a panel to make it work, the design is not finished.
|
|
||||||
|
|
||||||
This is the law the others serve. It was stated after two proposals in a row
|
|
||||||
reached for a dropdown, and the failure mode it names is real and recurrent: a
|
|
||||||
fix that "adds a row to the plane combo" is the side-panel pattern wearing a
|
|
||||||
different hat.
|
|
||||||
|
|
||||||
### L2 — Everything draggable is typable, and everything typable is draggable
|
|
||||||
|
|
||||||
Any value produced by direct manipulation (a fillet radius, an extrude depth, a
|
|
||||||
pattern spacing, a plane offset) shows a live label on the geometry, and that
|
|
||||||
label is an editable field. Any value entered numerically has a corresponding
|
|
||||||
handle in the viewport.
|
|
||||||
|
|
||||||
Dragging is for finding the answer. Typing is for committing to it. A tool that
|
|
||||||
offers only one of the two is half a tool.
|
|
||||||
|
|
||||||
> **Test.** Point at the number the tool produces. Can you drag it? Can you
|
|
||||||
> click it and type? Both must be yes.
|
|
||||||
|
|
||||||
### L3 — Noun then verb, always the same way round
|
|
||||||
|
|
||||||
Selection precedes action, without exception, across sketch tools, features,
|
|
||||||
dress-up, booleans and mates. There is no tool in the product that is armed
|
|
||||||
first and asks for its input afterwards.
|
|
||||||
|
|
||||||
> **Test.** Does this tool work if the user has already selected the thing they
|
|
||||||
> want it applied to? Does it work *only* that way?
|
|
||||||
|
|
||||||
### L4 — No modal dialog in the modelling loop
|
|
||||||
|
|
||||||
Dialogs belong to document-level actions: open, save, import, export, preferences.
|
|
||||||
Modelling never opens one. A feature that needs three values gets three labels on
|
|
||||||
the geometry, not a form; a feature that needs confirming gets a ghost preview and
|
|
||||||
a confirm/cancel puck in the scene beside it (§4.2) — an object, not a window: the
|
|
||||||
camera still orbits, the values are still editable, nothing is blocked.
|
|
||||||
|
|
||||||
> **Test.** Between starting an operation and seeing its result, does a window
|
|
||||||
> appear that must be dismissed? If yes, redesign.
|
|
||||||
|
|
||||||
### L5 — One click, one visible change
|
|
||||||
|
|
||||||
Every click either changes what is on screen or tells the user why it did not.
|
|
||||||
A click that opens something invisible, arms an invisible state, or requires a
|
|
||||||
second identical click to have any effect is a defect, not a design.
|
|
||||||
|
|
||||||
This law exists because we shipped its violation twice. Sketch-tool family
|
|
||||||
buttons were flyouts whose first click only rendered a pressed state — three
|
|
||||||
separate sessions filed bugs against tools that were working. Solid picking used
|
|
||||||
a click *cycle* (first click selects the body, second refines to the face), so
|
|
||||||
sketching on a face appeared broken to anyone who clicked a face once, the way
|
|
||||||
every human does.
|
|
||||||
|
|
||||||
> **Test.** Perform the gesture exactly once, as a first-time user would. Take a
|
|
||||||
> screenshot. Is the state visibly different, and is the difference the one the
|
|
||||||
> user intended?
|
|
||||||
|
|
||||||
### L6 — The default is the answer four times out of five
|
|
||||||
|
|
||||||
Every option that has a default must have the *common* answer as its default,
|
|
||||||
measured against real parts, not against generality. "New body" as the default
|
|
||||||
result of an extrude is wrong: most extrudes join. Radius as the input for a
|
|
||||||
circle is wrong: drawings give diameter.
|
|
||||||
|
|
||||||
> **Test.** Take ten real parts. In how many is the default correct? Below eight,
|
|
||||||
> change the default or infer it from context.
|
|
||||||
|
|
||||||
### L7 — Errors are caught before the commit, in the user's words
|
|
||||||
|
|
||||||
A self-intersecting profile, a cut that removes no material, a wall thinner than
|
|
||||||
the nozzle: these are reported at the moment they become knowable, on the
|
|
||||||
geometry that is wrong, phrased as what happened and what to do — not as a kernel
|
|
||||||
exception after the fact, and never silently.
|
|
||||||
|
|
||||||
> **Test.** Is the failure detectable before the user commits? Then it must be
|
|
||||||
> reported before the user commits. Read the message aloud: does it name a thing
|
|
||||||
> the user can see and an action they can take?
|
|
||||||
|
|
||||||
### L8 — The camera is the application's job
|
|
||||||
|
|
||||||
Selecting a sketch plane orients the view to it. Committing a feature does not
|
|
||||||
throw the camera away. Zoom-to-fit exists and is one keystroke. The user is never
|
|
||||||
required to fight the view in order to reach the geometry, and orbit is bound to
|
|
||||||
the gesture people actually try.
|
|
||||||
|
|
||||||
> **Test.** Count camera manipulations in a representative modelling session.
|
|
||||||
> Any camera action the application could have performed for the user is a bug.
|
|
||||||
|
|
||||||
### L9 — Accessible by construction, not by retrofit
|
|
||||||
|
|
||||||
The floor, applied to every new interaction (details in §6.2): full keyboard
|
|
||||||
reach, no meaning carried by colour alone, hit targets that survive a shaky hand
|
|
||||||
and a HiDPI screen, legible labels over an arbitrary 3D background, no gesture
|
|
||||||
that depends on timing.
|
|
||||||
|
|
||||||
> **Test.** Drive the whole interaction from the keyboard. Then drive it in
|
|
||||||
> greyscale. Both must work.
|
|
||||||
|
|
||||||
### L10 — Vocabulary from the drawing office
|
|
||||||
|
|
||||||
Names come from the language of people who make parts: fillet, chamfer, boss,
|
|
||||||
rib, counterbore, mate, exploded view. Not from the kernel (no "boolean
|
|
||||||
subtract", no "B-rep"), not from invented product-speak. Where the drawing-office
|
|
||||||
word and the beginner's word differ, use the drawing-office word and make the
|
|
||||||
tooltip teach it — an approachable tool that leaves the user unable to talk to a
|
|
||||||
machinist has failed them.
|
|
||||||
|
|
||||||
> **Test.** Would a shop-floor engineer recognise this word? Would a first-time
|
|
||||||
> user be able to look it up and find a real definition?
|
|
||||||
|
|
||||||
### L11 — The floor is a school laptop, and nothing is behind a door
|
|
||||||
|
|
||||||
The product runs, completely, on a low-end laptop with integrated graphics and a
|
|
||||||
small screen, offline, with no account, no subscription and no feature withheld.
|
|
||||||
No capability in this document is reserved for a paid tier, a cloud service, a
|
|
||||||
plugin, or a machine with a discrete GPU — there is one product and everybody
|
|
||||||
gets all of it.
|
|
||||||
|
|
||||||
> **Test.** On the reference low-end machine (§6.1), at 1366×768, with the
|
|
||||||
> network cable pulled and no account ever created: does this feature work, and
|
|
||||||
> is it usable at an honest frame rate? Any "no" is a defect, not a limitation.
|
|
||||||
|
|
||||||
## 4. Interaction grammar — object-driven
|
|
||||||
|
|
||||||
The rules above compose into one sentence the whole product obeys:
|
|
||||||
|
|
||||||
> **Point at geometry → the geometry offers what can be done to it → choose the
|
|
||||||
> tool → manipulate handles and type exact values → confirm or cancel.**
|
|
||||||
|
|
||||||
The selection does not merely feed the tool. **The selection determines which
|
|
||||||
tools exist.** Pick a planar face and the product shows you the small set of
|
|
||||||
things a planar face can become — sketch on it, extrude it, hole it, shell it,
|
|
||||||
put a datum on it. Pick an edge and that set is fillet, chamfer, and the sketch
|
|
||||||
tools that can use it as a reference. Nothing else is offered, because nothing
|
|
||||||
else is possible.
|
|
||||||
|
|
||||||
This is the single largest thing we can do for a first-time user, and it is
|
|
||||||
worth stating as the reason: a beginner's difficulty is not operating a tool,
|
|
||||||
it is **not knowing which tools apply to what they are looking at**. A palette
|
|
||||||
of sixty icons answers a question they cannot yet ask. A face that offers its
|
|
||||||
own five verbs teaches the model of the product by using it. It also removes an
|
|
||||||
entire class of failure — a tool that silently does nothing because the
|
|
||||||
selection was wrong can no longer be reached.
|
|
||||||
|
|
||||||
### 4.1 The offer, and the one thing that makes it work
|
|
||||||
|
|
||||||
The flow, in full:
|
|
||||||
|
|
||||||
> **left-click the geometry to select it → right-click to open the offer → a
|
|
||||||
> vertical list, always in the same order, each row an icon, a name and its
|
|
||||||
> keyboard shortcut → click.**
|
|
||||||
|
|
||||||
- **Selecting and acting are separate gestures.** Left-click only ever selects,
|
|
||||||
so pointing at things is quiet — nothing pops up while you look around.
|
|
||||||
Right-click on the selection opens the offer, at the pointer, over the
|
|
||||||
geometry it acts on.
|
|
||||||
- **Order is fixed and it is the whole point.** A verb occupies one permanent
|
|
||||||
row, and that row is the same in every selection where the verb appears.
|
|
||||||
Dress-up is the fourth row on an edge, on a face, on a body, on the day the
|
|
||||||
product ships and two years later. The hand learns the position; the eye stops
|
|
||||||
being needed.
|
|
||||||
- **What does not apply is DISABLED IN PLACE, never removed.** This is the
|
|
||||||
single strongest thing the list does, and it is why it beat the radial we
|
|
||||||
drew first: a greyed row still carries its name *and the reason it is grey* —
|
|
||||||
"Create a sketch, or pick a solid face, first", "Create a solid body to
|
|
||||||
pattern first" — in the words the product already ships. On a first-run
|
|
||||||
document the offer is therefore not a mostly-empty control but a map of what
|
|
||||||
the product does and what you have to do first.
|
|
||||||
- **It is an accelerator, not a toll gate.** The toolbar and the single-letter
|
|
||||||
shortcuts keep working exactly as they do now, and pressing a tool directly
|
|
||||||
consumes the same selection (L3). An expert never has to open the offer; a
|
|
||||||
beginner never has to know the toolbar exists. Both routes land in the same
|
|
||||||
place — this is the only way one interface serves §2's three audiences.
|
|
||||||
- **Every row shows its keyboard shortcut**, right-aligned so the keys stack
|
|
||||||
into a column the eye learns without trying, beside the icon and the
|
|
||||||
drawing-office word (L10). This is deliberate: the offer is the path by which
|
|
||||||
a user stops needing the offer. You reach for fillet in its row, the row says
|
|
||||||
"F", and one day your hand types F before the menu has finished opening. A
|
|
||||||
menu that teaches its own shortcut is how a beginner becomes the power user
|
|
||||||
who never opens it — the same interface at two speeds, with no "advanced mode"
|
|
||||||
between them (§7).
|
|
||||||
- **A family with more than one applicable verb opens a submenu** to the side,
|
|
||||||
in its own fixed order. A family with exactly one shows that verb directly, so
|
|
||||||
the common path is never one click longer than it needs to be.
|
|
||||||
- **It never blocks the view of what it acts on**: it opens beside the pick,
|
|
||||||
never over it, with a thin leader back to the point it belongs to, and it
|
|
||||||
dismisses the moment the selection changes.
|
|
||||||
- **The header names what is selected** ("Top face · Body 1"), because a user
|
|
||||||
who mis-picked should find that out before choosing a verb, not after.
|
|
||||||
|
|
||||||
#### Opening the offer on every machine
|
|
||||||
|
|
||||||
Right-click is the primary gesture and every platform must have a first-class
|
|
||||||
equivalent — this is a reach requirement (L11), not a nicety:
|
|
||||||
|
|
||||||
| Input | Gesture |
|
|
||||||
|---|---|
|
|
||||||
| Two-button mouse | right-click |
|
|
||||||
| Trackpad | two-finger tap (the OS-standard secondary click) |
|
|
||||||
| macOS, one-button mouse | **long-press**, and Ctrl-click, which is the platform convention |
|
|
||||||
| Keyboard | the Menu key, or Shift+F10, on the current selection |
|
|
||||||
| Touch / pen | long-press |
|
|
||||||
|
|
||||||
The long-press is an **additional** route, never the only one — §6.2 forbids
|
|
||||||
press-and-hold as a sole path to a function, and it stays forbidden. Every
|
|
||||||
opening gesture is reachable at least two ways on every platform, and the
|
|
||||||
keyboard route exists everywhere. A long-press must show that it is charging
|
|
||||||
(a growing ring under the finger) so a user who holds too briefly learns why
|
|
||||||
nothing happened rather than concluding the product is broken (L5).
|
|
||||||
|
|
||||||
#### The row-constancy invariant
|
|
||||||
|
|
||||||
This is the rule that has to survive every future feature, so it is written as
|
|
||||||
an invariant rather than as advice:
|
|
||||||
|
|
||||||
> **Every verb has exactly one row index in the offer. That index is identical
|
|
||||||
> for every selection type in which the verb appears. Verbs that do not apply to
|
|
||||||
> the current selection are DISABLED IN PLACE, with their reason — the offer is
|
|
||||||
> never compacted, re-sorted or re-ordered. Adding a verb never changes the
|
|
||||||
> index of an existing one.**
|
|
||||||
|
|
||||||
Two consequences the group must accept together with the invariant:
|
|
||||||
|
|
||||||
- **No adaptive ordering. Ever.** Not most-used-first, not recently-used-first,
|
|
||||||
not per-selection frequency. An offer that rearranges itself to be helpful
|
|
||||||
destroys the only thing that made it fast, and it does so precisely for the
|
|
||||||
user who has just started to learn it. (Office 2000's adaptive menus are the
|
|
||||||
textbook case; they were removed.)
|
|
||||||
- **Greyed rows are the price, and they are cheap.** A compacted menu is shorter
|
|
||||||
and unlearnable. A constant one is a few rows longer, teaches while it waits,
|
|
||||||
and is memorised in a week.
|
|
||||||
|
|
||||||
#### The map — RATIFIED 2026-07-31
|
|
||||||
|
|
||||||
The invariant is not negotiable, and as of 2026-07-31 neither is the assignment:
|
|
||||||
the row order below is **ratified**. It was argued once; it is not argued again.
|
|
||||||
Changing an index from here on is a breaking change to every user's muscle
|
|
||||||
memory and needs the group, not a pull request (§9 q12).
|
|
||||||
|
|
||||||
Eight families, ordered so the sequence itself has a logic: material is created,
|
|
||||||
grows, is taken away, is refined, is repeated, is moved, is referred to, is
|
|
||||||
edited.
|
|
||||||
|
|
||||||
| Row | Family | On a face | On an edge | On a body | On text/art |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| **1** | Create | Sketch on it | — | — | Edit text |
|
|
||||||
| **2** | Add material | Extrude, thicken | — | Combine, thicken | Extrude |
|
|
||||||
| **3** | Remove | Hole, shell | Thread | Shell, cut, split | — |
|
|
||||||
| **4** | Dress-up | Draft | Fillet, chamfer | Fillet, chamfer | — |
|
|
||||||
| **5** | Repeat | Pattern | Pattern along it | Pattern, mirror | Pattern |
|
|
||||||
| **6** | Transform | Align to, mate | — | Move, mate | Move, size |
|
|
||||||
| **7** | Reference | Plane, axis, measure | Axis, measure | Project, measure, mass | — |
|
|
||||||
| **8** | Modify | Delete face, edit | — | Edit, colour, delete | Replace art |
|
|
||||||
|
|
||||||
A dash means the row is drawn greyed for that selection, with its reason.
|
|
||||||
|
|
||||||
The authoritative version of this table is **`docs/ux/tool_atlas.json`**, which
|
|
||||||
carries all 52 verbs with their preconditions and their refusal strings, taken
|
|
||||||
from the code rather than from memory. Every state it produces — 20 selection
|
|
||||||
kinds × 2 document states, 40 primary menus and 73 submenus — is rendered by
|
|
||||||
`docs/ux/mockups/gen_offer_mockups.py` into `docs/ux/offer_atlas.html`. Read the
|
|
||||||
atlas before proposing a change to the map; the generator refuses to render an
|
|
||||||
address collision, so the map cannot silently rot.
|
|
||||||
|
|
||||||
#### Rejected: the radial ring
|
|
||||||
|
|
||||||
The first design put the eight families at eight compass points around the pick.
|
|
||||||
It is recorded here because it is a good idea that loses on evidence, and
|
|
||||||
someone will propose it again:
|
|
||||||
|
|
||||||
- an inapplicable slot could only be drawn empty, and **an empty slot says
|
|
||||||
nothing** — the reason text above has nowhere to live;
|
|
||||||
- the measured fill was **3.45 of 8 slots**, so most of the control was blank
|
|
||||||
most of the time, and on a fresh document only two of eight were live;
|
|
||||||
- sketch-mode *Create* needs **nine** addresses; eight forced two primitives
|
|
||||||
behind a "More" slot, and a ninth position costs the 45° spacing that made the
|
|
||||||
ring worth having;
|
|
||||||
- long translated names do not fit around a circle, and screen readers and arrow
|
|
||||||
keys need bespoke handling a list gets for free;
|
|
||||||
- a 380 px disc over the model costs more on a 1366×768 screen than a 324 px
|
|
||||||
list beside it (§6.1).
|
|
||||||
|
|
||||||
What it kept — equidistant targets and a future flick gesture — buys little in a
|
|
||||||
product whose experts live on the keyboard by design.
|
|
||||||
|
|
||||||
### 4.2 Confirm and cancel are objects, not gestures
|
|
||||||
|
|
||||||
The old rule — click empty space to commit — is withdrawn. It was an invisible
|
|
||||||
gesture with a destructive meaning: nothing on screen said it, and a stray click
|
|
||||||
committed a feature the user was still adjusting. That is exactly what L5
|
|
||||||
forbids, and it is hostile to the audience §6.1 exists for.
|
|
||||||
|
|
||||||
- **A pending feature carries a confirm/cancel puck**, attached to the geometry
|
|
||||||
it is editing, next to its handles: ✓ commits, ✗ discards. Enter and Escape
|
|
||||||
mirror them for the keyboard (L9). It is drawn where the user's attention
|
|
||||||
already is, and it is the only thing in the viewport that commits.
|
|
||||||
- **Empty space now means "clear the selection"** — the safe meaning, and the
|
|
||||||
same meaning everywhere.
|
|
||||||
- **This is not a dialog** (L4). It is two objects in the scene, on the
|
|
||||||
geometry, non-modal: the camera still orbits, the tree is still there, the
|
|
||||||
values are still editable while it waits.
|
|
||||||
- **Continuous tools do not ask.** Drawing a line, a rectangle, a circle commits
|
|
||||||
each entity as its own gesture completes — a ✓ per line would destroy the
|
|
||||||
inner loop. The puck belongs to *features* (extrude, fillet, hole, pattern,
|
|
||||||
mate) and to sketch edits that hold a pending state. Enter/Escape end a
|
|
||||||
continuous tool rather than confirming an entity.
|
|
||||||
- **Ambiguity resolves toward keeping work, never toward losing it.** Starting
|
|
||||||
another operation while a valid feature is pending commits it rather than
|
|
||||||
discarding it; if it is not valid, the product says why (L7) and keeps it
|
|
||||||
pending. Since undo reaches everything (§6.1), the recoverable direction is
|
|
||||||
always the right default.
|
|
||||||
|
|
||||||
### 4.3 The rest of the grammar
|
|
||||||
|
|
||||||
- **The status line is one imperative sentence** naming what the tool wants
|
|
||||||
next, and it names the target when the target came from a selection
|
|
||||||
("Circle — click centre, then radius · on the picked face"). It is the
|
|
||||||
authoritative feedback surface for the armed tool; the toolbar is not.
|
|
||||||
- **Hover previews, click commits.** A hover shows the ghost of what a click
|
|
||||||
would do wherever this is cheap to compute.
|
|
||||||
- **Selection is persistent and visible** until consumed or cleared. A tool that
|
|
||||||
consumes a selection clears it, so the next feature cannot silently inherit it.
|
|
||||||
- **Every gesture is undoable**, and the feature tree is editable history, not a
|
|
||||||
log. Re-editing a feature re-enters the same on-geometry interaction that
|
|
||||||
created it — including its offer and its puck.
|
|
||||||
|
|
||||||
## 5. Layout and screen budget
|
|
||||||
|
|
||||||
The viewport is the application. Chrome is a tax on it.
|
|
||||||
|
|
||||||
- **One toolbar**, contextual to the mode (model / sketch). Tools are grouped by
|
|
||||||
what they make, not by which subsystem implements them.
|
|
||||||
- **A left rail for the document, not for parameters**: feature tree, bodies,
|
|
||||||
variables. It answers "what exists", never "what value should this be".
|
|
||||||
- **No parameter panel.** Where one exists today it is technical debt with a
|
|
||||||
scheduled removal (§10).
|
|
||||||
- **Print context is ambient**, not a panel: the plate is visible in the design
|
|
||||||
space, and print-domain warnings appear on the geometry that will fail.
|
|
||||||
- **Nothing is added to permanent chrome without removing something**, or
|
|
||||||
demonstrating that the addition is used in the majority of sessions.
|
|
||||||
- **The budget is set by the smallest screen we serve**, 1366×768 (§6.1) — not
|
|
||||||
by the reviewer's monitor. Chrome that fits a 27-inch display and swallows a
|
|
||||||
laptop's has not fitted, it has just failed somewhere the author cannot see.
|
|
||||||
|
|
||||||
## 6. Accessibility — reach first, then the assistive floor
|
|
||||||
|
|
||||||
"Accessible" means two different things and the product owes both. §6.1 is about
|
|
||||||
**who can get in at all**; §6.2 is about **who can operate it once inside**.
|
|
||||||
Neither is a phase. Both are merge requirements.
|
|
||||||
|
|
||||||
### 6.1 Reach — the door has to be open
|
|
||||||
|
|
||||||
The premise of the whole project: someone with no money, no licence, no account,
|
|
||||||
no fast machine and no teacher can open this and make a real thing. Free
|
|
||||||
software on a school laptop is the only path to a CAD tool that reaches people
|
|
||||||
who were never going to be handed one. If a design decision quietly raises the
|
|
||||||
cost of entry, it has broken the premise, however elegant it is.
|
|
||||||
|
|
||||||
- **The reference machine.** A 5-year-old laptop: dual/quad-core CPU,
|
|
||||||
**integrated graphics**, 8 GB RAM, **1366×768** screen, no discrete GPU. The
|
|
||||||
Design tab must be usable there, and any interaction that needs more is a
|
|
||||||
design failure to be solved, not a requirement to be documented. The GPU path
|
|
||||||
degrades gracefully to software rendering rather than refusing to start; the
|
|
||||||
viewport stays interactive while the kernel thinks.
|
|
||||||
- **1366×768 is the layout target, not the stretch case.** A form-heavy side
|
|
||||||
panel is not merely inelegant on that screen — it takes the model off it.
|
|
||||||
This is the second, independent argument for the whole of L1 and §5.
|
|
||||||
- **No account, no cloud, no connection.** The product works forever with the
|
|
||||||
network unplugged. Nothing is uploaded, no sign-in gates any feature, no
|
|
||||||
telemetry is required to use it. A school network that blocks everything must
|
|
||||||
not be able to block this.
|
|
||||||
- **No tier, no plugin wall, no "pro".** Every feature named in this document is
|
|
||||||
in the product everyone downloads. Assemblies and exploded views are not the
|
|
||||||
paid half.
|
|
||||||
- **Files belong to the user**, on their disk, in a format that outlives the
|
|
||||||
project: the design travels inside the ordinary project file, and the geometry
|
|
||||||
exports to STEP and mesh formats anyone can open.
|
|
||||||
- **Learnable without instruction.** The first solid comes with no
|
|
||||||
documentation, no video and no tutorial mode — from noticing that a face can
|
|
||||||
be clicked. Tooltips teach the vocabulary (L10) at the moment it is needed;
|
|
||||||
nothing is explained in a manual the user will never open.
|
|
||||||
- **Plain language at the entry tier.** The Make tier speaks in words a
|
|
||||||
thirteen-year-old reads without stopping. Precision comes with the tier that
|
|
||||||
needs it, and everything is translated, because "accessible" in English only
|
|
||||||
is not accessible.
|
|
||||||
- **Exploration must be free.** Undo reaches everything, work is never lost to a
|
|
||||||
wrong click, and no dialog ever asks the user to be sure. A tool that punishes
|
|
||||||
experiments teaches people to stop experimenting, which is the one thing this
|
|
||||||
audience cannot afford to learn.
|
|
||||||
- **The product never blames the user.** Failures are stated as what happened
|
|
||||||
and what to do (L7). "Invalid input" is not an acceptable sentence anywhere.
|
|
||||||
|
|
||||||
### 6.2 Assistive floor
|
|
||||||
|
|
||||||
- **Keyboard**: every operation reachable and completable without a pointer.
|
|
||||||
Single-letter shortcuts for sketch tools, shown in the offer itself (§4.1) as
|
|
||||||
well as in the tooltip. The offer opens from the keyboard (Menu key or
|
|
||||||
Shift+F10) and walks by arrow key and by type-ahead, so the row map works for
|
|
||||||
someone who never touches the pointer. A visible focus state on every
|
|
||||||
focusable element. No shortcut that only works while the pointer happens to be
|
|
||||||
over the canvas.
|
|
||||||
- **Colour**: never the sole carrier of meaning. Selection is colour *and*
|
|
||||||
outline; an error is colour *and* an icon *and* text. Verify in greyscale.
|
|
||||||
- **Contrast**: labels over the 3D viewport get a scrim or halo so 4.5:1 holds
|
|
||||||
against any background the model can produce, including a white body under a
|
|
||||||
white plate.
|
|
||||||
- **Targets**: handles and grips no smaller than 32 px at 100 % scale, scaling
|
|
||||||
with the OS factor; the grab tolerance is larger than the drawn glyph.
|
|
||||||
- **Timing**: no double-click-to-mean-something-else, no press-and-hold as the
|
|
||||||
only route to a function, no cycle that depends on repeated clicks
|
|
||||||
(see L5). The long-press that opens the offer on a one-button Mac and on touch
|
|
||||||
(§4.1) is explicitly an *additional* route — Ctrl-click, two-finger tap and
|
|
||||||
the keyboard all reach the same place — and it shows its own progress while
|
|
||||||
charging, so it never fails silently.
|
|
||||||
- **Motion**: animation is functional (showing where a thing went), never
|
|
||||||
decorative, and it respects the reduced-motion preference.
|
|
||||||
- **Text**: no fixed-width assumptions; the UI holds together in German and in
|
|
||||||
Chinese, at 125 % and 200 % scale. Every string routed through the normal
|
|
||||||
translation path.
|
|
||||||
|
|
||||||
## 7. Depth without clutter — the three tiers
|
|
||||||
|
|
||||||
Power for experts is delivered by **progressive disclosure of tools, never by
|
|
||||||
relocation of tools**. A tool that appears in a later tier is in the same place
|
|
||||||
it will always be; it is simply not shown yet.
|
|
||||||
|
|
||||||
| Tier | Who | What appears |
|
|
||||||
|---|---|---|
|
|
||||||
| **Make** | first hour | Sketch, extrude, revolve, hole, fillet/chamfer, move, commit to plate |
|
|
||||||
| **Model** | competent user | Patterns, shell, draft, sweep/loft, booleans, reference geometry, variables, import/export |
|
|
||||||
| **Mechanism** | mechanical designer | Assemblies and mates, exploded views, interference detection, surfaces, feature-level editing of imported solids |
|
|
||||||
|
|
||||||
Rules that keep this honest:
|
|
||||||
|
|
||||||
1. **Tiers are non-modal.** No mode switch, no workbench selector, no "advanced
|
|
||||||
mode" toggle that changes the meaning of anything. The tier only governs what
|
|
||||||
is *offered*.
|
|
||||||
2. **A tier reveals itself by use.** Using a body reveals boolean tools; adding
|
|
||||||
a second body reveals assembly tools. The product notices what you are doing.
|
|
||||||
3. **Nothing moves when a tier appears.** A user who learned where fillet lives
|
|
||||||
finds it in the same place forever.
|
|
||||||
4. **An expert tool obeys the same grammar** as a beginner tool. Mates are
|
|
||||||
picked in 3D like everything else, not configured in a table.
|
|
||||||
5. **Exploded views are a view state**, not a document mode — reversible,
|
|
||||||
draggable along mate axes, and never a separate file.
|
|
||||||
|
|
||||||
## 8. Designing for print — the home advantage
|
|
||||||
|
|
||||||
Design-time knowledge the application already has, and must use:
|
|
||||||
|
|
||||||
- **The plate is present** in the design space, at the real size, with the real
|
|
||||||
origin. Committing a body to the plate is one action and preserves placement.
|
|
||||||
- **Print-domain checks run on the model, on the geometry, before slicing**:
|
|
||||||
walls thinner than the nozzle, unsupported overhangs beyond the material's
|
|
||||||
angle, features smaller than the layer height, a part that does not fit the
|
|
||||||
build volume.
|
|
||||||
- **These are warnings on the geometry, never a report.** The thin wall glows;
|
|
||||||
the tooltip says how thin and what the nozzle is.
|
|
||||||
- **Material and machine context is inherited** from the active slicer profile,
|
|
||||||
not re-entered in the Design tab.
|
|
||||||
- **The round trip is preserved**: editing a design after slicing returns to the
|
|
||||||
feature history, not to a mesh.
|
|
||||||
|
|
||||||
## 9. The review gate
|
|
||||||
|
|
||||||
Every pull request that touches the Design tab UI answers these, in the PR body.
|
|
||||||
A "no" that is not accompanied by an argument is a request for changes.
|
|
||||||
|
|
||||||
1. Which law (L1–L11) does the change most directly serve?
|
|
||||||
2. Can the whole operation be completed without the pointer leaving the
|
|
||||||
viewport? If not, why is this the exception?
|
|
||||||
And: does the relevant selection *offer* this tool (§4.1), or must the user
|
|
||||||
already know it exists?
|
|
||||||
3. Are the values draggable *and* typable?
|
|
||||||
4. Screenshot of the state after **exactly one** click of the new gesture,
|
|
||||||
performed as a first-time user.
|
|
||||||
5. Keyboard-only walkthrough: does it complete?
|
|
||||||
6. Greyscale screenshot: is every state still distinguishable?
|
|
||||||
7. What was **removed**? (Net additions to permanent chrome require an argument.)
|
|
||||||
8. Which tier does it belong to, and does it appear without moving anything else?
|
|
||||||
9. What does it do when the geometry is invalid, and is that reported before the
|
|
||||||
commit?
|
|
||||||
10. Interaction cost: actions required for the canonical task it addresses,
|
|
||||||
before and after.
|
|
||||||
11. Reach (L11): screenshot at 1366×768 with the panel open — is the model still
|
|
||||||
on screen? Does it run on integrated graphics? Does it need the network, an
|
|
||||||
account, or a file the user cannot keep?
|
|
||||||
12. If the change adds or moves a verb in the offer: which row, and is it that
|
|
||||||
verb's row in **every** selection where it appears? Did any existing verb's
|
|
||||||
index change? (If yes, this is not a UI change, it is a breaking change to
|
|
||||||
every user's muscle memory, and it needs the group — see §4.1.) Was
|
|
||||||
`docs/ux/tool_atlas.json` updated and the atlas regenerated?
|
|
||||||
13. If the change adds a pointer gesture: what is its keyboard equivalent, and
|
|
||||||
what does a one-button Mac, a trackpad and a touch screen do (§4.1)?
|
|
||||||
|
|
||||||
## 10. Where we stand today — honest inventory
|
|
||||||
|
|
||||||
Complying with the laws already:
|
|
||||||
|
|
||||||
- Sketch inline editors — draw an entity and its dimension tab opens on the
|
|
||||||
geometry; Tab walks Length → Width → Angle.
|
|
||||||
- Fillet/chamfer draggable radius arrow with an editable value label.
|
|
||||||
- Extrude depth arrow; move-body three-axis arrows.
|
|
||||||
- Datum-plane resize handles and offset arrow; ghost reference planes picked in
|
|
||||||
3D.
|
|
||||||
- Imported-art place/size gizmo.
|
|
||||||
- Sketch plane taken from the picked face, with the target named in the status
|
|
||||||
line, and the sketch-plane dropdown deleted outright.
|
|
||||||
|
|
||||||
Violating them, with removal scheduled:
|
|
||||||
|
|
||||||
- **Every tool card is a two-column form** of combos and spin fields in the left
|
|
||||||
panel. This is the single largest debt in the product and the reason this
|
|
||||||
document exists. Tracked as an epic; each card is replaced by its on-geometry
|
|
||||||
equivalent, not improved in place. It fails L1 and it fails L11 twice over —
|
|
||||||
on a 1366×768 screen the cards leave the model a strip.
|
|
||||||
- Seven remaining plane pickers still populate a combo instead of consuming a
|
|
||||||
viewport selection.
|
|
||||||
- Pattern has no on-geometry spacing arrow or count badge.
|
|
||||||
- Hole is positioned by X/Y fields rather than by a point on a face.
|
|
||||||
- Booleans and cuts pick their operands from lists rather than in 3D.
|
|
||||||
- Fillet/chamfer edge selection still requires the click cycle L5 forbids.
|
|
||||||
- **Selecting geometry offers nothing.** There is no contextual offer (§4.1):
|
|
||||||
the user faces the full toolbar whatever they have picked, and finds out that
|
|
||||||
a tool did not apply by it doing nothing. This is the largest single item of
|
|
||||||
new work the charter asks for. The map and every state of it are already
|
|
||||||
drawn (`docs/ux/offer_atlas.html`); what the group owes itself before the code
|
|
||||||
is ratifying the row order, since every verb built before that lands has to be
|
|
||||||
addressed afterwards anyway.
|
|
||||||
- **Committing is an invisible click in empty space** rather than the
|
|
||||||
confirm/cancel puck of §4.2 — the exact gesture that rule withdraws.
|
|
||||||
|
|
||||||
Nothing on the violating list is defended. The only open question for each is
|
|
||||||
what its on-geometry replacement should be.
|
|
||||||
|
|
||||||
## 11. How the group works
|
|
||||||
|
|
||||||
**Roles.** Product/UX lead (owns this document and casts the tie-break vote on
|
|
||||||
interaction questions); kernel maintainer; GUI maintainer; a print-domain
|
|
||||||
reviewer; a mechanical-design reviewer who uses the product on real work; an
|
|
||||||
accessibility reviewer covering both senses of §6 — reach and assistive — who
|
|
||||||
owns the reference machine and actually runs on it. One person may hold more
|
|
||||||
than one role; the UX lead and the mechanical-design reviewer should not be the
|
|
||||||
same person, and nobody reviews reach from a workstation.
|
|
||||||
|
|
||||||
**The absent audience needs a seat.** The fourteen-year-old is not in the room
|
|
||||||
and cannot file an issue. Someone in the group is accountable for B5 and B6, and
|
|
||||||
the group watches real first-timers use the product on the reference machine at
|
|
||||||
least once a quarter — school, makerspace, or a friend's kid. Everything else in
|
|
||||||
this document can be argued from principle; approachability can only be
|
|
||||||
observed.
|
|
||||||
|
|
||||||
**Cadence.** A short weekly review of open interaction proposals. A monthly pass
|
|
||||||
over the violating inventory in §10 — anything that has not moved in two months
|
|
||||||
is either scheduled or explicitly accepted as permanent, with a reason written
|
|
||||||
into this document.
|
|
||||||
|
|
||||||
**How a change moves.**
|
|
||||||
|
|
||||||
1. *Problem* — a described user difficulty, ideally with an interaction-cost
|
|
||||||
measurement, never a solution in disguise.
|
|
||||||
2. *Sketch* — one or two on-geometry interaction proposals, drawn or described
|
|
||||||
as a gesture sequence. Reviewed against §3 before any code.
|
|
||||||
3. *Prototype* — built behind whatever the smallest safe path is, driven end to
|
|
||||||
end on a real display, and screenshotted at each state.
|
|
||||||
4. *Gate* — §9 answered in the PR.
|
|
||||||
5. *Merge*, then update §10.
|
|
||||||
|
|
||||||
**Decisions are written down.** Any resolution that constrains future work is
|
|
||||||
appended to this document as a numbered law or as an accepted exception with its
|
|
||||||
reasoning. A decision that lives only in a call is not a decision.
|
|
||||||
|
|
||||||
**How disagreements resolve.** Against the laws first. If the laws do not decide
|
|
||||||
it, the tie-break is the interaction cost measured on the canonical tasks in
|
|
||||||
§12; if that does not decide it, the UX lead chooses and records why.
|
|
||||||
|
|
||||||
## 12. Canonical tasks — the benchmark
|
|
||||||
|
|
||||||
The measure of every UX change is the cost of these five tasks. Each is timed and
|
|
||||||
counted (clicks, keystrokes, camera actions, mode switches) on the headless rig
|
|
||||||
and, periodically, with real users who have not seen the product.
|
|
||||||
|
|
||||||
| # | Task | What it exercises |
|
|
||||||
|---|---|---|
|
|
||||||
| **B1** | Bracket: sketch an L, extrude, two holes, fillet the inside corner, send to plate | The inner loop |
|
|
||||||
| **B2** | Change a hole diameter and the plate thickness, six features deep, and rebuild | Parametric editability |
|
|
||||||
| **B3** | Take an imported STEP, delete a boss, close the face, thicken a wall to nozzle width | Direct editing + print awareness |
|
|
||||||
| **B4** | Two parts, one revolute mate, check interference, produce an exploded view | The Mechanism tier |
|
|
||||||
| **B5** | First-run: from opening the Design tab to a print-ready solid, no documentation | Approachability |
|
|
||||||
| **B6** | B1 again, on the reference machine at 1366×768, offline, on a fresh account-less install | Reach (L11) |
|
|
||||||
|
|
||||||
Every task is run on the reference machine of §6.1, not on a workstation — a
|
|
||||||
number measured on a fast desktop describes an experience most of our users will
|
|
||||||
never have. B6 repeats the inner loop under the full entry conditions so that
|
|
||||||
reach is a measured quantity and not an intention.
|
|
||||||
|
|
||||||
Targets are set once each task has been measured on the current build. B5's
|
|
||||||
target is expressed in minutes-to-first-solid **by someone who has never seen a
|
|
||||||
CAD program**, and it is the number this project is ultimately judged by.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Appendix — anti-patterns we have already paid for
|
|
||||||
|
|
||||||
Kept because each cost real time and each is easy to reintroduce.
|
|
||||||
|
|
||||||
- **The dropdown that grew a row.** Fixing "cannot sketch on a face" by adding a
|
|
||||||
"Face of Body 1" entry to a plane combo. It reads as a small fix and it is the
|
|
||||||
side-panel architecture reproducing itself.
|
|
||||||
- **The invisible first click.** Flyout buttons and pick cycles whose first click
|
|
||||||
changes nothing meaningful. Filed as bugs three separate times against working
|
|
||||||
code, and made a real bug look fixed when it was not.
|
|
||||||
- **The fix verified through a path the user will never take.** A face-sketch fix
|
|
||||||
confirmed by double-clicking to reach face level. Users click once. A fix
|
|
||||||
reachable only by an undiscoverable gesture is indistinguishable from no fix.
|
|
||||||
- **The wrong feedback surface.** Measuring an armed tool by the toolbar, which
|
|
||||||
never renders keyboard-armed state. The status line is the surface that
|
|
||||||
answers.
|
|
||||||
- **The silent success.** A cut that removed no material, reported as done. Now
|
|
||||||
an error naming the likely cause.
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
# BearConnector.step — examination
|
|
||||||
|
|
||||||
> **Scope.** One file was supplied and it contains **one object: the male.** Everything below is
|
|
||||||
> measured from that single solid. Earlier drafts of this note reasoned about a female pocket and a
|
|
||||||
> mating pair — those objects were never supplied, so any statement about them was speculation and
|
|
||||||
> has been removed. The clearance, the fit, and the pocket's legibility are all **unassessed**.
|
|
||||||
|
|
||||||
Measured, not eyeballed. Imported into the Design tab's own OpenCascade kernel
|
|
||||||
(`import_step` → one valid closed solid), topology queried, geometry checked numerically.
|
|
||||||
Flat drawing: `artifacts/shots/bear-flat.png`. Viewport: `artifacts/shots/bear-02-zoom.png`.
|
|
||||||
|
|
||||||
**File:** AP242 Edition 2, ST-Developer. 1 `MANIFOLD_SOLID_BREP`, 1 `CLOSED_SHELL`.
|
|
||||||
**Size:** 83.06 × 66.69 × 17.27 mm. **Faces:** 30 — 24 planar + 6 cylindrical.
|
|
||||||
**Curves:** 69 lines + 12 circles. **No** splines, spheres, tori or cones.
|
|
||||||
**Relief:** only four Z levels — 0, 3.00, 10.66, 17.27.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What is right, and precisely so
|
|
||||||
|
|
||||||
**The sloping ridge is implemented exactly as briefed.** From (0.00, 18.40, 17.27) to
|
|
||||||
(0.00, 46.72, 10.66): 28.3 mm long, 6.61 mm drop, **13.1° slope**, and both ends sit dead on
|
|
||||||
x = 0.00. It breaks 180° rotation on its own.
|
|
||||||
|
|
||||||
**20.0° uniform draft on all four snout flanks**, identical to within 0.1°:
|
|
||||||
`(0,−0.94,0.342) (0.936,0.08,0.342) (0,0.94,0.342) (−0.936,0.08,0.342)`. That is a real,
|
|
||||||
deliberate lead-in — it self-centres into a matching pocket, and it demoulds and prints.
|
|
||||||
|
|
||||||
**The eyes are exactly symmetric**: Ø9.87 at x = ±16.43, y = 48.01, matching to 0.01 mm.
|
|
||||||
Someone mirrored those on purpose.
|
|
||||||
|
|
||||||
**The mating feature is extremely economical**: only **five edges** exist above the 3 mm plate —
|
|
||||||
the ridge plus two flank edges at each end. Base plate is exactly 3.00 mm.
|
|
||||||
|
|
||||||
The low-poly constraint is honoured. All six cylinders are outline rounds and eye holes; none of
|
|
||||||
them is a mating surface.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The asymmetry is deliberate, and it is complete
|
|
||||||
|
|
||||||
**Correction.** A first pass read the left/right differences as an unfinished mirror. That was wrong:
|
|
||||||
the asymmetry is intentional. Tested properly — every candidate self-symmetry, in the part's own
|
|
||||||
centred frame, with a generous 0.1 mm tolerance:
|
|
||||||
|
|
||||||
| operation | edges mapped onto the part |
|
|
||||||
|---|---|
|
|
||||||
| identity | 81 / 81 — 100 % |
|
|
||||||
| mirror about x = 0 (left/right) | **0 / 81** |
|
|
||||||
| mirror about y = 0 (top/bottom) | **0 / 81** |
|
|
||||||
| rotate 180° about Z | **0 / 81** |
|
|
||||||
| rotate 90° about Z | **0 / 81** |
|
|
||||||
| mirror about the diagonal | **0 / 81** |
|
|
||||||
|
|
||||||
**The symmetry group is trivial.** No rigid motion or reflection maps this part onto itself, so
|
|
||||||
**every partial view determines the orientation uniquely** — you never need to see the whole face to
|
|
||||||
know which way round it goes. That is the strongest possible result for a keying interface and it is
|
|
||||||
exactly what the earlier abstract glyph work kept failing to achieve: a symmetric shape seen at a
|
|
||||||
grazing angle, or half-occluded, gives an ambiguous read.
|
|
||||||
|
|
||||||
### Does it let you GRASP the orientation? Measured, not asserted.
|
|
||||||
|
|
||||||
Unique-in-principle and graspable-at-a-glance are different claims. The symmetry table proves the
|
|
||||||
first. For the second, the front-on picture (outline + eyes + mouth, filled) was rasterised and
|
|
||||||
compared against its own mirror and its own 180° rotation — the two ways a person can get it wrong.
|
|
||||||
|
|
||||||
**By size** (percentage of pixels that differ):
|
|
||||||
|
|
||||||
| width | vs mirror | vs rotated 180° |
|
|
||||||
|---|---|---|
|
|
||||||
| 16 px | 20.7 % | 26.0 % |
|
|
||||||
| 24 px | 21.9 % | 30.9 % |
|
|
||||||
| 32 px | 23.0 % | 28.1 % |
|
|
||||||
| 48 px | 22.4 % | 30.6 % |
|
|
||||||
| 80 px | 24.7 % | 31.0 % |
|
|
||||||
| 160 px | 23.6 % | 31.0 % |
|
|
||||||
|
|
||||||
**The curve is flat.** The full signal is already there at 16 pixels and more resolution adds
|
|
||||||
nothing. That is the whole result: **the orientation cue lives at low spatial frequency**, carried by
|
|
||||||
the overall shape rather than by any detail. It therefore survives distance, blur, poor light,
|
|
||||||
peripheral vision, a small print and a low-resolution screen. It is the exact opposite of the abstract
|
|
||||||
disc glyph, whose roll cue was a small high-frequency feature and died at a grazing angle.
|
|
||||||
|
|
||||||
**Partial views — a claim I made and then withdrew.** I ran a masked-window test and concluded that
|
|
||||||
a single quarter of the face was enough to read the orientation. **That test was invalid and the
|
|
||||||
conclusion is wrong.** It compared a window of the original against *the same window* of the mirrored
|
|
||||||
and rotated versions — which silently hands the observer the registration. It assumes you already
|
|
||||||
know that the patch you are looking at is the top-left quarter, which is exactly the thing you would
|
|
||||||
not know if you could only see a quarter.
|
|
||||||
|
|
||||||
**You need to see the whole face.** The cues here are *relational*: the big ear only means something
|
|
||||||
next to the small ear, and the mouth offset only means something relative to the centreline. None of
|
|
||||||
them is self-locating. Whole-face is the operating condition, and the design should be judged and
|
|
||||||
used on that basis.
|
|
||||||
|
|
||||||
That does not weaken the size result above, which always used the complete silhouette: the whole face
|
|
||||||
reads at 16 px. Needing all of it, and needing very little resolution of it, are compatible — and for
|
|
||||||
a part held in a hand, seeing all of it is the normal case.
|
|
||||||
|
|
||||||
**The signal is allocated to the right risks.** The strongest cue (up to 41.7 %) guards against
|
|
||||||
inserting it upside down — the mistake people actually make. The weakest (~23 %) guards the mirror
|
|
||||||
case, which needs the part flipped over and which the protrusion already prevents mechanically.
|
|
||||||
|
|
||||||
It also does mechanical work beyond the ridge. The ridge alone breaks 180° rotation; the asymmetric
|
|
||||||
outline additionally defeats the **mirrored-part** case — a mirror-image copy will not fit, so a
|
|
||||||
modelling or printing mirror is caught at assembly rather than three steps later.
|
|
||||||
|
|
||||||
And for children specifically, a symmetric cartoon face reads as a mask; illustrators asymmetrise
|
|
||||||
deliberately so a face reads as a *character*. The asymmetry is earning its keep three ways at once.
|
|
||||||
|
|
||||||
### What is worth keeping in mind anyway
|
|
||||||
|
|
||||||
**The ears differ by 42 %** — left 8.33 mm wide (top y 65.68), right 11.81 mm (top y 66.69). Both
|
|
||||||
start at the same y = 60.79, so they read as a deliberate pair rather than an error. 42 % is well
|
|
||||||
above the perceptual threshold: you see it instantly. Good cue.
|
|
||||||
|
|
||||||
**The mouth is a smirk** — x −21.93 … 0.00, centred at x = −10.96, stopping on the centreline. A
|
|
||||||
classic character device and a strong asymmetry.
|
|
||||||
|
|
||||||
**The rounds are the best cue and the one safety question.** All four are on the left — Ø11.71 at
|
|
||||||
(−40.82, 7.38), Ø11.71 at (−34.76, 0.58), Ø10.00 at (−29.85, 60.83), Ø2.90 at (−26.70, 65.95) — and
|
|
||||||
the right side is entirely sharp. This is the *most locally readable* cue in the design: the ears
|
|
||||||
differ only by comparison (you must see both to know which is which), whereas a rounded corner tells
|
|
||||||
you "this is the left" from that corner alone, by eye **or by fingertip**. For children assembling by
|
|
||||||
feel that is the cue doing the real work.
|
|
||||||
|
|
||||||
The tension is that "sharp" on a children's part is a hazard, and the obvious safety fix — round
|
|
||||||
everything — destroys the cue. The resolution is not round-vs-sharp but **large-vs-small radius**:
|
|
||||||
keep R≈6 on the left and give the right R≈1. R1 still reads and feels sharp locally, so the cue
|
|
||||||
survives, and the actual edge hazard goes away. That is the one recommendation that outlives the
|
|
||||||
correction.
|
|
||||||
|
|
||||||
**One measurement that does not fit the story:** the outline is off-centre by **0.54 mm** (left reach
|
|
||||||
40.99, right reach 42.07). A deliberate cue should be unmissable; 0.54 mm is invisible. It is
|
|
||||||
probably a by-product of the other features rather than intent — worth a look, not a defect.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Two judgement calls, not defects
|
|
||||||
|
|
||||||
**The snout is highest at the nose tip and slopes down toward the brow** — a real bear's muzzle
|
|
||||||
does the opposite. Anatomically it reads more like a beak or a horn than a snout. But mechanically
|
|
||||||
it is the better choice: the nose tip enters the pocket first and does the finding. Keep it if the
|
|
||||||
lead-in matters more than the likeness; flip it if "it must look like a bear" wins.
|
|
||||||
|
|
||||||
**Only the male was supplied**, so the clearance, the fit and the pocket are unassessed. Nothing in
|
|
||||||
this note should be read as a judgement on them.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The strategic point, which is the real reason this design is good
|
|
||||||
|
|
||||||
It gives orientation **a name**. "Ears up, nose down" needs no legend, no convention and no
|
|
||||||
documentation. Face recognition is the most robust pattern-matching humans have: it survives low
|
|
||||||
resolution, poor light, partial occlusion and peripheral vision. That is exactly the robustness the
|
|
||||||
abstract ridge key was reaching for, and here it comes for free.
|
|
||||||
|
|
||||||
**One earlier objection does not transfer — noting it only so it is not carried over by mistake.**
|
|
||||||
In §8c of the design doc a female *pocket* measured as visually invisible — flat-shaded, a recess
|
|
||||||
reads as a blank rectangle — and I concluded male/female
|
|
||||||
is the wrong polarity cue. **That was a viewport finding, and it does not apply to a physical part.**
|
|
||||||
Nobody looks into the pocket of a toy; they feel it. For a part in a child's hands, male/female is
|
|
||||||
exactly the right polarity language. The earlier conclusion stands for the on-screen glyph and must
|
|
||||||
not be carried over to this.
|
|
||||||
|
|
||||||
**The one rule to write down now:** the face and the key must never be allowed to disagree. People
|
|
||||||
will trust the face over the mechanics every time. Here they agree — ridge on the centreline, ears
|
|
||||||
up. If the face is ever restyled independently of the key, a user will orient by the bear and be
|
|
||||||
wrong. Tie them permanently, in the model and in whatever generates it.
|
|
||||||
@@ -1,998 +0,0 @@
|
|||||||
ISO-10303-21;
|
|
||||||
HEADER;
|
|
||||||
FILE_DESCRIPTION(('FreeCAD Model'),'2;1');
|
|
||||||
FILE_NAME('Open CASCADE Shape Model','2026-08-05T12:46:26',('FreeCAD'),(
|
|
||||||
'FreeCAD'),'Open CASCADE STEP processor 7.8','FreeCAD','Unknown');
|
|
||||||
FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));
|
|
||||||
ENDSEC;
|
|
||||||
DATA;
|
|
||||||
#1 = APPLICATION_PROTOCOL_DEFINITION('international standard',
|
|
||||||
'automotive_design',2000,#2);
|
|
||||||
#2 = APPLICATION_CONTEXT(
|
|
||||||
'core data for automotive mechanical design processes');
|
|
||||||
#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10);
|
|
||||||
#4 = PRODUCT_DEFINITION_SHAPE('','',#5);
|
|
||||||
#5 = PRODUCT_DEFINITION('design','',#6,#9);
|
|
||||||
#6 = PRODUCT_DEFINITION_FORMATION('','',#7);
|
|
||||||
#7 = PRODUCT('Open CASCADE STEP translator 7.8 1',
|
|
||||||
'Open CASCADE STEP translator 7.8 1','',(#8));
|
|
||||||
#8 = PRODUCT_CONTEXT('',#2,'mechanical');
|
|
||||||
#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design');
|
|
||||||
#10 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#15),#958);
|
|
||||||
#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14);
|
|
||||||
#12 = CARTESIAN_POINT('',(0.,0.,0.));
|
|
||||||
#13 = DIRECTION('',(0.,0.,1.));
|
|
||||||
#14 = DIRECTION('',(1.,0.,-0.));
|
|
||||||
#15 = MANIFOLD_SOLID_BREP('',#16);
|
|
||||||
#16 = CLOSED_SHELL('',(#17,#229,#260,#497,#514,#531,#548,#565,#582,#599,
|
|
||||||
#616,#633,#650,#667,#684,#701,#718,#735,#747,#770,#794,#810,#822,
|
|
||||||
#839,#856,#878,#895,#912,#929,#946));
|
|
||||||
#17 = ADVANCED_FACE('',(#18,#68,#79,#213),#224,.F.);
|
|
||||||
#18 = FACE_BOUND('',#19,.F.);
|
|
||||||
#19 = EDGE_LOOP('',(#20,#30,#38,#46,#54,#62));
|
|
||||||
#20 = ORIENTED_EDGE('',*,*,#21,.F.);
|
|
||||||
#21 = EDGE_CURVE('',#22,#24,#26,.T.);
|
|
||||||
#22 = VERTEX_POINT('',#23);
|
|
||||||
#23 = CARTESIAN_POINT('',(19.029295926024,-0.2,-17.63009960955));
|
|
||||||
#24 = VERTEX_POINT('',#25);
|
|
||||||
#25 = CARTESIAN_POINT('',(16.626582997737,-0.2,-8.940188245231));
|
|
||||||
#26 = LINE('',#27,#28);
|
|
||||||
#27 = CARTESIAN_POINT('',(19.849519003668,-0.2,-20.59660707692));
|
|
||||||
#28 = VECTOR('',#29,1.);
|
|
||||||
#29 = DIRECTION('',(-0.26649542889,0.,0.963836182336));
|
|
||||||
#30 = ORIENTED_EDGE('',*,*,#31,.F.);
|
|
||||||
#31 = EDGE_CURVE('',#32,#22,#34,.T.);
|
|
||||||
#32 = VERTEX_POINT('',#33);
|
|
||||||
#33 = CARTESIAN_POINT('',(22.059435554995,-0.2,-3.734519760785));
|
|
||||||
#34 = LINE('',#35,#36);
|
|
||||||
#35 = CARTESIAN_POINT('',(17.698510515043,-0.2,-23.73280021221));
|
|
||||||
#36 = VECTOR('',#37,1.);
|
|
||||||
#37 = DIRECTION('',(-0.213058124893,0.,-0.977039526026));
|
|
||||||
#38 = ORIENTED_EDGE('',*,*,#39,.F.);
|
|
||||||
#39 = EDGE_CURVE('',#40,#32,#42,.T.);
|
|
||||||
#40 = VERTEX_POINT('',#41);
|
|
||||||
#41 = CARTESIAN_POINT('',(-21.72552223146,-0.2,-3.734519760785));
|
|
||||||
#42 = LINE('',#43,#44);
|
|
||||||
#43 = CARTESIAN_POINT('',(0.297084840953,-0.2,-3.734519760785));
|
|
||||||
#44 = VECTOR('',#45,1.);
|
|
||||||
#45 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#46 = ORIENTED_EDGE('',*,*,#47,.F.);
|
|
||||||
#47 = EDGE_CURVE('',#48,#40,#50,.T.);
|
|
||||||
#48 = VERTEX_POINT('',#49);
|
|
||||||
#49 = CARTESIAN_POINT('',(-21.72552223146,-0.2,-8.903751135252));
|
|
||||||
#50 = LINE('',#51,#52);
|
|
||||||
#51 = CARTESIAN_POINT('',(-21.72552223146,-0.2,-19.83215600037));
|
|
||||||
#52 = VECTOR('',#53,1.);
|
|
||||||
#53 = DIRECTION('',(0.,0.,1.));
|
|
||||||
#54 = ORIENTED_EDGE('',*,*,#55,.F.);
|
|
||||||
#55 = EDGE_CURVE('',#56,#48,#58,.T.);
|
|
||||||
#56 = VERTEX_POINT('',#57);
|
|
||||||
#57 = CARTESIAN_POINT('',(4.383041634064E-04,-0.2,-8.903751615529));
|
|
||||||
#58 = LINE('',#59,#60);
|
|
||||||
#59 = CARTESIAN_POINT('',(-5.279852300138,-0.2,-8.903751135252));
|
|
||||||
#60 = VECTOR('',#61,1.);
|
|
||||||
#61 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#62 = ORIENTED_EDGE('',*,*,#63,.F.);
|
|
||||||
#63 = EDGE_CURVE('',#24,#56,#64,.T.);
|
|
||||||
#64 = LINE('',#65,#66);
|
|
||||||
#65 = CARTESIAN_POINT('',(4.347099726942,-0.2,-8.913277437397));
|
|
||||||
#66 = VECTOR('',#67,1.);
|
|
||||||
#67 = DIRECTION('',(-0.999997598615,0.,2.191520817069E-03));
|
|
||||||
#68 = FACE_BOUND('',#69,.F.);
|
|
||||||
#69 = EDGE_LOOP('',(#70));
|
|
||||||
#70 = ORIENTED_EDGE('',*,*,#71,.F.);
|
|
||||||
#71 = EDGE_CURVE('',#72,#72,#74,.T.);
|
|
||||||
#72 = VERTEX_POINT('',#73);
|
|
||||||
#73 = CARTESIAN_POINT('',(21.163799345768,-0.2,-48.00951684793));
|
|
||||||
#74 = CIRCLE('',#75,4.735522705283);
|
|
||||||
#75 = AXIS2_PLACEMENT_3D('',#76,#77,#78);
|
|
||||||
#76 = CARTESIAN_POINT('',(16.428276640485,-0.2,-48.00951684793));
|
|
||||||
#77 = DIRECTION('',(-0.,1.,0.));
|
|
||||||
#78 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#79 = FACE_BOUND('',#80,.F.);
|
|
||||||
#80 = EDGE_LOOP('',(#81,#91,#100,#108,#116,#125,#133,#142,#150,#158,#166
|
|
||||||
,#174,#182,#190,#198,#206));
|
|
||||||
#81 = ORIENTED_EDGE('',*,*,#82,.T.);
|
|
||||||
#82 = EDGE_CURVE('',#83,#85,#87,.T.);
|
|
||||||
#83 = VERTEX_POINT('',#84);
|
|
||||||
#84 = CARTESIAN_POINT('',(-27.86158468659,-0.2,-65.78852163128));
|
|
||||||
#85 = VERTEX_POINT('',#86);
|
|
||||||
#86 = CARTESIAN_POINT('',(-29.08766684168,-0.2,-64.52156932717));
|
|
||||||
#87 = LINE('',#88,#89);
|
|
||||||
#88 = CARTESIAN_POINT('',(-29.44004200272,-0.2,-64.15744811364));
|
|
||||||
#89 = VECTOR('',#90,1.);
|
|
||||||
#90 = DIRECTION('',(-0.695421216677,0.,0.718602345805));
|
|
||||||
#91 = ORIENTED_EDGE('',*,*,#92,.F.);
|
|
||||||
#92 = EDGE_CURVE('',#93,#85,#95,.T.);
|
|
||||||
#93 = VERTEX_POINT('',#94);
|
|
||||||
#94 = CARTESIAN_POINT('',(-28.96712497021,-0.2,-57.16864680227));
|
|
||||||
#95 = CIRCLE('',#96,5.2);
|
|
||||||
#96 = AXIS2_PLACEMENT_3D('',#97,#98,#99);
|
|
||||||
#97 = CARTESIAN_POINT('',(-25.35093464349,-0.2,-60.90537900045));
|
|
||||||
#98 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#99 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#100 = ORIENTED_EDGE('',*,*,#101,.T.);
|
|
||||||
#101 = EDGE_CURVE('',#93,#102,#104,.T.);
|
|
||||||
#102 = VERTEX_POINT('',#103);
|
|
||||||
#103 = CARTESIAN_POINT('',(-26.93875323652,-0.2,-55.20570756731));
|
|
||||||
#104 = LINE('',#105,#106);
|
|
||||||
#105 = CARTESIAN_POINT('',(-14.90185526362,-0.2,-43.55710346492));
|
|
||||||
#106 = VECTOR('',#107,1.);
|
|
||||||
#107 = DIRECTION('',(0.718602345805,0.,0.695421216677));
|
|
||||||
#108 = ORIENTED_EDGE('',*,*,#109,.T.);
|
|
||||||
#109 = EDGE_CURVE('',#102,#110,#112,.T.);
|
|
||||||
#110 = VERTEX_POINT('',#111);
|
|
||||||
#111 = CARTESIAN_POINT('',(-41.17904151244,-0.2,-10.52828909594));
|
|
||||||
#112 = LINE('',#113,#114);
|
|
||||||
#113 = CARTESIAN_POINT('',(-32.39120436163,-0.2,-38.09921113329));
|
|
||||||
#114 = VECTOR('',#115,1.);
|
|
||||||
#115 = DIRECTION('',(-0.30368282823,0.,0.952773183837));
|
|
||||||
#116 = ORIENTED_EDGE('',*,*,#117,.F.);
|
|
||||||
#117 = EDGE_CURVE('',#118,#110,#120,.T.);
|
|
||||||
#118 = VERTEX_POINT('',#119);
|
|
||||||
#119 = CARTESIAN_POINT('',(-39.69176606491,-0.2,-4.408923352436));
|
|
||||||
#120 = CIRCLE('',#121,6.054044962965);
|
|
||||||
#121 = AXIS2_PLACEMENT_3D('',#122,#123,#124);
|
|
||||||
#122 = CARTESIAN_POINT('',(-35.41090981799,-0.2,-8.689779599357));
|
|
||||||
#123 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#124 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#125 = ORIENTED_EDGE('',*,*,#126,.T.);
|
|
||||||
#126 = EDGE_CURVE('',#118,#127,#129,.T.);
|
|
||||||
#127 = VERTEX_POINT('',#128);
|
|
||||||
#128 = CARTESIAN_POINT('',(-36.85603142851,-0.2,-1.573188716044));
|
|
||||||
#129 = LINE('',#130,#131);
|
|
||||||
#130 = CARTESIAN_POINT('',(-36.19319006079,-0.2,-0.910347348321));
|
|
||||||
#131 = VECTOR('',#132,1.);
|
|
||||||
#132 = DIRECTION('',(0.707106781187,0.,0.707106781187));
|
|
||||||
#133 = ORIENTED_EDGE('',*,*,#134,.F.);
|
|
||||||
#134 = EDGE_CURVE('',#135,#127,#137,.T.);
|
|
||||||
#135 = VERTEX_POINT('',#136);
|
|
||||||
#136 = CARTESIAN_POINT('',(-32.57517518159,-0.2,0.2));
|
|
||||||
#137 = CIRCLE('',#138,6.054044962965);
|
|
||||||
#138 = AXIS2_PLACEMENT_3D('',#139,#140,#141);
|
|
||||||
#139 = CARTESIAN_POINT('',(-32.57517518159,-0.2,-5.854044962965));
|
|
||||||
#140 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#141 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#142 = ORIENTED_EDGE('',*,*,#143,.T.);
|
|
||||||
#143 = EDGE_CURVE('',#135,#144,#146,.T.);
|
|
||||||
#144 = VERTEX_POINT('',#145);
|
|
||||||
#145 = CARTESIAN_POINT('',(35.082842712475,-0.2,0.2));
|
|
||||||
#146 = LINE('',#147,#148);
|
|
||||||
#147 = CARTESIAN_POINT('',(-7.942265537672,-0.2,0.2));
|
|
||||||
#148 = VECTOR('',#149,1.);
|
|
||||||
#149 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#150 = ORIENTED_EDGE('',*,*,#151,.F.);
|
|
||||||
#151 = EDGE_CURVE('',#152,#144,#154,.T.);
|
|
||||||
#152 = VERTEX_POINT('',#153);
|
|
||||||
#153 = CARTESIAN_POINT('',(42.298608189024,-0.2,-7.015765476549));
|
|
||||||
#154 = LINE('',#155,#156);
|
|
||||||
#155 = CARTESIAN_POINT('',(36.596246576251,-0.2,-1.313403863776));
|
|
||||||
#156 = VECTOR('',#157,1.);
|
|
||||||
#157 = DIRECTION('',(-0.707106781187,0.,0.707106781187));
|
|
||||||
#158 = ORIENTED_EDGE('',*,*,#159,.F.);
|
|
||||||
#159 = EDGE_CURVE('',#160,#152,#162,.T.);
|
|
||||||
#160 = VERTEX_POINT('',#161);
|
|
||||||
#161 = CARTESIAN_POINT('',(26.938753236523,-0.2,-55.20570756731));
|
|
||||||
#162 = LINE('',#163,#164);
|
|
||||||
#163 = CARTESIAN_POINT('',(32.699020781567,-0.2,-37.13346923684));
|
|
||||||
#164 = VECTOR('',#165,1.);
|
|
||||||
#165 = DIRECTION('',(0.30368282823,0.,0.952773183837));
|
|
||||||
#166 = ORIENTED_EDGE('',*,*,#167,.F.);
|
|
||||||
#167 = EDGE_CURVE('',#168,#160,#170,.T.);
|
|
||||||
#168 = VERTEX_POINT('',#169);
|
|
||||||
#169 = CARTESIAN_POINT('',(32.703857168398,-0.2,-60.78483712899));
|
|
||||||
#170 = LINE('',#171,#172);
|
|
||||||
#171 = CARTESIAN_POINT('',(16.008242280412,-0.2,-44.62779994931));
|
|
||||||
#172 = VECTOR('',#173,1.);
|
|
||||||
#173 = DIRECTION('',(-0.718602345805,0.,0.695421216677));
|
|
||||||
#174 = ORIENTED_EDGE('',*,*,#175,.F.);
|
|
||||||
#175 = EDGE_CURVE('',#176,#168,#178,.T.);
|
|
||||||
#176 = VERTEX_POINT('',#177);
|
|
||||||
#177 = CARTESIAN_POINT('',(26.715163243538,-0.2,-66.97315781796));
|
|
||||||
#178 = LINE('',#179,#180);
|
|
||||||
#179 = CARTESIAN_POINT('',(30.25240665457,-0.2,-63.31800414721));
|
|
||||||
#180 = VECTOR('',#181,1.);
|
|
||||||
#181 = DIRECTION('',(0.695421216677,0.,0.718602345805));
|
|
||||||
#182 = ORIENTED_EDGE('',*,*,#183,.F.);
|
|
||||||
#183 = EDGE_CURVE('',#184,#176,#186,.T.);
|
|
||||||
#184 = VERTEX_POINT('',#185);
|
|
||||||
#185 = CARTESIAN_POINT('',(20.532019001374,-0.2,-60.98947335481));
|
|
||||||
#186 = LINE('',#187,#188);
|
|
||||||
#187 = CARTESIAN_POINT('',(9.922784884512,-0.2,-50.72247862452));
|
|
||||||
#188 = VECTOR('',#189,1.);
|
|
||||||
#189 = DIRECTION('',(0.718602345805,0.,-0.695421216677));
|
|
||||||
#190 = ORIENTED_EDGE('',*,*,#191,.F.);
|
|
||||||
#191 = EDGE_CURVE('',#192,#184,#194,.T.);
|
|
||||||
#192 = VERTEX_POINT('',#193);
|
|
||||||
#193 = CARTESIAN_POINT('',(-20.53201900137,-0.2,-60.98947335481));
|
|
||||||
#194 = LINE('',#195,#196);
|
|
||||||
#195 = CARTESIAN_POINT('',(5.354765181569,-0.2,-60.98947335481));
|
|
||||||
#196 = VECTOR('',#197,1.);
|
|
||||||
#197 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#198 = ORIENTED_EDGE('',*,*,#199,.T.);
|
|
||||||
#199 = EDGE_CURVE('',#192,#200,#202,.T.);
|
|
||||||
#200 = VERTEX_POINT('',#201);
|
|
||||||
#201 = CARTESIAN_POINT('',(-25.53052705686,-0.2,-65.82673637491));
|
|
||||||
#202 = LINE('',#203,#204);
|
|
||||||
#203 = CARTESIAN_POINT('',(-9.454421870603,-0.2,-50.26922436104));
|
|
||||||
#204 = VECTOR('',#205,1.);
|
|
||||||
#205 = DIRECTION('',(-0.718602345805,0.,-0.695421216677));
|
|
||||||
#206 = ORIENTED_EDGE('',*,*,#207,.F.);
|
|
||||||
#207 = EDGE_CURVE('',#83,#200,#208,.T.);
|
|
||||||
#208 = CIRCLE('',#209,1.648528137424);
|
|
||||||
#209 = AXIS2_PLACEMENT_3D('',#210,#211,#212);
|
|
||||||
#210 = CARTESIAN_POINT('',(-26.67694849991,-0.2,-64.64210018823));
|
|
||||||
#211 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#212 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#213 = FACE_BOUND('',#214,.F.);
|
|
||||||
#214 = EDGE_LOOP('',(#215));
|
|
||||||
#215 = ORIENTED_EDGE('',*,*,#216,.F.);
|
|
||||||
#216 = EDGE_CURVE('',#217,#217,#219,.T.);
|
|
||||||
#217 = VERTEX_POINT('',#218);
|
|
||||||
#218 = CARTESIAN_POINT('',(-11.6927539352,-0.2,-48.00951684793));
|
|
||||||
#219 = CIRCLE('',#220,4.735522705283);
|
|
||||||
#220 = AXIS2_PLACEMENT_3D('',#221,#222,#223);
|
|
||||||
#221 = CARTESIAN_POINT('',(-16.42827664048,-0.2,-48.00951684793));
|
|
||||||
#222 = DIRECTION('',(-0.,1.,0.));
|
|
||||||
#223 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#224 = PLANE('',#225);
|
|
||||||
#225 = AXIS2_PLACEMENT_3D('',#226,#227,#228);
|
|
||||||
#226 = CARTESIAN_POINT('',(0.403056515455,-0.2,-33.34517655273));
|
|
||||||
#227 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#228 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#229 = ADVANCED_FACE('',(#230),#255,.F.);
|
|
||||||
#230 = FACE_BOUND('',#231,.F.);
|
|
||||||
#231 = EDGE_LOOP('',(#232,#240,#241,#249));
|
|
||||||
#232 = ORIENTED_EDGE('',*,*,#233,.T.);
|
|
||||||
#233 = EDGE_CURVE('',#234,#160,#236,.T.);
|
|
||||||
#234 = VERTEX_POINT('',#235);
|
|
||||||
#235 = CARTESIAN_POINT('',(26.938753236523,3.2,-55.20570756731));
|
|
||||||
#236 = LINE('',#237,#238);
|
|
||||||
#237 = CARTESIAN_POINT('',(26.938753236523,3.,-55.20570756731));
|
|
||||||
#238 = VECTOR('',#239,1.);
|
|
||||||
#239 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#240 = ORIENTED_EDGE('',*,*,#159,.T.);
|
|
||||||
#241 = ORIENTED_EDGE('',*,*,#242,.F.);
|
|
||||||
#242 = EDGE_CURVE('',#243,#152,#245,.T.);
|
|
||||||
#243 = VERTEX_POINT('',#244);
|
|
||||||
#244 = CARTESIAN_POINT('',(42.298608189024,3.2,-7.015765476549));
|
|
||||||
#245 = LINE('',#246,#247);
|
|
||||||
#246 = CARTESIAN_POINT('',(42.298608189024,3.,-7.015765476549));
|
|
||||||
#247 = VECTOR('',#248,1.);
|
|
||||||
#248 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#249 = ORIENTED_EDGE('',*,*,#250,.F.);
|
|
||||||
#250 = EDGE_CURVE('',#234,#243,#251,.T.);
|
|
||||||
#251 = LINE('',#252,#253);
|
|
||||||
#252 = CARTESIAN_POINT('',(32.699020781567,3.2,-37.13346923684));
|
|
||||||
#253 = VECTOR('',#254,1.);
|
|
||||||
#254 = DIRECTION('',(0.30368282823,0.,0.952773183837));
|
|
||||||
#255 = PLANE('',#256);
|
|
||||||
#256 = AXIS2_PLACEMENT_3D('',#257,#258,#259);
|
|
||||||
#257 = CARTESIAN_POINT('',(34.581352051556,3.,-31.22785130119));
|
|
||||||
#258 = DIRECTION('',(-0.952773183837,0.,0.30368282823));
|
|
||||||
#259 = DIRECTION('',(0.30368282823,0.,0.952773183837));
|
|
||||||
#260 = ADVANCED_FACE('',(#261,#311,#322,#447,#481),#492,.T.);
|
|
||||||
#261 = FACE_BOUND('',#262,.T.);
|
|
||||||
#262 = EDGE_LOOP('',(#263,#273,#281,#289,#297,#305));
|
|
||||||
#263 = ORIENTED_EDGE('',*,*,#264,.F.);
|
|
||||||
#264 = EDGE_CURVE('',#265,#267,#269,.T.);
|
|
||||||
#265 = VERTEX_POINT('',#266);
|
|
||||||
#266 = CARTESIAN_POINT('',(16.626582997737,3.2,-8.940188245231));
|
|
||||||
#267 = VERTEX_POINT('',#268);
|
|
||||||
#268 = CARTESIAN_POINT('',(4.383041634064E-04,3.2,-8.903751615529));
|
|
||||||
#269 = LINE('',#270,#271);
|
|
||||||
#270 = CARTESIAN_POINT('',(4.347099726942,3.2,-8.913277437397));
|
|
||||||
#271 = VECTOR('',#272,1.);
|
|
||||||
#272 = DIRECTION('',(-0.999997598615,0.,2.191520817069E-03));
|
|
||||||
#273 = ORIENTED_EDGE('',*,*,#274,.F.);
|
|
||||||
#274 = EDGE_CURVE('',#275,#265,#277,.T.);
|
|
||||||
#275 = VERTEX_POINT('',#276);
|
|
||||||
#276 = CARTESIAN_POINT('',(19.029295926024,3.2,-17.63009960955));
|
|
||||||
#277 = LINE('',#278,#279);
|
|
||||||
#278 = CARTESIAN_POINT('',(19.849519003668,3.2,-20.59660707692));
|
|
||||||
#279 = VECTOR('',#280,1.);
|
|
||||||
#280 = DIRECTION('',(-0.26649542889,0.,0.963836182336));
|
|
||||||
#281 = ORIENTED_EDGE('',*,*,#282,.F.);
|
|
||||||
#282 = EDGE_CURVE('',#283,#275,#285,.T.);
|
|
||||||
#283 = VERTEX_POINT('',#284);
|
|
||||||
#284 = CARTESIAN_POINT('',(22.059435554995,3.2,-3.734519760785));
|
|
||||||
#285 = LINE('',#286,#287);
|
|
||||||
#286 = CARTESIAN_POINT('',(17.698510515043,3.2,-23.73280021221));
|
|
||||||
#287 = VECTOR('',#288,1.);
|
|
||||||
#288 = DIRECTION('',(-0.213058124893,0.,-0.977039526026));
|
|
||||||
#289 = ORIENTED_EDGE('',*,*,#290,.F.);
|
|
||||||
#290 = EDGE_CURVE('',#291,#283,#293,.T.);
|
|
||||||
#291 = VERTEX_POINT('',#292);
|
|
||||||
#292 = CARTESIAN_POINT('',(-21.72552223146,3.2,-3.734519760785));
|
|
||||||
#293 = LINE('',#294,#295);
|
|
||||||
#294 = CARTESIAN_POINT('',(0.297084840953,3.2,-3.734519760785));
|
|
||||||
#295 = VECTOR('',#296,1.);
|
|
||||||
#296 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#297 = ORIENTED_EDGE('',*,*,#298,.F.);
|
|
||||||
#298 = EDGE_CURVE('',#299,#291,#301,.T.);
|
|
||||||
#299 = VERTEX_POINT('',#300);
|
|
||||||
#300 = CARTESIAN_POINT('',(-21.72552223146,3.2,-8.903751135252));
|
|
||||||
#301 = LINE('',#302,#303);
|
|
||||||
#302 = CARTESIAN_POINT('',(-21.72552223146,3.2,-19.83215600037));
|
|
||||||
#303 = VECTOR('',#304,1.);
|
|
||||||
#304 = DIRECTION('',(0.,0.,1.));
|
|
||||||
#305 = ORIENTED_EDGE('',*,*,#306,.F.);
|
|
||||||
#306 = EDGE_CURVE('',#267,#299,#307,.T.);
|
|
||||||
#307 = LINE('',#308,#309);
|
|
||||||
#308 = CARTESIAN_POINT('',(-5.279852300138,3.2,-8.903751135252));
|
|
||||||
#309 = VECTOR('',#310,1.);
|
|
||||||
#310 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#311 = FACE_BOUND('',#312,.T.);
|
|
||||||
#312 = EDGE_LOOP('',(#313));
|
|
||||||
#313 = ORIENTED_EDGE('',*,*,#314,.F.);
|
|
||||||
#314 = EDGE_CURVE('',#315,#315,#317,.T.);
|
|
||||||
#315 = VERTEX_POINT('',#316);
|
|
||||||
#316 = CARTESIAN_POINT('',(21.163799345768,3.2,-48.00951684793));
|
|
||||||
#317 = CIRCLE('',#318,4.735522705283);
|
|
||||||
#318 = AXIS2_PLACEMENT_3D('',#319,#320,#321);
|
|
||||||
#319 = CARTESIAN_POINT('',(16.428276640485,3.2,-48.00951684793));
|
|
||||||
#320 = DIRECTION('',(-0.,1.,0.));
|
|
||||||
#321 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#322 = FACE_BOUND('',#323,.T.);
|
|
||||||
#323 = EDGE_LOOP('',(#324,#334,#343,#351,#360,#368,#374,#375,#383,#391,
|
|
||||||
#399,#407,#415,#424,#432,#441));
|
|
||||||
#324 = ORIENTED_EDGE('',*,*,#325,.T.);
|
|
||||||
#325 = EDGE_CURVE('',#326,#328,#330,.T.);
|
|
||||||
#326 = VERTEX_POINT('',#327);
|
|
||||||
#327 = CARTESIAN_POINT('',(-26.93875323652,3.2,-55.20570756731));
|
|
||||||
#328 = VERTEX_POINT('',#329);
|
|
||||||
#329 = CARTESIAN_POINT('',(-41.17904151244,3.2,-10.52828909594));
|
|
||||||
#330 = LINE('',#331,#332);
|
|
||||||
#331 = CARTESIAN_POINT('',(-32.39120436163,3.2,-38.09921113329));
|
|
||||||
#332 = VECTOR('',#333,1.);
|
|
||||||
#333 = DIRECTION('',(-0.30368282823,0.,0.952773183837));
|
|
||||||
#334 = ORIENTED_EDGE('',*,*,#335,.F.);
|
|
||||||
#335 = EDGE_CURVE('',#336,#328,#338,.T.);
|
|
||||||
#336 = VERTEX_POINT('',#337);
|
|
||||||
#337 = CARTESIAN_POINT('',(-39.69176606491,3.2,-4.408923352436));
|
|
||||||
#338 = CIRCLE('',#339,6.054044962965);
|
|
||||||
#339 = AXIS2_PLACEMENT_3D('',#340,#341,#342);
|
|
||||||
#340 = CARTESIAN_POINT('',(-35.41090981799,3.2,-8.689779599357));
|
|
||||||
#341 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#342 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#343 = ORIENTED_EDGE('',*,*,#344,.T.);
|
|
||||||
#344 = EDGE_CURVE('',#336,#345,#347,.T.);
|
|
||||||
#345 = VERTEX_POINT('',#346);
|
|
||||||
#346 = CARTESIAN_POINT('',(-36.85603142851,3.2,-1.573188716044));
|
|
||||||
#347 = LINE('',#348,#349);
|
|
||||||
#348 = CARTESIAN_POINT('',(-36.19319006079,3.2,-0.910347348321));
|
|
||||||
#349 = VECTOR('',#350,1.);
|
|
||||||
#350 = DIRECTION('',(0.707106781187,0.,0.707106781187));
|
|
||||||
#351 = ORIENTED_EDGE('',*,*,#352,.F.);
|
|
||||||
#352 = EDGE_CURVE('',#353,#345,#355,.T.);
|
|
||||||
#353 = VERTEX_POINT('',#354);
|
|
||||||
#354 = CARTESIAN_POINT('',(-32.57517518159,3.2,0.2));
|
|
||||||
#355 = CIRCLE('',#356,6.054044962965);
|
|
||||||
#356 = AXIS2_PLACEMENT_3D('',#357,#358,#359);
|
|
||||||
#357 = CARTESIAN_POINT('',(-32.57517518159,3.2,-5.854044962965));
|
|
||||||
#358 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#359 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#360 = ORIENTED_EDGE('',*,*,#361,.T.);
|
|
||||||
#361 = EDGE_CURVE('',#353,#362,#364,.T.);
|
|
||||||
#362 = VERTEX_POINT('',#363);
|
|
||||||
#363 = CARTESIAN_POINT('',(35.082842712475,3.2,0.2));
|
|
||||||
#364 = LINE('',#365,#366);
|
|
||||||
#365 = CARTESIAN_POINT('',(-7.942265537672,3.2,0.2));
|
|
||||||
#366 = VECTOR('',#367,1.);
|
|
||||||
#367 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#368 = ORIENTED_EDGE('',*,*,#369,.F.);
|
|
||||||
#369 = EDGE_CURVE('',#243,#362,#370,.T.);
|
|
||||||
#370 = LINE('',#371,#372);
|
|
||||||
#371 = CARTESIAN_POINT('',(36.596246576251,3.2,-1.313403863776));
|
|
||||||
#372 = VECTOR('',#373,1.);
|
|
||||||
#373 = DIRECTION('',(-0.707106781187,0.,0.707106781187));
|
|
||||||
#374 = ORIENTED_EDGE('',*,*,#250,.F.);
|
|
||||||
#375 = ORIENTED_EDGE('',*,*,#376,.F.);
|
|
||||||
#376 = EDGE_CURVE('',#377,#234,#379,.T.);
|
|
||||||
#377 = VERTEX_POINT('',#378);
|
|
||||||
#378 = CARTESIAN_POINT('',(32.703857168398,3.2,-60.78483712899));
|
|
||||||
#379 = LINE('',#380,#381);
|
|
||||||
#380 = CARTESIAN_POINT('',(16.008242280412,3.2,-44.62779994931));
|
|
||||||
#381 = VECTOR('',#382,1.);
|
|
||||||
#382 = DIRECTION('',(-0.718602345805,0.,0.695421216677));
|
|
||||||
#383 = ORIENTED_EDGE('',*,*,#384,.F.);
|
|
||||||
#384 = EDGE_CURVE('',#385,#377,#387,.T.);
|
|
||||||
#385 = VERTEX_POINT('',#386);
|
|
||||||
#386 = CARTESIAN_POINT('',(26.715163243538,3.2,-66.97315781796));
|
|
||||||
#387 = LINE('',#388,#389);
|
|
||||||
#388 = CARTESIAN_POINT('',(30.25240665457,3.2,-63.31800414721));
|
|
||||||
#389 = VECTOR('',#390,1.);
|
|
||||||
#390 = DIRECTION('',(0.695421216677,0.,0.718602345805));
|
|
||||||
#391 = ORIENTED_EDGE('',*,*,#392,.F.);
|
|
||||||
#392 = EDGE_CURVE('',#393,#385,#395,.T.);
|
|
||||||
#393 = VERTEX_POINT('',#394);
|
|
||||||
#394 = CARTESIAN_POINT('',(20.532019001374,3.2,-60.98947335481));
|
|
||||||
#395 = LINE('',#396,#397);
|
|
||||||
#396 = CARTESIAN_POINT('',(9.922784884512,3.2,-50.72247862452));
|
|
||||||
#397 = VECTOR('',#398,1.);
|
|
||||||
#398 = DIRECTION('',(0.718602345805,0.,-0.695421216677));
|
|
||||||
#399 = ORIENTED_EDGE('',*,*,#400,.F.);
|
|
||||||
#400 = EDGE_CURVE('',#401,#393,#403,.T.);
|
|
||||||
#401 = VERTEX_POINT('',#402);
|
|
||||||
#402 = CARTESIAN_POINT('',(-20.53201900137,3.2,-60.98947335481));
|
|
||||||
#403 = LINE('',#404,#405);
|
|
||||||
#404 = CARTESIAN_POINT('',(5.354765181569,3.2,-60.98947335481));
|
|
||||||
#405 = VECTOR('',#406,1.);
|
|
||||||
#406 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#407 = ORIENTED_EDGE('',*,*,#408,.T.);
|
|
||||||
#408 = EDGE_CURVE('',#401,#409,#411,.T.);
|
|
||||||
#409 = VERTEX_POINT('',#410);
|
|
||||||
#410 = CARTESIAN_POINT('',(-25.53052705686,3.2,-65.82673637491));
|
|
||||||
#411 = LINE('',#412,#413);
|
|
||||||
#412 = CARTESIAN_POINT('',(-9.454421870603,3.2,-50.26922436104));
|
|
||||||
#413 = VECTOR('',#414,1.);
|
|
||||||
#414 = DIRECTION('',(-0.718602345805,0.,-0.695421216677));
|
|
||||||
#415 = ORIENTED_EDGE('',*,*,#416,.F.);
|
|
||||||
#416 = EDGE_CURVE('',#417,#409,#419,.T.);
|
|
||||||
#417 = VERTEX_POINT('',#418);
|
|
||||||
#418 = CARTESIAN_POINT('',(-27.86158468659,3.2,-65.78852163128));
|
|
||||||
#419 = CIRCLE('',#420,1.648528137424);
|
|
||||||
#420 = AXIS2_PLACEMENT_3D('',#421,#422,#423);
|
|
||||||
#421 = CARTESIAN_POINT('',(-26.67694849991,3.2,-64.64210018823));
|
|
||||||
#422 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#423 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#424 = ORIENTED_EDGE('',*,*,#425,.T.);
|
|
||||||
#425 = EDGE_CURVE('',#417,#426,#428,.T.);
|
|
||||||
#426 = VERTEX_POINT('',#427);
|
|
||||||
#427 = CARTESIAN_POINT('',(-29.08766684168,3.2,-64.52156932717));
|
|
||||||
#428 = LINE('',#429,#430);
|
|
||||||
#429 = CARTESIAN_POINT('',(-29.44004200272,3.2,-64.15744811364));
|
|
||||||
#430 = VECTOR('',#431,1.);
|
|
||||||
#431 = DIRECTION('',(-0.695421216677,0.,0.718602345805));
|
|
||||||
#432 = ORIENTED_EDGE('',*,*,#433,.F.);
|
|
||||||
#433 = EDGE_CURVE('',#434,#426,#436,.T.);
|
|
||||||
#434 = VERTEX_POINT('',#435);
|
|
||||||
#435 = CARTESIAN_POINT('',(-28.96712497021,3.2,-57.16864680227));
|
|
||||||
#436 = CIRCLE('',#437,5.2);
|
|
||||||
#437 = AXIS2_PLACEMENT_3D('',#438,#439,#440);
|
|
||||||
#438 = CARTESIAN_POINT('',(-25.35093464349,3.2,-60.90537900045));
|
|
||||||
#439 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#440 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#441 = ORIENTED_EDGE('',*,*,#442,.T.);
|
|
||||||
#442 = EDGE_CURVE('',#434,#326,#443,.T.);
|
|
||||||
#443 = LINE('',#444,#445);
|
|
||||||
#444 = CARTESIAN_POINT('',(-14.90185526362,3.2,-43.55710346492));
|
|
||||||
#445 = VECTOR('',#446,1.);
|
|
||||||
#446 = DIRECTION('',(0.718602345805,0.,0.695421216677));
|
|
||||||
#447 = FACE_BOUND('',#448,.T.);
|
|
||||||
#448 = EDGE_LOOP('',(#449,#459,#467,#475));
|
|
||||||
#449 = ORIENTED_EDGE('',*,*,#450,.F.);
|
|
||||||
#450 = EDGE_CURVE('',#451,#453,#455,.T.);
|
|
||||||
#451 = VERTEX_POINT('',#452);
|
|
||||||
#452 = CARTESIAN_POINT('',(5.809375885494,3.2,-13.06417917474));
|
|
||||||
#453 = VERTEX_POINT('',#454);
|
|
||||||
#454 = CARTESIAN_POINT('',(2.688069798796,3.2,-49.64588621989));
|
|
||||||
#455 = LINE('',#456,#457);
|
|
||||||
#456 = CARTESIAN_POINT('',(4.914157977861,3.2,-23.55613296875));
|
|
||||||
#457 = VECTOR('',#458,1.);
|
|
||||||
#458 = DIRECTION('',(-8.501532861635E-02,0.,-0.996379643459));
|
|
||||||
#459 = ORIENTED_EDGE('',*,*,#460,.F.);
|
|
||||||
#460 = EDGE_CURVE('',#461,#451,#463,.T.);
|
|
||||||
#461 = VERTEX_POINT('',#462);
|
|
||||||
#462 = CARTESIAN_POINT('',(-5.809375885494,3.2,-13.06417917474));
|
|
||||||
#463 = LINE('',#464,#465);
|
|
||||||
#464 = CARTESIAN_POINT('',(1.615747408047,3.2,-13.06417917474));
|
|
||||||
#465 = VECTOR('',#466,1.);
|
|
||||||
#466 = DIRECTION('',(1.,0.,-3.066574716487E-16));
|
|
||||||
#467 = ORIENTED_EDGE('',*,*,#468,.F.);
|
|
||||||
#468 = EDGE_CURVE('',#469,#461,#471,.T.);
|
|
||||||
#469 = VERTEX_POINT('',#470);
|
|
||||||
#470 = CARTESIAN_POINT('',(-2.688069798796,3.2,-49.64588621989));
|
|
||||||
#471 = LINE('',#472,#473);
|
|
||||||
#472 = CARTESIAN_POINT('',(-4.890802006217,3.2,-23.82986495424));
|
|
||||||
#473 = VECTOR('',#474,1.);
|
|
||||||
#474 = DIRECTION('',(-8.501532861635E-02,0.,0.996379643459));
|
|
||||||
#475 = ORIENTED_EDGE('',*,*,#476,.F.);
|
|
||||||
#476 = EDGE_CURVE('',#453,#469,#477,.T.);
|
|
||||||
#477 = LINE('',#478,#479);
|
|
||||||
#478 = CARTESIAN_POINT('',(1.615747408047,3.2,-49.64588621989));
|
|
||||||
#479 = VECTOR('',#480,1.);
|
|
||||||
#480 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#481 = FACE_BOUND('',#482,.T.);
|
|
||||||
#482 = EDGE_LOOP('',(#483));
|
|
||||||
#483 = ORIENTED_EDGE('',*,*,#484,.F.);
|
|
||||||
#484 = EDGE_CURVE('',#485,#485,#487,.T.);
|
|
||||||
#485 = VERTEX_POINT('',#486);
|
|
||||||
#486 = CARTESIAN_POINT('',(-11.6927539352,3.2,-48.00951684793));
|
|
||||||
#487 = CIRCLE('',#488,4.735522705283);
|
|
||||||
#488 = AXIS2_PLACEMENT_3D('',#489,#490,#491);
|
|
||||||
#489 = CARTESIAN_POINT('',(-16.42827664048,3.2,-48.00951684793));
|
|
||||||
#490 = DIRECTION('',(-0.,1.,0.));
|
|
||||||
#491 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#492 = PLANE('',#493);
|
|
||||||
#493 = AXIS2_PLACEMENT_3D('',#494,#495,#496);
|
|
||||||
#494 = CARTESIAN_POINT('',(0.403056515455,3.2,-33.34517655273));
|
|
||||||
#495 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#496 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#497 = ADVANCED_FACE('',(#498),#509,.F.);
|
|
||||||
#498 = FACE_BOUND('',#499,.F.);
|
|
||||||
#499 = EDGE_LOOP('',(#500,#506,#507,#508));
|
|
||||||
#500 = ORIENTED_EDGE('',*,*,#501,.F.);
|
|
||||||
#501 = EDGE_CURVE('',#168,#377,#502,.T.);
|
|
||||||
#502 = LINE('',#503,#504);
|
|
||||||
#503 = CARTESIAN_POINT('',(32.703857168398,3.,-60.78483712899));
|
|
||||||
#504 = VECTOR('',#505,1.);
|
|
||||||
#505 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#506 = ORIENTED_EDGE('',*,*,#167,.T.);
|
|
||||||
#507 = ORIENTED_EDGE('',*,*,#233,.F.);
|
|
||||||
#508 = ORIENTED_EDGE('',*,*,#376,.F.);
|
|
||||||
#509 = PLANE('',#510);
|
|
||||||
#510 = AXIS2_PLACEMENT_3D('',#511,#512,#513);
|
|
||||||
#511 = CARTESIAN_POINT('',(29.704873980143,3.,-57.88259703786));
|
|
||||||
#512 = DIRECTION('',(-0.695421216677,0.,-0.718602345805));
|
|
||||||
#513 = DIRECTION('',(-0.718602345805,0.,0.695421216677));
|
|
||||||
#514 = ADVANCED_FACE('',(#515),#526,.F.);
|
|
||||||
#515 = FACE_BOUND('',#516,.F.);
|
|
||||||
#516 = EDGE_LOOP('',(#517,#523,#524,#525));
|
|
||||||
#517 = ORIENTED_EDGE('',*,*,#518,.F.);
|
|
||||||
#518 = EDGE_CURVE('',#176,#385,#519,.T.);
|
|
||||||
#519 = LINE('',#520,#521);
|
|
||||||
#520 = CARTESIAN_POINT('',(26.715163243538,3.,-66.97315781796));
|
|
||||||
#521 = VECTOR('',#522,1.);
|
|
||||||
#522 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#523 = ORIENTED_EDGE('',*,*,#175,.T.);
|
|
||||||
#524 = ORIENTED_EDGE('',*,*,#501,.T.);
|
|
||||||
#525 = ORIENTED_EDGE('',*,*,#384,.F.);
|
|
||||||
#526 = PLANE('',#527);
|
|
||||||
#527 = AXIS2_PLACEMENT_3D('',#528,#529,#530);
|
|
||||||
#528 = CARTESIAN_POINT('',(29.709510205968,3.,-63.87899747347));
|
|
||||||
#529 = DIRECTION('',(-0.718602345805,0.,0.695421216677));
|
|
||||||
#530 = DIRECTION('',(0.695421216677,0.,0.718602345805));
|
|
||||||
#531 = ADVANCED_FACE('',(#532),#543,.F.);
|
|
||||||
#532 = FACE_BOUND('',#533,.F.);
|
|
||||||
#533 = EDGE_LOOP('',(#534,#540,#541,#542));
|
|
||||||
#534 = ORIENTED_EDGE('',*,*,#535,.T.);
|
|
||||||
#535 = EDGE_CURVE('',#393,#184,#536,.T.);
|
|
||||||
#536 = LINE('',#537,#538);
|
|
||||||
#537 = CARTESIAN_POINT('',(20.532019001374,3.,-60.98947335481));
|
|
||||||
#538 = VECTOR('',#539,1.);
|
|
||||||
#539 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#540 = ORIENTED_EDGE('',*,*,#183,.T.);
|
|
||||||
#541 = ORIENTED_EDGE('',*,*,#518,.T.);
|
|
||||||
#542 = ORIENTED_EDGE('',*,*,#392,.F.);
|
|
||||||
#543 = PLANE('',#544);
|
|
||||||
#544 = AXIS2_PLACEMENT_3D('',#545,#546,#547);
|
|
||||||
#545 = CARTESIAN_POINT('',(23.522653113203,3.,-63.8836336993));
|
|
||||||
#546 = DIRECTION('',(0.695421216677,0.,0.718602345805));
|
|
||||||
#547 = DIRECTION('',(0.718602345805,0.,-0.695421216677));
|
|
||||||
#548 = ADVANCED_FACE('',(#549),#560,.F.);
|
|
||||||
#549 = FACE_BOUND('',#550,.F.);
|
|
||||||
#550 = EDGE_LOOP('',(#551,#557,#558,#559));
|
|
||||||
#551 = ORIENTED_EDGE('',*,*,#552,.F.);
|
|
||||||
#552 = EDGE_CURVE('',#192,#401,#553,.T.);
|
|
||||||
#553 = LINE('',#554,#555);
|
|
||||||
#554 = CARTESIAN_POINT('',(-20.53201900137,3.,-60.98947335481));
|
|
||||||
#555 = VECTOR('',#556,1.);
|
|
||||||
#556 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#557 = ORIENTED_EDGE('',*,*,#191,.T.);
|
|
||||||
#558 = ORIENTED_EDGE('',*,*,#535,.F.);
|
|
||||||
#559 = ORIENTED_EDGE('',*,*,#400,.F.);
|
|
||||||
#560 = PLANE('',#561);
|
|
||||||
#561 = AXIS2_PLACEMENT_3D('',#562,#563,#564);
|
|
||||||
#562 = CARTESIAN_POINT('',(10.306473847682,3.,-60.98947335481));
|
|
||||||
#563 = DIRECTION('',(0.,0.,1.));
|
|
||||||
#564 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#565 = ADVANCED_FACE('',(#566),#577,.T.);
|
|
||||||
#566 = FACE_BOUND('',#567,.T.);
|
|
||||||
#567 = EDGE_LOOP('',(#568,#574,#575,#576));
|
|
||||||
#568 = ORIENTED_EDGE('',*,*,#569,.F.);
|
|
||||||
#569 = EDGE_CURVE('',#409,#200,#570,.T.);
|
|
||||||
#570 = LINE('',#571,#572);
|
|
||||||
#571 = CARTESIAN_POINT('',(-25.53052705686,3.,-65.82673637491));
|
|
||||||
#572 = VECTOR('',#573,1.);
|
|
||||||
#573 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#574 = ORIENTED_EDGE('',*,*,#408,.F.);
|
|
||||||
#575 = ORIENTED_EDGE('',*,*,#552,.F.);
|
|
||||||
#576 = ORIENTED_EDGE('',*,*,#199,.T.);
|
|
||||||
#577 = PLANE('',#578);
|
|
||||||
#578 = AXIS2_PLACEMENT_3D('',#579,#580,#581);
|
|
||||||
#579 = CARTESIAN_POINT('',(-23.00219525444,3.,-63.37996509944));
|
|
||||||
#580 = DIRECTION('',(0.695421216677,0.,-0.718602345805));
|
|
||||||
#581 = DIRECTION('',(-0.718602345805,0.,-0.695421216677));
|
|
||||||
#582 = ADVANCED_FACE('',(#583),#594,.T.);
|
|
||||||
#583 = FACE_BOUND('',#584,.T.);
|
|
||||||
#584 = EDGE_LOOP('',(#585,#591,#592,#593));
|
|
||||||
#585 = ORIENTED_EDGE('',*,*,#586,.F.);
|
|
||||||
#586 = EDGE_CURVE('',#417,#83,#587,.T.);
|
|
||||||
#587 = LINE('',#588,#589);
|
|
||||||
#588 = CARTESIAN_POINT('',(-27.86158468659,3.,-65.78852163128));
|
|
||||||
#589 = VECTOR('',#590,1.);
|
|
||||||
#590 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#591 = ORIENTED_EDGE('',*,*,#416,.T.);
|
|
||||||
#592 = ORIENTED_EDGE('',*,*,#569,.T.);
|
|
||||||
#593 = ORIENTED_EDGE('',*,*,#207,.F.);
|
|
||||||
#594 = CYLINDRICAL_SURFACE('',#595,1.648528137424);
|
|
||||||
#595 = AXIS2_PLACEMENT_3D('',#596,#597,#598);
|
|
||||||
#596 = CARTESIAN_POINT('',(-26.67694849991,3.,-64.64210018823));
|
|
||||||
#597 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#598 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#599 = ADVANCED_FACE('',(#600),#611,.T.);
|
|
||||||
#600 = FACE_BOUND('',#601,.T.);
|
|
||||||
#601 = EDGE_LOOP('',(#602,#608,#609,#610));
|
|
||||||
#602 = ORIENTED_EDGE('',*,*,#603,.F.);
|
|
||||||
#603 = EDGE_CURVE('',#426,#85,#604,.T.);
|
|
||||||
#604 = LINE('',#605,#606);
|
|
||||||
#605 = CARTESIAN_POINT('',(-29.08766684168,3.,-64.52156932717));
|
|
||||||
#606 = VECTOR('',#607,1.);
|
|
||||||
#607 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#608 = ORIENTED_EDGE('',*,*,#425,.F.);
|
|
||||||
#609 = ORIENTED_EDGE('',*,*,#586,.T.);
|
|
||||||
#610 = ORIENTED_EDGE('',*,*,#82,.T.);
|
|
||||||
#611 = PLANE('',#612);
|
|
||||||
#612 = AXIS2_PLACEMENT_3D('',#613,#614,#615);
|
|
||||||
#613 = CARTESIAN_POINT('',(-28.47462576413,3.,-65.15504547923));
|
|
||||||
#614 = DIRECTION('',(-0.718602345805,0.,-0.695421216677));
|
|
||||||
#615 = DIRECTION('',(-0.695421216677,0.,0.718602345805));
|
|
||||||
#616 = ADVANCED_FACE('',(#617),#628,.T.);
|
|
||||||
#617 = FACE_BOUND('',#618,.T.);
|
|
||||||
#618 = EDGE_LOOP('',(#619,#625,#626,#627));
|
|
||||||
#619 = ORIENTED_EDGE('',*,*,#620,.F.);
|
|
||||||
#620 = EDGE_CURVE('',#434,#93,#621,.T.);
|
|
||||||
#621 = LINE('',#622,#623);
|
|
||||||
#622 = CARTESIAN_POINT('',(-28.96712497021,3.,-57.16864680227));
|
|
||||||
#623 = VECTOR('',#624,1.);
|
|
||||||
#624 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#625 = ORIENTED_EDGE('',*,*,#433,.T.);
|
|
||||||
#626 = ORIENTED_EDGE('',*,*,#603,.T.);
|
|
||||||
#627 = ORIENTED_EDGE('',*,*,#92,.F.);
|
|
||||||
#628 = CYLINDRICAL_SURFACE('',#629,5.2);
|
|
||||||
#629 = AXIS2_PLACEMENT_3D('',#630,#631,#632);
|
|
||||||
#630 = CARTESIAN_POINT('',(-25.35093464349,3.,-60.90537900045));
|
|
||||||
#631 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#632 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#633 = ADVANCED_FACE('',(#634),#645,.T.);
|
|
||||||
#634 = FACE_BOUND('',#635,.T.);
|
|
||||||
#635 = EDGE_LOOP('',(#636,#642,#643,#644));
|
|
||||||
#636 = ORIENTED_EDGE('',*,*,#637,.F.);
|
|
||||||
#637 = EDGE_CURVE('',#326,#102,#638,.T.);
|
|
||||||
#638 = LINE('',#639,#640);
|
|
||||||
#639 = CARTESIAN_POINT('',(-26.93875323652,3.,-55.20570756731));
|
|
||||||
#640 = VECTOR('',#641,1.);
|
|
||||||
#641 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#642 = ORIENTED_EDGE('',*,*,#442,.F.);
|
|
||||||
#643 = ORIENTED_EDGE('',*,*,#620,.T.);
|
|
||||||
#644 = ORIENTED_EDGE('',*,*,#101,.T.);
|
|
||||||
#645 = PLANE('',#646);
|
|
||||||
#646 = AXIS2_PLACEMENT_3D('',#647,#648,#649);
|
|
||||||
#647 = CARTESIAN_POINT('',(-27.90836811563,3.,-56.14404399617));
|
|
||||||
#648 = DIRECTION('',(-0.695421216677,0.,0.718602345805));
|
|
||||||
#649 = DIRECTION('',(0.718602345805,0.,0.695421216677));
|
|
||||||
#650 = ADVANCED_FACE('',(#651),#662,.T.);
|
|
||||||
#651 = FACE_BOUND('',#652,.T.);
|
|
||||||
#652 = EDGE_LOOP('',(#653,#659,#660,#661));
|
|
||||||
#653 = ORIENTED_EDGE('',*,*,#654,.F.);
|
|
||||||
#654 = EDGE_CURVE('',#328,#110,#655,.T.);
|
|
||||||
#655 = LINE('',#656,#657);
|
|
||||||
#656 = CARTESIAN_POINT('',(-41.17904151244,3.,-10.52828909594));
|
|
||||||
#657 = VECTOR('',#658,1.);
|
|
||||||
#658 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#659 = ORIENTED_EDGE('',*,*,#325,.F.);
|
|
||||||
#660 = ORIENTED_EDGE('',*,*,#637,.T.);
|
|
||||||
#661 = ORIENTED_EDGE('',*,*,#109,.T.);
|
|
||||||
#662 = PLANE('',#663);
|
|
||||||
#663 = AXIS2_PLACEMENT_3D('',#664,#665,#666);
|
|
||||||
#664 = CARTESIAN_POINT('',(-34.04006158346,3.,-32.92609366041));
|
|
||||||
#665 = DIRECTION('',(-0.952773183837,0.,-0.30368282823));
|
|
||||||
#666 = DIRECTION('',(-0.30368282823,0.,0.952773183837));
|
|
||||||
#667 = ADVANCED_FACE('',(#668),#679,.T.);
|
|
||||||
#668 = FACE_BOUND('',#669,.T.);
|
|
||||||
#669 = EDGE_LOOP('',(#670,#676,#677,#678));
|
|
||||||
#670 = ORIENTED_EDGE('',*,*,#671,.F.);
|
|
||||||
#671 = EDGE_CURVE('',#336,#118,#672,.T.);
|
|
||||||
#672 = LINE('',#673,#674);
|
|
||||||
#673 = CARTESIAN_POINT('',(-39.69176606491,3.,-4.408923352436));
|
|
||||||
#674 = VECTOR('',#675,1.);
|
|
||||||
#675 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#676 = ORIENTED_EDGE('',*,*,#335,.T.);
|
|
||||||
#677 = ORIENTED_EDGE('',*,*,#654,.T.);
|
|
||||||
#678 = ORIENTED_EDGE('',*,*,#117,.F.);
|
|
||||||
#679 = CYLINDRICAL_SURFACE('',#680,6.054044962965);
|
|
||||||
#680 = AXIS2_PLACEMENT_3D('',#681,#682,#683);
|
|
||||||
#681 = CARTESIAN_POINT('',(-35.41090981799,3.,-8.689779599357));
|
|
||||||
#682 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#683 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#684 = ADVANCED_FACE('',(#685),#696,.T.);
|
|
||||||
#685 = FACE_BOUND('',#686,.T.);
|
|
||||||
#686 = EDGE_LOOP('',(#687,#693,#694,#695));
|
|
||||||
#687 = ORIENTED_EDGE('',*,*,#688,.F.);
|
|
||||||
#688 = EDGE_CURVE('',#345,#127,#689,.T.);
|
|
||||||
#689 = LINE('',#690,#691);
|
|
||||||
#690 = CARTESIAN_POINT('',(-36.85603142851,3.,-1.573188716044));
|
|
||||||
#691 = VECTOR('',#692,1.);
|
|
||||||
#692 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#693 = ORIENTED_EDGE('',*,*,#344,.F.);
|
|
||||||
#694 = ORIENTED_EDGE('',*,*,#671,.T.);
|
|
||||||
#695 = ORIENTED_EDGE('',*,*,#126,.T.);
|
|
||||||
#696 = PLANE('',#697);
|
|
||||||
#697 = AXIS2_PLACEMENT_3D('',#698,#699,#700);
|
|
||||||
#698 = CARTESIAN_POINT('',(-38.27389874671,3.,-2.99105603424));
|
|
||||||
#699 = DIRECTION('',(-0.707106781187,0.,0.707106781187));
|
|
||||||
#700 = DIRECTION('',(0.707106781187,0.,0.707106781187));
|
|
||||||
#701 = ADVANCED_FACE('',(#702),#713,.T.);
|
|
||||||
#702 = FACE_BOUND('',#703,.T.);
|
|
||||||
#703 = EDGE_LOOP('',(#704,#710,#711,#712));
|
|
||||||
#704 = ORIENTED_EDGE('',*,*,#705,.F.);
|
|
||||||
#705 = EDGE_CURVE('',#353,#135,#706,.T.);
|
|
||||||
#706 = LINE('',#707,#708);
|
|
||||||
#707 = CARTESIAN_POINT('',(-32.57517518159,3.,0.2));
|
|
||||||
#708 = VECTOR('',#709,1.);
|
|
||||||
#709 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#710 = ORIENTED_EDGE('',*,*,#352,.T.);
|
|
||||||
#711 = ORIENTED_EDGE('',*,*,#688,.T.);
|
|
||||||
#712 = ORIENTED_EDGE('',*,*,#134,.F.);
|
|
||||||
#713 = CYLINDRICAL_SURFACE('',#714,6.054044962965);
|
|
||||||
#714 = AXIS2_PLACEMENT_3D('',#715,#716,#717);
|
|
||||||
#715 = CARTESIAN_POINT('',(-32.57517518159,3.,-5.854044962965));
|
|
||||||
#716 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#717 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#718 = ADVANCED_FACE('',(#719),#730,.T.);
|
|
||||||
#719 = FACE_BOUND('',#720,.T.);
|
|
||||||
#720 = EDGE_LOOP('',(#721,#727,#728,#729));
|
|
||||||
#721 = ORIENTED_EDGE('',*,*,#722,.F.);
|
|
||||||
#722 = EDGE_CURVE('',#362,#144,#723,.T.);
|
|
||||||
#723 = LINE('',#724,#725);
|
|
||||||
#724 = CARTESIAN_POINT('',(35.082842712475,3.,0.2));
|
|
||||||
#725 = VECTOR('',#726,1.);
|
|
||||||
#726 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#727 = ORIENTED_EDGE('',*,*,#361,.F.);
|
|
||||||
#728 = ORIENTED_EDGE('',*,*,#705,.T.);
|
|
||||||
#729 = ORIENTED_EDGE('',*,*,#143,.T.);
|
|
||||||
#730 = PLANE('',#731);
|
|
||||||
#731 = AXIS2_PLACEMENT_3D('',#732,#733,#734);
|
|
||||||
#732 = CARTESIAN_POINT('',(-16.28758759079,3.,0.2));
|
|
||||||
#733 = DIRECTION('',(0.,0.,1.));
|
|
||||||
#734 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#735 = ADVANCED_FACE('',(#736),#742,.F.);
|
|
||||||
#736 = FACE_BOUND('',#737,.F.);
|
|
||||||
#737 = EDGE_LOOP('',(#738,#739,#740,#741));
|
|
||||||
#738 = ORIENTED_EDGE('',*,*,#151,.T.);
|
|
||||||
#739 = ORIENTED_EDGE('',*,*,#722,.F.);
|
|
||||||
#740 = ORIENTED_EDGE('',*,*,#369,.F.);
|
|
||||||
#741 = ORIENTED_EDGE('',*,*,#242,.T.);
|
|
||||||
#742 = PLANE('',#743);
|
|
||||||
#743 = AXIS2_PLACEMENT_3D('',#744,#745,#746);
|
|
||||||
#744 = CARTESIAN_POINT('',(38.67695526217,3.,-3.394112549695));
|
|
||||||
#745 = DIRECTION('',(-0.707106781187,0.,-0.707106781187));
|
|
||||||
#746 = DIRECTION('',(-0.707106781187,0.,0.707106781187));
|
|
||||||
#747 = ADVANCED_FACE('',(#748),#765,.T.);
|
|
||||||
#748 = FACE_BOUND('',#749,.T.);
|
|
||||||
#749 = EDGE_LOOP('',(#750,#758,#764));
|
|
||||||
#750 = ORIENTED_EDGE('',*,*,#751,.T.);
|
|
||||||
#751 = EDGE_CURVE('',#451,#752,#754,.T.);
|
|
||||||
#752 = VERTEX_POINT('',#753);
|
|
||||||
#753 = CARTESIAN_POINT('',(3.256654205567E-15,17.8572529153,
|
|
||||||
-18.39898295202));
|
|
||||||
#754 = LINE('',#755,#756);
|
|
||||||
#755 = CARTESIAN_POINT('',(5.649679875255,3.602918464526,-13.21082950266
|
|
||||||
));
|
|
||||||
#756 = VECTOR('',#757,1.);
|
|
||||||
#757 = DIRECTION('',(-0.349023821871,0.880598971639,-0.320511814002));
|
|
||||||
#758 = ORIENTED_EDGE('',*,*,#759,.T.);
|
|
||||||
#759 = EDGE_CURVE('',#752,#461,#760,.T.);
|
|
||||||
#760 = LINE('',#761,#762);
|
|
||||||
#761 = CARTESIAN_POINT('',(-5.275833888477,4.546144602338,
|
|
||||||
-13.55413574101));
|
|
||||||
#762 = VECTOR('',#763,1.);
|
|
||||||
#763 = DIRECTION('',(-0.349023821871,-0.880598971639,0.320511814002));
|
|
||||||
#764 = ORIENTED_EDGE('',*,*,#460,.T.);
|
|
||||||
#765 = PLANE('',#766);
|
|
||||||
#766 = AXIS2_PLACEMENT_3D('',#767,#768,#769);
|
|
||||||
#767 = CARTESIAN_POINT('',(2.828438300639,3.068404028665,-13.01628215822
|
|
||||||
));
|
|
||||||
#768 = DIRECTION('',(2.881637632171E-16,0.342020143326,0.939692620786));
|
|
||||||
#769 = DIRECTION('',(-1.048830324052E-16,0.939692620786,-0.342020143326)
|
|
||||||
);
|
|
||||||
#770 = ADVANCED_FACE('',(#771),#789,.T.);
|
|
||||||
#771 = FACE_BOUND('',#772,.T.);
|
|
||||||
#772 = EDGE_LOOP('',(#773,#781,#782,#783));
|
|
||||||
#773 = ORIENTED_EDGE('',*,*,#774,.T.);
|
|
||||||
#774 = EDGE_CURVE('',#775,#752,#777,.T.);
|
|
||||||
#775 = VERTEX_POINT('',#776);
|
|
||||||
#776 = CARTESIAN_POINT('',(1.480297366167E-15,11.242400581089,
|
|
||||||
-46.71869179633));
|
|
||||||
#777 = LINE('',#778,#779);
|
|
||||||
#778 = CARTESIAN_POINT('',(2.6645352591E-15,18.133069549222,
|
|
||||||
-17.21814831317));
|
|
||||||
#779 = VECTOR('',#780,1.);
|
|
||||||
#780 = DIRECTION('',(5.275122655166E-17,0.227455280238,0.97378852709));
|
|
||||||
#781 = ORIENTED_EDGE('',*,*,#751,.F.);
|
|
||||||
#782 = ORIENTED_EDGE('',*,*,#450,.T.);
|
|
||||||
#783 = ORIENTED_EDGE('',*,*,#784,.T.);
|
|
||||||
#784 = EDGE_CURVE('',#453,#775,#785,.T.);
|
|
||||||
#785 = LINE('',#786,#787);
|
|
||||||
#786 = CARTESIAN_POINT('',(1.103762571829,7.940067898719,-47.92064259636
|
|
||||||
));
|
|
||||||
#787 = VECTOR('',#788,1.);
|
|
||||||
#788 = DIRECTION('',(-0.299648208284,0.896513522642,0.326304236859));
|
|
||||||
#789 = PLANE('',#790);
|
|
||||||
#790 = AXIS2_PLACEMENT_3D('',#791,#792,#793);
|
|
||||||
#791 = CARTESIAN_POINT('',(5.823691883056,3.068404028665,-13.45978839622
|
|
||||||
));
|
|
||||||
#792 = DIRECTION('',(0.93629059846,0.342020143326,-7.988827695448E-02));
|
|
||||||
#793 = DIRECTION('',(-0.340781908463,0.939692620786,2.907695487824E-02)
|
|
||||||
);
|
|
||||||
#794 = ADVANCED_FACE('',(#795),#805,.T.);
|
|
||||||
#795 = FACE_BOUND('',#796,.T.);
|
|
||||||
#796 = EDGE_LOOP('',(#797,#803,#804));
|
|
||||||
#797 = ORIENTED_EDGE('',*,*,#798,.T.);
|
|
||||||
#798 = EDGE_CURVE('',#469,#775,#799,.T.);
|
|
||||||
#799 = LINE('',#800,#801);
|
|
||||||
#800 = CARTESIAN_POINT('',(-0.871390517001,8.635298785064,
|
|
||||||
-47.66759924779));
|
|
||||||
#801 = VECTOR('',#802,1.);
|
|
||||||
#802 = DIRECTION('',(0.299648208284,0.896513522642,0.326304236859));
|
|
||||||
#803 = ORIENTED_EDGE('',*,*,#784,.F.);
|
|
||||||
#804 = ORIENTED_EDGE('',*,*,#476,.T.);
|
|
||||||
#805 = PLANE('',#806);
|
|
||||||
#806 = AXIS2_PLACEMENT_3D('',#807,#808,#809);
|
|
||||||
#807 = CARTESIAN_POINT('',(2.828438300639,3.068404028665,-49.69378323641
|
|
||||||
));
|
|
||||||
#808 = DIRECTION('',(0.,0.342020143326,-0.939692620786));
|
|
||||||
#809 = DIRECTION('',(0.,0.939692620786,0.342020143326));
|
|
||||||
#810 = ADVANCED_FACE('',(#811),#817,.T.);
|
|
||||||
#811 = FACE_BOUND('',#812,.T.);
|
|
||||||
#812 = EDGE_LOOP('',(#813,#814,#815,#816));
|
|
||||||
#813 = ORIENTED_EDGE('',*,*,#468,.T.);
|
|
||||||
#814 = ORIENTED_EDGE('',*,*,#759,.F.);
|
|
||||||
#815 = ORIENTED_EDGE('',*,*,#774,.F.);
|
|
||||||
#816 = ORIENTED_EDGE('',*,*,#798,.F.);
|
|
||||||
#817 = PLANE('',#818);
|
|
||||||
#818 = AXIS2_PLACEMENT_3D('',#819,#820,#821);
|
|
||||||
#819 = CARTESIAN_POINT('',(-5.782806207227,3.068404028665,
|
|
||||||
-13.93896851312));
|
|
||||||
#820 = DIRECTION('',(-0.93629059846,0.342020143326,-7.988827695448E-02)
|
|
||||||
);
|
|
||||||
#821 = DIRECTION('',(0.340781908463,0.939692620786,2.907695487824E-02));
|
|
||||||
#822 = ADVANCED_FACE('',(#823),#834,.F.);
|
|
||||||
#823 = FACE_BOUND('',#824,.F.);
|
|
||||||
#824 = EDGE_LOOP('',(#825,#831,#832,#833));
|
|
||||||
#825 = ORIENTED_EDGE('',*,*,#826,.F.);
|
|
||||||
#826 = EDGE_CURVE('',#217,#485,#827,.T.);
|
|
||||||
#827 = LINE('',#828,#829);
|
|
||||||
#828 = CARTESIAN_POINT('',(-11.6927539352,-22.,-48.00951684793));
|
|
||||||
#829 = VECTOR('',#830,1.);
|
|
||||||
#830 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#831 = ORIENTED_EDGE('',*,*,#216,.T.);
|
|
||||||
#832 = ORIENTED_EDGE('',*,*,#826,.T.);
|
|
||||||
#833 = ORIENTED_EDGE('',*,*,#484,.F.);
|
|
||||||
#834 = CYLINDRICAL_SURFACE('',#835,4.735522705283);
|
|
||||||
#835 = AXIS2_PLACEMENT_3D('',#836,#837,#838);
|
|
||||||
#836 = CARTESIAN_POINT('',(-16.42827664048,-22.,-48.00951684793));
|
|
||||||
#837 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#838 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#839 = ADVANCED_FACE('',(#840),#851,.F.);
|
|
||||||
#840 = FACE_BOUND('',#841,.F.);
|
|
||||||
#841 = EDGE_LOOP('',(#842,#848,#849,#850));
|
|
||||||
#842 = ORIENTED_EDGE('',*,*,#843,.F.);
|
|
||||||
#843 = EDGE_CURVE('',#72,#315,#844,.T.);
|
|
||||||
#844 = LINE('',#845,#846);
|
|
||||||
#845 = CARTESIAN_POINT('',(21.163799345768,-22.,-48.00951684793));
|
|
||||||
#846 = VECTOR('',#847,1.);
|
|
||||||
#847 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#848 = ORIENTED_EDGE('',*,*,#71,.T.);
|
|
||||||
#849 = ORIENTED_EDGE('',*,*,#843,.T.);
|
|
||||||
#850 = ORIENTED_EDGE('',*,*,#314,.F.);
|
|
||||||
#851 = CYLINDRICAL_SURFACE('',#852,4.735522705283);
|
|
||||||
#852 = AXIS2_PLACEMENT_3D('',#853,#854,#855);
|
|
||||||
#853 = CARTESIAN_POINT('',(16.428276640485,-22.,-48.00951684793));
|
|
||||||
#854 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#855 = DIRECTION('',(1.,0.,0.));
|
|
||||||
#856 = ADVANCED_FACE('',(#857),#873,.F.);
|
|
||||||
#857 = FACE_BOUND('',#858,.F.);
|
|
||||||
#858 = EDGE_LOOP('',(#859,#865,#866,#872));
|
|
||||||
#859 = ORIENTED_EDGE('',*,*,#860,.F.);
|
|
||||||
#860 = EDGE_CURVE('',#24,#265,#861,.T.);
|
|
||||||
#861 = LINE('',#862,#863);
|
|
||||||
#862 = CARTESIAN_POINT('',(16.626582997737,-22.,-8.940188245231));
|
|
||||||
#863 = VECTOR('',#864,1.);
|
|
||||||
#864 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#865 = ORIENTED_EDGE('',*,*,#63,.T.);
|
|
||||||
#866 = ORIENTED_EDGE('',*,*,#867,.T.);
|
|
||||||
#867 = EDGE_CURVE('',#56,#267,#868,.T.);
|
|
||||||
#868 = LINE('',#869,#870);
|
|
||||||
#869 = CARTESIAN_POINT('',(-5.329070518201E-15,-22.,-8.903751135252));
|
|
||||||
#870 = VECTOR('',#871,1.);
|
|
||||||
#871 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#872 = ORIENTED_EDGE('',*,*,#264,.F.);
|
|
||||||
#873 = PLANE('',#874);
|
|
||||||
#874 = AXIS2_PLACEMENT_3D('',#875,#876,#877);
|
|
||||||
#875 = CARTESIAN_POINT('',(8.237581109188,-22.,-8.921803528809));
|
|
||||||
#876 = DIRECTION('',(-2.191520817069E-03,0.,-0.999997598615));
|
|
||||||
#877 = DIRECTION('',(-0.999997598615,0.,2.191520817069E-03));
|
|
||||||
#878 = ADVANCED_FACE('',(#879),#890,.F.);
|
|
||||||
#879 = FACE_BOUND('',#880,.F.);
|
|
||||||
#880 = EDGE_LOOP('',(#881,#887,#888,#889));
|
|
||||||
#881 = ORIENTED_EDGE('',*,*,#882,.T.);
|
|
||||||
#882 = EDGE_CURVE('',#48,#299,#883,.T.);
|
|
||||||
#883 = LINE('',#884,#885);
|
|
||||||
#884 = CARTESIAN_POINT('',(-21.72552223146,-22.,-8.903751135252));
|
|
||||||
#885 = VECTOR('',#886,1.);
|
|
||||||
#886 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#887 = ORIENTED_EDGE('',*,*,#306,.F.);
|
|
||||||
#888 = ORIENTED_EDGE('',*,*,#867,.F.);
|
|
||||||
#889 = ORIENTED_EDGE('',*,*,#55,.T.);
|
|
||||||
#890 = PLANE('',#891);
|
|
||||||
#891 = AXIS2_PLACEMENT_3D('',#892,#893,#894);
|
|
||||||
#892 = CARTESIAN_POINT('',(-10.96276111573,-22.,-8.903751135252));
|
|
||||||
#893 = DIRECTION('',(0.,0.,-1.));
|
|
||||||
#894 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#895 = ADVANCED_FACE('',(#896),#907,.F.);
|
|
||||||
#896 = FACE_BOUND('',#897,.F.);
|
|
||||||
#897 = EDGE_LOOP('',(#898,#904,#905,#906));
|
|
||||||
#898 = ORIENTED_EDGE('',*,*,#899,.T.);
|
|
||||||
#899 = EDGE_CURVE('',#40,#291,#900,.T.);
|
|
||||||
#900 = LINE('',#901,#902);
|
|
||||||
#901 = CARTESIAN_POINT('',(-21.72552223146,-22.,-3.734519760785));
|
|
||||||
#902 = VECTOR('',#903,1.);
|
|
||||||
#903 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#904 = ORIENTED_EDGE('',*,*,#298,.F.);
|
|
||||||
#905 = ORIENTED_EDGE('',*,*,#882,.F.);
|
|
||||||
#906 = ORIENTED_EDGE('',*,*,#47,.T.);
|
|
||||||
#907 = PLANE('',#908);
|
|
||||||
#908 = AXIS2_PLACEMENT_3D('',#909,#910,#911);
|
|
||||||
#909 = CARTESIAN_POINT('',(-21.72552223146,-22.,-6.319135448019));
|
|
||||||
#910 = DIRECTION('',(-1.,0.,0.));
|
|
||||||
#911 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#912 = ADVANCED_FACE('',(#913),#924,.F.);
|
|
||||||
#913 = FACE_BOUND('',#914,.F.);
|
|
||||||
#914 = EDGE_LOOP('',(#915,#921,#922,#923));
|
|
||||||
#915 = ORIENTED_EDGE('',*,*,#916,.T.);
|
|
||||||
#916 = EDGE_CURVE('',#32,#283,#917,.T.);
|
|
||||||
#917 = LINE('',#918,#919);
|
|
||||||
#918 = CARTESIAN_POINT('',(22.059435554995,-22.,-3.734519760785));
|
|
||||||
#919 = VECTOR('',#920,1.);
|
|
||||||
#920 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#921 = ORIENTED_EDGE('',*,*,#290,.F.);
|
|
||||||
#922 = ORIENTED_EDGE('',*,*,#899,.F.);
|
|
||||||
#923 = ORIENTED_EDGE('',*,*,#39,.T.);
|
|
||||||
#924 = PLANE('',#925);
|
|
||||||
#925 = AXIS2_PLACEMENT_3D('',#926,#927,#928);
|
|
||||||
#926 = CARTESIAN_POINT('',(0.19111316645,-22.,-3.734519760785));
|
|
||||||
#927 = DIRECTION('',(0.,0.,1.));
|
|
||||||
#928 = DIRECTION('',(0.,-1.,0.));
|
|
||||||
#929 = ADVANCED_FACE('',(#930),#941,.F.);
|
|
||||||
#930 = FACE_BOUND('',#931,.F.);
|
|
||||||
#931 = EDGE_LOOP('',(#932,#938,#939,#940));
|
|
||||||
#932 = ORIENTED_EDGE('',*,*,#933,.T.);
|
|
||||||
#933 = EDGE_CURVE('',#22,#275,#934,.T.);
|
|
||||||
#934 = LINE('',#935,#936);
|
|
||||||
#935 = CARTESIAN_POINT('',(19.029295926024,-22.,-17.63009960955));
|
|
||||||
#936 = VECTOR('',#937,1.);
|
|
||||||
#937 = DIRECTION('',(0.,1.,0.));
|
|
||||||
#938 = ORIENTED_EDGE('',*,*,#282,.F.);
|
|
||||||
#939 = ORIENTED_EDGE('',*,*,#916,.F.);
|
|
||||||
#940 = ORIENTED_EDGE('',*,*,#31,.T.);
|
|
||||||
#941 = PLANE('',#942);
|
|
||||||
#942 = AXIS2_PLACEMENT_3D('',#943,#944,#945);
|
|
||||||
#943 = CARTESIAN_POINT('',(20.484588228021,-22.,-10.95643672209));
|
|
||||||
#944 = DIRECTION('',(0.977039526026,0.,-0.213058124893));
|
|
||||||
#945 = DIRECTION('',(-0.213058124893,0.,-0.977039526026));
|
|
||||||
#946 = ADVANCED_FACE('',(#947),#953,.F.);
|
|
||||||
#947 = FACE_BOUND('',#948,.F.);
|
|
||||||
#948 = EDGE_LOOP('',(#949,#950,#951,#952));
|
|
||||||
#949 = ORIENTED_EDGE('',*,*,#21,.T.);
|
|
||||||
#950 = ORIENTED_EDGE('',*,*,#860,.T.);
|
|
||||||
#951 = ORIENTED_EDGE('',*,*,#274,.F.);
|
|
||||||
#952 = ORIENTED_EDGE('',*,*,#933,.F.);
|
|
||||||
#953 = PLANE('',#954);
|
|
||||||
#954 = AXIS2_PLACEMENT_3D('',#955,#956,#957);
|
|
||||||
#955 = CARTESIAN_POINT('',(17.956031892536,-22.,-13.7484168618));
|
|
||||||
#956 = DIRECTION('',(-0.963836182336,0.,-0.26649542889));
|
|
||||||
#957 = DIRECTION('',(-0.26649542889,0.,0.963836182336));
|
|
||||||
#958 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3)
|
|
||||||
GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#962)) GLOBAL_UNIT_ASSIGNED_CONTEXT
|
|
||||||
((#959,#960,#961)) REPRESENTATION_CONTEXT('Context #1',
|
|
||||||
'3D Context with UNIT and UNCERTAINTY') );
|
|
||||||
#959 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) );
|
|
||||||
#960 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) );
|
|
||||||
#961 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() );
|
|
||||||
#962 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-05),#959,
|
|
||||||
'distance_accuracy_value','confusion accuracy');
|
|
||||||
#963 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7));
|
|
||||||
ENDSEC;
|
|
||||||
END-ISO-10303-21;
|
|
||||||
@@ -1,922 +0,0 @@
|
|||||||
# Mate connectors: aligning with the mainstream CAD systems
|
|
||||||
|
|
||||||
Research date: 2026-08-05. Written against `orca_cad` / `Snapmaker` at the M8 state
|
|
||||||
(`CadDocument.{hpp,cpp}`, `apply_mate`, `datum_frame`, the `Mate` card in `DesignPanel.cpp`).
|
|
||||||
|
|
||||||
**Brief:** align with the mate-connector concept as the main CAD programs actually implement it,
|
|
||||||
and be simple, unequivocal, unconfusing. Alignment is the organising principle of this document:
|
|
||||||
every recommendation is labelled either **[INDUSTRY]** — do what they all do — or **[DEVIATION]** —
|
|
||||||
we would be departing, here is why and what it costs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. The answer in ten lines
|
|
||||||
|
|
||||||
1. Seven systems surveyed. **Five of the seven use the same model**; two are the old world.
|
|
||||||
2. The model: a joint is defined between **two local coordinate frames**, one rigidly attached to
|
|
||||||
each part, plus **one type** naming which DOF stay free.
|
|
||||||
3. The frame is called a mate connector (Onshape), a **joint origin** (Fusion, Inventor), a joint
|
|
||||||
connector (FreeCAD 1.0). Same object, three names.
|
|
||||||
4. **Every one of them expresses every DOF about the frame's Z axis.** One axis, one convention.
|
|
||||||
5. **Five types appear in every frame-based system with identical names and identical DOF**:
|
|
||||||
Fastened/Rigid, Revolute, Slider, Cylindrical, Planar. Ball is in four of five.
|
|
||||||
6. That is not fashion — those are the classical **lower kinematic pairs**. The vocabulary converged
|
|
||||||
because the mechanics converged.
|
|
||||||
7. Our kernel is already on the right side of the line: frame-based, five types, Z-relative,
|
|
||||||
superimpose-then-relax. **The architecture needs no revisiting.**
|
|
||||||
8. Where we are out of step: connectors that are not attached to a body; an origin that can only be
|
|
||||||
a face centroid; no live preview of the two Z arrows; a mate card of abstract dropdowns.
|
|
||||||
9. Where we would knowingly deviate: refusing a second mate per body (no vendor does this — it is
|
|
||||||
forced on us by having no solver) and possibly inverting the default mate direction.
|
|
||||||
10. Biggest single win for the stated goal, and it costs no kernel work: **draw both frames and
|
|
||||||
ghost the result before Confirm.** The convention stops needing to be remembered.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. The two families
|
|
||||||
|
|
||||||
**Constraint-based ("old CAD").** The user states pairwise *geometric relations* between raw
|
|
||||||
topology — this face coincident with that face, this axis concentric with that axis, this plane
|
|
||||||
parallel at 12 mm. Each relation removes some DOF; a numerical solver satisfies all of them at once.
|
|
||||||
Fully positioning one part typically takes **three or more mates**, and the set can be
|
|
||||||
over-constrained, under-constrained, or satisfiable in several configurations.
|
|
||||||
|
|
||||||
**Frame-based ("mate connectors").** The user places a *local coordinate system* on each part and
|
|
||||||
states **one** relation between the two frames. The relation is not "these surfaces touch" but
|
|
||||||
"these frames coincide, except for the following DOF, which stay free."
|
|
||||||
|
|
||||||
Onshape's help page opens by drawing exactly this line:
|
|
||||||
|
|
||||||
> *"Mates in Onshape are different than mates in old CAD systems. Many assemblies require only one
|
|
||||||
> Onshape Mate between any two instances, as the movement (degrees of freedom) between those two
|
|
||||||
> instances is embedded in the Mate."*
|
|
||||||
|
|
||||||
The frame-based model won for three reasons, all of which matter here:
|
|
||||||
|
|
||||||
- **One mate per pair.** No mental arithmetic about which three constraints add up to a hinge.
|
|
||||||
- **The DOF are declared, not deduced.** A revolute mate *is* one rotation. You do not discover the
|
|
||||||
remaining freedom by dragging.
|
|
||||||
- **It needs no simultaneous solver for the common case.** Frame-to-frame alignment is a matrix
|
|
||||||
composition — precisely what `apply_mate` already does.
|
|
||||||
|
|
||||||
> **Caveat — several vendors ship both, and "align with X" is therefore ambiguous.** **Inventor**
|
|
||||||
> kept its legacy constraints *and* added frame-based Joints in 2012; many Inventor users still build
|
|
||||||
> assemblies entirely with the old constraint stack. **Creo** has placement constraints *and*
|
|
||||||
> Mechanism connections. **FreeCAD** had constraint-based Assembly2/3 add-ons before the frame-based
|
|
||||||
> Assembly workbench shipped in 1.0. So copying "what Inventor does" means copying **one of two
|
|
||||||
> coexisting workflows**. **Onshape and Fusion 360 are the only pure frame-based examples**, and they
|
|
||||||
> are the ones to weight most heavily when the evidence conflicts.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Field survey — seven systems
|
|
||||||
|
|
||||||
| | Onshape | Fusion 360 | Inventor | FreeCAD 1.0 | Creo | Siemens NX | SOLIDWORKS |
|
|
||||||
|---|---|---|---|---|---|---|---|
|
|
||||||
| **Family** | Frame | Frame | Frame (+ legacy constraints) | Frame (+ legacy add-ons) | Both | Constraint | Constraint |
|
|
||||||
| **Frame object** | Mate connector | Joint origin | Joint origin | Joint connector (`Placement1/2`) | CSYS on `Weld`/`6DOF` | — | — (nearest: **mate reference**) |
|
|
||||||
| **Where it lives** | Part Studio **and** Assembly; in the feature list | Component, inside the joint | Component / inside the joint | Inside the Joint object | Part | — | Part (up to 3 named entities) |
|
|
||||||
| **Origin placement** | Inferred family on hover; `Shift` locks | Discrete **snap points**; `Ctrl` cycles | Snap points + explicit origins | Inferred, previewed on hover | Picked CSYS | Picked entities | Picked entities |
|
|
||||||
| **Orientation control** | Primary axis (Z) + secondary axis; flip + 90° reorient | Flip, angle, offsets | Flip, angle, offsets | `Placement1/2` + `Offset1/2` | CSYS + offset | — | — |
|
|
||||||
| **Type inference** | No — explicit | No — explicit | **Yes — "Automatic"** from picked geometry | No | No | No | Partial (mate reference type) |
|
|
||||||
| **Solver** | Yes, simultaneous — *"order won't affect a Mate"* | Yes | Yes | Yes (Ondsel) | Yes | Yes | Yes |
|
|
||||||
| **Reuse across instances** | **Yes** — a Part Studio connector exists on every instance | Weak | Partial | Per-joint | Interfaces | Product Interface | Mate references auto-mate on insert |
|
|
||||||
|
|
||||||
Three observations that shape everything below.
|
|
||||||
|
|
||||||
- **Every frame-based system reduced the type list by an order of magnitude** relative to SOLIDWORKS
|
|
||||||
(7–13 vs ~25) and lost nothing. That is not simplification-by-omission; it is what happens when the
|
|
||||||
DOF live in the mate instead of being assembled from constraints.
|
|
||||||
- **Every one of them defines its types relative to a single axis.** Slider translates along Z,
|
|
||||||
Revolute rotates about Z, Cylindrical does both, Planar translates in X/Y and rotates about Z.
|
|
||||||
One axis carries the whole vocabulary.
|
|
||||||
- **Onshape alone treats the connector as a first-class, reusable, named object** — and that is also
|
|
||||||
where its worst usability complaints come from (§4).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. The type vocabulary — cross-system table
|
|
||||||
|
|
||||||
DOF = degrees of freedom left **free**, stated about/along the connector Z.
|
|
||||||
|
|
||||||
| DOF | Onshape | Fusion 360 | Inventor | FreeCAD 1.0 | Creo | **Ours today** |
|
|
||||||
|---|---|---|---|---|---|---|
|
|
||||||
| 0 | Fastened | Rigid | Rigid | Fixed | Rigid / Weld | **Fastened** ✅ |
|
|
||||||
| 1 — rot Z | Revolute | Revolute | Rotational | Revolute | Pin | **Revolute** ✅ |
|
|
||||||
| 1 — trans Z | Slider | Slider | Slider | Slider | Slider | **Slider** ✅ |
|
|
||||||
| 2 — rot + trans Z | Cylindrical | Cylindrical | Cylindrical | Cylindrical | Cylinder | **Cylindrical** ✅ |
|
|
||||||
| 3 — trans XY + rot Z | Planar | Planar | Planar | *(Parallel+Distance)* | Planar | **Planar** ✅ |
|
|
||||||
| 3 — rot XYZ | Ball | Ball | Ball | Ball | Ball | — |
|
|
||||||
| 2 — different axes | Pin slot | Pin-Slot | — | — | Slot / Bearing | — |
|
|
||||||
| 1 — coupled | Screw | — | — | Screw | — | — |
|
|
||||||
| 4 | Parallel | — | — | Parallel | — | — |
|
|
||||||
| other | Tangent, Width, Group | As-built | Automatic | Perpendicular, Angle, Distance, Gears, Belt, RackPinion | General, 6DOF | — |
|
|
||||||
|
|
||||||
**Five types appear in every frame-based system, with the same name and the same DOF.** Those five
|
|
||||||
are the industry's common denominator, and they are exactly `mate_kind` 0–4 as already implemented.
|
|
||||||
Ball is in four of five. Everything past that is a long tail no two vendors agree on.
|
|
||||||
|
|
||||||
### Why the convergence is a fact, not a fashion
|
|
||||||
|
|
||||||
A rigid-body placement is an element of SE(3). A mate leaves some set of relative motions free. For
|
|
||||||
the mate to behave the same throughout its range — for a hinge to be a hinge at every angle — that
|
|
||||||
free set must be **closed under composition**: two allowed motions must compose to an allowed motion.
|
|
||||||
A closed set of motions is a **subgroup** of SE(3).
|
|
||||||
|
|
||||||
The subgroups corresponding to physical surface-on-surface contact are the classical **six lower
|
|
||||||
pairs** (Reuleaux):
|
|
||||||
|
|
||||||
| Pair | Free motion relative to Z | DOF |
|
|
||||||
|---|---|---|
|
|
||||||
| Revolute (R) | rotation about Z | 1 |
|
|
||||||
| Prismatic / slider (P) | translation along Z | 1 |
|
|
||||||
| Helical / screw (H) | coupled rotation + translation | 1 |
|
|
||||||
| Cylindrical (C) | rotation about **and** translation along Z | 2 |
|
|
||||||
| Planar (E/G) | translation in X,Y + rotation about Z | 3 |
|
|
||||||
| Spherical / ball (S) | rotation about X, Y, Z | 3 |
|
|
||||||
|
|
||||||
Plus the two trivial ends: identity (0 DOF — **fastened**) and all of SE(3) (6 DOF — floating, i.e.
|
|
||||||
no mate). Hervé's Lie-subgroup analysis of the displacement group is the standard reference for
|
|
||||||
treating these as the algebraic building blocks of mechanism synthesis.
|
|
||||||
|
|
||||||
**Consequence.** Anything outside this table is either (a) a *composition* needing a solver, or
|
|
||||||
(b) not a joint at all but a *measurement*:
|
|
||||||
|
|
||||||
- Onshape's **Parallel** (4 DOF), **Tangent**, **Width**, **Pin slot**, and FreeCAD's **Distance /
|
|
||||||
Angle / Perpendicular** are constraints, not pairs — their free set is not a subgroup, so they only
|
|
||||||
make sense alongside a simultaneous solver.
|
|
||||||
- **Gear, Belt, Rack-and-pinion** are *relations between two mates*, a different object entirely.
|
|
||||||
- **Screw (H)** is a legitimate lower pair but needs a pitch parameter and is rare in printed parts.
|
|
||||||
|
|
||||||
So the vendors' shared five, the lower pairs, and our `mate_kind` 0–4 are the same list arrived at
|
|
||||||
three ways. **[INDUSTRY] Stop looking for missing types and spend the budget on the connector.**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. What they all agree on — adopt verbatim
|
|
||||||
|
|
||||||
Deviating from any of these makes an experienced user's intuition *wrong*, which is the operational
|
|
||||||
definition of "confusing".
|
|
||||||
|
|
||||||
**A1 [INDUSTRY] — The connector is a full right-handed frame.**
|
|
||||||
Origin + Z (primary) + X (secondary). Onshape and Fusion expose exactly these two axis controls and
|
|
||||||
nothing else. A point cannot express spin; an axis cannot express clocking.
|
|
||||||
*Status: we comply* — `DatumCoordSys` carries origin/x/y and derives Z.
|
|
||||||
|
|
||||||
**A2 [INDUSTRY] — Z is the joint axis; every DOF is about or along Z.**
|
|
||||||
Revolute rotates about Z. Slider translates along Z. Planar's free plane is normal to Z. Offsets run
|
|
||||||
along Z. This single rule is what makes the system learnable: **one axis to look at, and its meaning
|
|
||||||
never changes.**
|
|
||||||
*Status: we comply* — `mate_offset` along A's z, `mate_angle` about A's z.
|
|
||||||
|
|
||||||
**A3 [INDUSTRY] — Mating superimposes the two frames; the type then relaxes specific DOF.**
|
|
||||||
FreeCAD states it most plainly: *"the second connector is superimposed on the first connector by
|
|
||||||
default and may change its position according to the joint type."* Fastened is not a special case —
|
|
||||||
it is the base case with nothing relaxed.
|
|
||||||
*Status: we comply* — `T = M_A · Rz · Tz · F · M_B⁻¹`, looser kinds relaxing from there.
|
|
||||||
|
|
||||||
**A4 [INDUSTRY] — The connector belongs to a part and moves with it.**
|
|
||||||
Onshape: a connector defined in a Part Studio *"is available for reuse on every instance of that part
|
|
||||||
in every assembly in which it is instanced."* It is part geometry, not assembly geometry.
|
|
||||||
*Status: **violated**.* `CoordSysType::PointWorld` is a bare world XYZ with `X = world X` and no
|
|
||||||
`coordsys_body`. Such a connector does not follow its part. See §6 G1.
|
|
||||||
|
|
||||||
**A5 [INDUSTRY] — Selection order is meaningful and must be visible.**
|
|
||||||
One connector is the reference; the other is driven onto it. Onshape spells out that offsets are
|
|
||||||
measured *"from the second Mate connector selected to the first"*, and that reversing the order
|
|
||||||
flips the sign.
|
|
||||||
*Status: complied with in the data model* (`mate_cs_a` fixed, `mate_cs_b` moves) *but not in the UI* —
|
|
||||||
two dropdowns labelled A and B do not tell the user which part is about to jump.
|
|
||||||
|
|
||||||
**A6 [INDUSTRY] — Flip and re-clock live in the mate dialog, always.**
|
|
||||||
Onshape: *"Click the arrow icon to flip the direction of the primary axis. Click the Reorient
|
|
||||||
secondary axis icon to rotate the secondary axis in 90-degree increments."*
|
|
||||||
*Status: partial.* We have `mate_flip` (Z reversal). We have `mate_angle` as a free number — strictly
|
|
||||||
more powerful than 90° steps, and much worse to *use*: the common case is "it came in a quarter turn
|
|
||||||
out", and typing 90 is a worse gesture than pressing a button.
|
|
||||||
|
|
||||||
**A7 [INDUSTRY] — DOF are shown, not inferred by the user.**
|
|
||||||
Onshape animates each mate's remaining DOF on demand; Fusion and Inventor name the DOF in the type
|
|
||||||
list. Our dropdown text already does this in words ("free spin + axial slide"). Keep it.
|
|
||||||
|
|
||||||
**A8 [INDUSTRY] — Free DOF are preserved from the current placement, not zeroed.**
|
|
||||||
Onshape: a Planar mate aligns the frames *"but they are not restricted to this location with respect
|
|
||||||
to their degrees of freedom."*
|
|
||||||
*Status: we comply* — and it must be *said*, because a Planar mate that leaves the part where it was
|
|
||||||
looks like a mate that did nothing.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Where they diverge — who to copy, and why
|
|
||||||
|
|
||||||
### D1 — Where the connector's origin comes from
|
|
||||||
|
|
||||||
| | Behaviour |
|
|
||||||
|---|---|
|
|
||||||
| **Fusion 360** | Discrete **snap points** only: vertex, edge midpoint, face centre, arc centre. `Ctrl` cycles the candidates under the cursor. A circle icon denotes a vertex, a triangle a midpoint. "Between two faces" is a separate explicit option. |
|
|
||||||
| **Onshape** | Infers a *family* on hover — centroid, every vertex, every edge midpoint, every arc centre, the centroids of interior regions (holes, slots), and the virtual sharps of conical faces. `Shift` locks the current candidate. |
|
|
||||||
| **Inventor** | Snap points, plus explicit joint origins for awkward cases. |
|
|
||||||
| **FreeCAD 1.0** | Hovering previews where the connector will land before you commit. |
|
|
||||||
| **Ours** | Always the **face centroid**. No alternative exists. |
|
|
||||||
|
|
||||||
Onshape's richness has a cost its own documentation admits: *"The suggested locations are based on
|
|
||||||
the underlying geometry of the part and changing the geometry will change the location of the Mate.
|
|
||||||
This can be undesirable in certain situations."* On the forum this shows up as connectors that move
|
|
||||||
or break on edit — the classic topological-naming failure. Fusion's discrete set is poorer and far
|
|
||||||
more predictable.
|
|
||||||
|
|
||||||
> **[INDUSTRY] Copy Fusion's candidate *set*.** A small, closed, enumerable set — **face centroid,
|
|
||||||
> vertex, edge midpoint, arc/circle centre** — each drawn before commit, with the card naming which is
|
|
||||||
> in use ("Origin: edge midpoint"). This is our largest expressiveness gap: a face centroid alone
|
|
||||||
> cannot place a hinge pin on a corner boss. It is also the one place where copying the *simpler*
|
|
||||||
> vendor is clearly right.
|
|
||||||
>
|
|
||||||
> **Open sub-choice — how the candidate is chosen.** Three options, in increasing order of magic:
|
|
||||||
> (1) **explicit dropdown** in the card after picking the face — no hover behaviour at all;
|
|
||||||
> (2) **Fusion's `Ctrl` cycling** through candidates under the cursor; (3) **Onshape's hover
|
|
||||||
> inference**. Kimi's independent review argued for (1) on the grounds that hover is exactly where
|
|
||||||
> both vendors' instability complaints originate, and that a dropdown gets ~90% of the expressiveness
|
|
||||||
> with none of the hover-guess debugging. That is a fair reading and (1) is the cheapest to build and
|
|
||||||
> the easiest to make unequivocal. **Recommendation: build (1) first; if hover is added later, let it
|
|
||||||
> *pre-fill the dropdown* rather than silently create an implicit connector** — which also keeps R2
|
|
||||||
> (one kind of connector) intact.
|
|
||||||
|
|
||||||
### D2 — Explicit type, or inferred from the geometry?
|
|
||||||
|
|
||||||
Inventor is the only surveyed system that infers: *"Rotational is selected if the two selected
|
|
||||||
origins are circular. Cylindrical if the two selected origins are points on a cylinder. Ball if
|
|
||||||
points on a sphere. Rigid for all other origin selections."* Onshape and Fusion require an explicit
|
|
||||||
choice.
|
|
||||||
|
|
||||||
> **[INDUSTRY, Inventor] Do both, in Inventor's order.** Infer a *default* type from what was picked,
|
|
||||||
> then show it in an editable control. Inference is what makes the tool feel like it understands the
|
|
||||||
> geometry; the visible, editable result is what keeps it unequivocal. Pure inference with no visible
|
|
||||||
> type is the confusing option; a pure dropdown with no default is the tedious one. This also fits
|
|
||||||
> the Design tab's geometry-first charter exactly: point at a bore, get Revolute offered.
|
|
||||||
|
|
||||||
### D3 — How the Z-direction ambiguity is resolved
|
|
||||||
|
|
||||||
This is the specific failure the brief is aimed at. A former IT trainer stated it precisely on the
|
|
||||||
Onshape forum:
|
|
||||||
|
|
||||||
> *"There is always the risk that users will build their own conceptual models of how software works
|
|
||||||
> which may not match the designer's concept. The result is usually a poor user experience and many
|
|
||||||
> mistakes… for a good (say) Fixed mate to occur do the Z axes of the two mates have to be pointing
|
|
||||||
> in the same direction… Alternatively, should they be facing each other?"*
|
|
||||||
|
|
||||||
He is asking the right question and **no vendor's documentation answers it.** Onshape's own advice —
|
|
||||||
*"if the behavior is not what you expected, try flipping the primary and/or secondary axis"* — is
|
|
||||||
trial and error. This is a gap in the industry, not a convention to copy.
|
|
||||||
|
|
||||||
> **[INDUSTRY, method] Resolve it with live preview, not documentation.** FreeCAD previews the
|
|
||||||
> connector on hover; Onshape and Fusion both draw the frames. Draw **both** Z arrows the moment the
|
|
||||||
> second connector is picked, and ghost the resulting placement *before* Confirm. The convention then
|
|
||||||
> never has to be remembered because it is on screen.
|
|
||||||
>
|
|
||||||
> **[DEVIATION, optional] Name the two cases in the user's words** rather than in axis-speak:
|
|
||||||
> "the two faces come together" vs "the axes run the same way". No surveyed vendor does this — they
|
|
||||||
> all ship a flip arrow. It is a small, low-risk improvement on the state of the art, and it is
|
|
||||||
> separable from the default-direction question in §8 D1.
|
|
||||||
|
|
||||||
### D4 — Named, reusable connectors on the part
|
|
||||||
|
|
||||||
Onshape: connectors created in the Part Studio are reused on every instance in every assembly.
|
|
||||||
SOLIDWORKS' **mate reference** reaches the same end by another route: up to three named entities
|
|
||||||
(primary/secondary/tertiary) baked into the part so it auto-mates on drag-and-drop — and a *named*
|
|
||||||
mate reference seeks out a matching name on insertion. That naming trick is how a library of
|
|
||||||
fasteners assembles itself.
|
|
||||||
|
|
||||||
> **[INDUSTRY] Out of scope now, but do not preclude it.** Give connectors a stable, user-visible
|
|
||||||
> name at creation. One string today; expensive to add once documents exist in the wild.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Confusion catalogue
|
|
||||||
|
|
||||||
Documented ways real implementations confuse people. Each is a requirement in disguise.
|
|
||||||
|
|
||||||
**C1 — Which way does Z point?** See D3. If a user has to ask once, they will mis-predict a hundred
|
|
||||||
times.
|
|
||||||
|
|
||||||
**C2 — The roll is unspecified.** Aligning Z leaves one rotation about Z undetermined. Something must
|
|
||||||
pin it, and if that something is world-derived, the frame does not rotate with its part. **This
|
|
||||||
codebase shipped exactly this bug** (`en4`): a face-only connector took Z from the face
|
|
||||||
normal but X from `coordsys_x_hint`, a world constant, so Fastened and Slider claimed to lock an
|
|
||||||
orientation the frame could not see. Fixed 2026-07-26 by deriving X from the face's own first usable
|
|
||||||
edge — but note the fix's own caveat: *"replaying an older document whose face-only connector fed a
|
|
||||||
mate can now place that body differently."* Roll conventions are load-bearing, and changing one is a
|
|
||||||
document-format change.
|
|
||||||
|
|
||||||
**C3 — The origin drifts.** See D1.
|
|
||||||
|
|
||||||
**C4 — Implicit and explicit connectors are not the same thing.** On the Onshape forum, implicit
|
|
||||||
connectors are reported to change their query structure when a feature is edited and re-accepted, and
|
|
||||||
are unusable in places explicit ones work. Two things called by one name that behave differently is a
|
|
||||||
permanent tax.
|
|
||||||
|
|
||||||
**C5 — Which part moves?** A frame alignment is asymmetric. If the UI does not say which frame is
|
|
||||||
driven, the user finds out by watching the wrong part jump.
|
|
||||||
|
|
||||||
**C6 — Which direction is a positive offset?** Onshape measures *"from the second Mate connector
|
|
||||||
selected to the first"* — the sign depends on pick order, and swapping the picks flips it. Documented
|
|
||||||
behaviour, documented surprise.
|
|
||||||
|
|
||||||
**C7 — One intent, several mates.** The SOLIDWORKS failure: expressing "this shaft is in this hole,
|
|
||||||
resting on this shoulder" as three constraints, then discovering the solver picked the mirror
|
|
||||||
configuration. Frame-based systems fix this by construction; the requirement is not to reintroduce it.
|
|
||||||
|
|
||||||
**C8 — Degenerate frames.** A circular face has no usable in-plane edge direction; a cylinder seam
|
|
||||||
projects to nothing; a picked edge parallel to Z gives a zero cross product. `datum_frame` handles all
|
|
||||||
three with fallbacks — the requirement is that a fallback be *visible*, because a silent fallback is
|
|
||||||
C2 wearing a different hat.
|
|
||||||
|
|
||||||
**C9 — Order dependence without a solver.** Onshape can say *"Onshape solves Mates simultaneously so
|
|
||||||
order won't affect a Mate."* A system that composes transforms in tree order cannot say that. Two
|
|
||||||
mates driving one body means the second wins and the first is a lie on screen.
|
|
||||||
|
|
||||||
**C10 — Mirrors and patterns.** A mirrored instance has a left-handed frame. Blindly mirroring a
|
|
||||||
connector gives a frame whose Z still points "out" but whose handedness flipped, so every rotation
|
|
||||||
runs backwards. Cheap to handle now, miserable to retrofit.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Requirements
|
|
||||||
|
|
||||||
Labelled **[INDUSTRY]** (what the frame-based systems do) or **[DEVIATION]** (we would depart).
|
|
||||||
|
|
||||||
### Definition
|
|
||||||
|
|
||||||
**R1 [INDUSTRY] — A mate connector is a frame attached to exactly one body.** No body, no connector.
|
|
||||||
*Test:* creating a connector without a body is rejected at creation, not at mate time.
|
|
||||||
→ **`CoordSysType::PointWorld` violates this.** It is a datum wearing a connector's name.
|
|
||||||
|
|
||||||
**R2 [INDUSTRY] — One kind of connector, not two.** No "implicit" connector that behaves differently
|
|
||||||
from an explicit one. If hover inference is offered, hovering *creates* an ordinary connector.
|
|
||||||
*Why:* C4. *Test:* everything that accepts a connector accepts any connector.
|
|
||||||
|
|
||||||
**R3 [INDUSTRY] — A mate names exactly one subgroup of free motion.** Fastened (0), Revolute (1),
|
|
||||||
Slider (1), Cylindrical (2), Planar (3), optionally Ball (3). *Why:* §3. *Test:* every type's free
|
|
||||||
set is closed; no type is "A and also B".
|
|
||||||
|
|
||||||
### Orientation
|
|
||||||
|
|
||||||
**R4 [INDUSTRY] — Everything is about Z. Say so once, in the UI.** *Test:* no mate parameter refers
|
|
||||||
to any other axis.
|
|
||||||
|
|
||||||
**R5 [DEVIATION] — Z is the outward material direction, and mates default to FACING.**
|
|
||||||
A mate would drive B's Z onto **−A's Z** by default, so picking two faces that should touch makes
|
|
||||||
them touch with no options changed. *Why:* it is the whole of C1.
|
|
||||||
**Cost and caveat:** this inverts today's default (`mate_flip=false` currently *aligns*), and I could
|
|
||||||
not establish from any vendor's documentation what their default actually is — the forum question in
|
|
||||||
D3 went unanswered precisely because it is undocumented. So this is marked a deviation on the honest
|
|
||||||
grounds that **I cannot prove the industry agrees with it.** If D3's live preview lands first, the
|
|
||||||
default matters much less, because the user sees the outcome before committing. See §9 D1.
|
|
||||||
|
|
||||||
**R6 [DEVIATION] — Name the two directions; do not ship a boolean called "flip".**
|
|
||||||
`Direction: Facing | Aligned`. Every surveyed vendor ships a flip arrow instead. A boolean requires
|
|
||||||
remembering what unticked means; two named values do not. Low risk, small improvement on the state of
|
|
||||||
the art.
|
|
||||||
|
|
||||||
**R7 [INDUSTRY] — Roll is picked, or a stored quarter turn. Never world-derived.**
|
|
||||||
X from a referenced edge or in-plane direction; failing that, a deterministic body-attached seed, with
|
|
||||||
**Rotate 90°** offered as a stored integer 0–3 on top (this is Onshape's "reorient secondary axis",
|
|
||||||
A6). *Why:* C2 and the world-constant bug this project already shipped. *Test:* rotate the parent
|
|
||||||
body by any angle; the connector's X rotates with it — *this test already exists* ("a face-only frame
|
|
||||||
rotates with its body").
|
|
||||||
|
|
||||||
**R8 [INDUSTRY] — A degenerate roll is reported, not absorbed.** *Test:* a connector on a full
|
|
||||||
cylindrical face reports "roll undefined — pick a direction" rather than silently taking a fallback.
|
|
||||||
|
|
||||||
### Placement
|
|
||||||
|
|
||||||
**R9 [INDUSTRY, Fusion] — Origin comes from a small closed set of named candidates.**
|
|
||||||
**Face centroid, arc/circle centre, edge midpoint, vertex.** Four. Each stored as
|
|
||||||
`(kind, topological reference)` and resolved at rebuild. *Why:* D1. *Test:* the stored kind is visible
|
|
||||||
in the card; a rebuild either resolves it or raises an error.
|
|
||||||
|
|
||||||
**R10 [INDUSTRY] — An unresolvable reference is an error, never a silent relocation.**
|
|
||||||
*Test:* delete the referenced face; the mate reports "connector A: face not found" and the body stays
|
|
||||||
where it was.
|
|
||||||
|
|
||||||
### Semantics without a solver
|
|
||||||
|
|
||||||
**R11 [DEVIATION] — A body is driven by at most one mate. The second is refused.**
|
|
||||||
**No surveyed system does this** — they all have solvers and all accept many mates per body. It is
|
|
||||||
forced on us by tree-order composition: a second mate on the same body silently overrides the first
|
|
||||||
and the screen shows a configuration satisfying only one stated intent (C9). *Test:* creating a
|
|
||||||
second mate whose moving body already has one is rejected, naming the existing mate.
|
|
||||||
This is the single largest departure in this document. See §9 D4.
|
|
||||||
|
|
||||||
> **A tempting misreading, checked and rejected.** It is easy to find the claim that Onshape mandates
|
|
||||||
> *"exactly one Mate between any two instances"*, which would make R11 an industry agreement rather
|
|
||||||
> than a deviation. **The Onshape page does not say that.** It says *"**Many assemblies require only**
|
|
||||||
> one Onshape Mate between any two instances"* and then lists, as an explicit remedy, *"**Use more
|
|
||||||
> than one Mate if necessary.**"* One mate per pair is Onshape's *typical case*, not its rule. R11
|
|
||||||
> remains a deviation and must be justified on our own architecture, not on theirs.
|
|
||||||
|
|
||||||
**R11a [DEVIATION] — The refusal list.** With no solver, these are unsupportable and must be refused
|
|
||||||
rather than half-done: a second mate on an already-driven body; cycles (A→B, B→A); closed loops
|
|
||||||
(A→B, A→C, B→C); relations *between* mates (gear, belt, rack-and-pinion, screw coupling); **joint
|
|
||||||
limits**, which nothing can enforce without a solver; and **dragging a body to exercise a free DOF**,
|
|
||||||
which requires keeping the body on the allowed manifold. Motion analysis and animation follow from the
|
|
||||||
same lack. *Requirement:* none of these may appear in the UI as something that half-works.
|
|
||||||
|
|
||||||
**R12 [DEVIATION] — The mate graph is an acyclic forest rooted at fixed bodies.** A body reached by
|
|
||||||
no mate is fixed; cycles are refused. Same root cause as R11. *Test:* A→B, B→A rejected at creation.
|
|
||||||
|
|
||||||
**R13 [INDUSTRY] — Free DOF are preserved from the current placement, and the user is told.**
|
|
||||||
Behaviour already matches Onshape (A8); the telling does not. *Test:* the card for any type with
|
|
||||||
DOF > 0 says which motions remain and that dragging exercises them.
|
|
||||||
|
|
||||||
**R14 [INDUSTRY] — State what mirroring does to a connector.**
|
|
||||||
*Checked in the code:* `datum_frame` ends with a Gram-Schmidt forcing a right-handed frame
|
|
||||||
(`ds.x = Y.cross(Z)`), so a connector resolved on a mirrored body comes out **right-handed, not
|
|
||||||
mirror-imaged**. Z follows the mirrored face's outward normal, X follows a mirrored edge, handedness
|
|
||||||
is re-imposed. Defensible — a mate on the mirrored part still turns the way its type says — but it
|
|
||||||
means a mirrored sub-assembly is *not* the mirror image of the original in its rotation sense.
|
|
||||||
*Requirement:* document it and pin it with a test. *Why:* C10.
|
|
||||||
|
|
||||||
### Feedback — the part that actually removes confusion
|
|
||||||
|
|
||||||
**R15 [INDUSTRY] — Before Confirm, the card answers four questions in words.** Which body moves;
|
|
||||||
which way Z points on each connector; how many DOF remain; what the offset is measured from.
|
|
||||||
|
|
||||||
**R16 [INDUSTRY] — Draw both frames live, with Z distinguishable, and ghost the result.**
|
|
||||||
Two triads with Z rendered differently from X/Y (length, arrowhead, colour). *Why:* D3 — the fastest
|
|
||||||
way to make a convention unequivocal is to show it. *Test:* both Z directions are readable in a
|
|
||||||
screenshot.
|
|
||||||
|
|
||||||
**R17 [INDUSTRY] — Show the DOF budget per body.** "Body 2: 1 of 6 DOF free (rotation about Z)."
|
|
||||||
The most educational readout in any assembly system, and free to compute here — the type *is* the DOF
|
|
||||||
count. *Test:* the number changes when the type changes.
|
|
||||||
|
|
||||||
**R18 [DEVIATION] — Refuse loudly and name the alternative.** Where something is out of scope (a
|
|
||||||
second mate, a tangency, a gear ratio), say what is unsupported and what to do instead. Vendors do not
|
|
||||||
need this because their solvers accept the input. *Test:* no refusal message ends without a suggested
|
|
||||||
next action.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Minimal specification, and gap analysis
|
|
||||||
|
|
||||||
### The connector
|
|
||||||
|
|
||||||
```
|
|
||||||
MateConnector
|
|
||||||
body int required, ≥ 0 (R1)
|
|
||||||
origin_kind enum FaceCentroid | ArcCentre | EdgeMidpoint | Vertex (R9)
|
|
||||||
origin_ref topo ref face / edge / vertex index on that body
|
|
||||||
z_source implied by origin_kind: face normal, arc axis, edge tangent
|
|
||||||
roll_ref topo ref optional in-plane edge; else deterministic seed (R7)
|
|
||||||
roll_quarters int 0..3 stored quarter turns on top of the seed (R7, A6)
|
|
||||||
flip_z bool reverse Z at the connector
|
|
||||||
name string stable, user-visible (D4)
|
|
||||||
```
|
|
||||||
|
|
||||||
`flip_z` is a property of the **connector**, chosen once when it is made — not a per-mate
|
|
||||||
afterthought. Keeping connector-flip and mate-direction separate is what stops the "which flip do I
|
|
||||||
tick?" question.
|
|
||||||
|
|
||||||
### The mate
|
|
||||||
|
|
||||||
```
|
|
||||||
Mate
|
|
||||||
kind enum Fastened | Revolute | Slider | Cylindrical | Planar [| Ball] (R3)
|
|
||||||
fixed connector A — its body does not move
|
|
||||||
moving connector B — its body is driven (A5, C5)
|
|
||||||
direction enum Facing | Aligned (R5, R6)
|
|
||||||
offset mm along A's Z, measured A → B — state this in the label (C6)
|
|
||||||
angle deg about A's Z (R4)
|
|
||||||
```
|
|
||||||
|
|
||||||
Within one field of what exists.
|
|
||||||
|
|
||||||
### Gaps against today
|
|
||||||
|
|
||||||
Source of record: `CadDocument.hpp:26,247-252,298-310`; `CadDocument.cpp:1669` (`datum_frame`),
|
|
||||||
`:2961` (`apply_mate`), `:1302` (`add_mate`); `DesignPanel.cpp:2671-2709` (the Mate card).
|
|
||||||
|
|
||||||
| # | Gap | Severity | Ref |
|
|
||||||
|---|---|---|---|
|
|
||||||
| G1 | `PointWorld` connectors are not attached to a body and their X is a world constant | **High — data model** | A4/R1 |
|
|
||||||
| G2 | Origin is always the face centroid; no vertex / edge-midpoint / arc-centre snap | **High — expressiveness** | D1/R9 |
|
|
||||||
| G3 | No live preview of the two Z arrows or of the resulting placement | **High — this is the brief** | D3/R16 |
|
|
||||||
| G4 | Mate card is two abstract dropdowns; nothing says which body moves | High — charter + A5 | R15 |
|
|
||||||
| G5 | No joint-type inference from the picked geometry | Medium — feel | D2 |
|
|
||||||
| G6 | `add_mate` validates nothing — no one-mate-per-body, no cycle check | Medium | R11/R12 |
|
|
||||||
| G7 | No `Ball` type | Low | §3 |
|
|
||||||
| G8 | Re-clocking needs a typed angle; no 90° step control | Low, cheap | A6/R7 |
|
|
||||||
| G9 | Degenerate roll falls back silently | Low | C8/R8 |
|
|
||||||
| G10 | Connectors have no stable user-facing name | Low now, expensive later | D4 |
|
|
||||||
|
|
||||||
**Already aligned — do not "fix" these:** the five types and their DOF; the frame definition (A1);
|
|
||||||
Z as the joint axis (A2); superimpose-then-relax (A3); the fixed/moving asymmetry in the data model
|
|
||||||
(A5); DOF wording in the type list (A7); free-DOF preservation (A8); right-handed frames under mirror
|
|
||||||
(R14); and `en4`'s fix, which put roll derivation on the body where it belongs (C2).
|
|
||||||
|
|
||||||
**The pattern worth naming: the kernel is in good shape and the concept is under-explained.** Half the
|
|
||||||
requirements here are wording and drawing, not geometry. The two real engineering items are R9 (origin
|
|
||||||
candidates) and R11/R12 (the mate-graph rules).
|
|
||||||
|
|
||||||
### Expensive-to-retrofit decisions — get these right in the data model now
|
|
||||||
|
|
||||||
Changing any of these after documents exist in the wild costs a migration, not an edit.
|
|
||||||
|
|
||||||
1. **Topological reference stability.** Storing raw face/edge indices is brittle — editing a body
|
|
||||||
renumbers faces. Either persistent topology IDs, or store the named origin *kind* plus a
|
|
||||||
deterministic search that re-finds the same geometric intent on rebuild. The latter is cheaper and
|
|
||||||
probably sufficient here; it is also what makes R10's "error, never silent relocation" enforceable.
|
|
||||||
2. **Connector ownership** (R1). Remove `PointWorld` or bind it to a body. Do this first.
|
|
||||||
3. **Mate direction semantics** (R5/D1). Inverting the default rewrites the meaning of every saved
|
|
||||||
mate.
|
|
||||||
4. **Roll representation** (R7). "First usable edge" is better than world-X but still fragile. Store
|
|
||||||
an explicit roll reference plus quarter turns.
|
|
||||||
5. **Coordinate convention** — Z = joint axis, X = roll reference. Changing this after release
|
|
||||||
invalidates every mate.
|
|
||||||
6. **Units** — offset in mm, angle in degrees. Never change.
|
|
||||||
7. **Mirror handedness** (R14) — document the decision, do not let it stay an accident.
|
|
||||||
8. **Flat body index vs. a component tree.** Mates currently reference bodies in a flat vector. If
|
|
||||||
**sub-assemblies** are ever in scope, mates must reference nodes in a tree instead. Retrofitting
|
|
||||||
this is painful and it is the one item on this list not already implied elsewhere in the document —
|
|
||||||
**decide now whether nested assemblies are in scope.**
|
|
||||||
9. **Serialization field semantics.** Adding fields is easy; redefining `mate_flip` or
|
|
||||||
`coordsys_x_hint` is not.
|
|
||||||
10. **The one-mate-per-body rule** (R11). Enforce at creation. Relaxing it later by adding a solver is
|
|
||||||
straightforward; allowing many mates now and discovering later that they silently conflict is not.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8b. The visual shape of the connector — polarity and verse
|
|
||||||
|
|
||||||
Researched separately (2026-08-05) by downloading and **looking at** the vendors' own figures, not
|
|
||||||
by reading their prose. Files kept alongside this document in `doc/design/mate-connectors/`.
|
|
||||||
|
|
||||||
### What the systems actually draw
|
|
||||||
|
|
||||||
**Onshape** — verified from `planarfacemateconnectors.png`, `cylindricalmateconnectors.png`,
|
|
||||||
`linearedgemateconnectors.png`, `mateconnector-planarpoints.png`, `matepointiconLG.png`:
|
|
||||||
|
|
||||||
> **A small circle with one quadrant filled, plus three short coloured axis arms (X red, Y green,
|
|
||||||
> Z blue).**
|
|
||||||
|
|
||||||
Three parts, each doing one job:
|
|
||||||
|
|
||||||
| Element | What it says |
|
|
||||||
|---|---|
|
|
||||||
| The **circle** | "I am a frame, and this is my XY plane." |
|
|
||||||
| The **filled quadrant** | **The roll.** The shaded sector is the +X/+Y quadrant. |
|
|
||||||
| The **coloured arms** | The three axis directions, Z distinguished by colour. |
|
|
||||||
|
|
||||||
The quadrant is the cleverest part of the whole design and it is easy to miss. The figure
|
|
||||||
`matepointreorientsecondaryaxis.png` shows three connectors side by side with the quadrant in three
|
|
||||||
different rotations — **it is the live readout of "reorient secondary axis in 90° increments" (A6).**
|
|
||||||
One glyph element makes the otherwise-invisible clocking visible, and makes the 90° button's effect
|
|
||||||
legible before you commit. The toolbar icon `matepointiconLG.png` is that same circle-with-a-quadrant,
|
|
||||||
so the symbol is consistent from toolbar to viewport.
|
|
||||||
|
|
||||||
Candidate snap points, before you choose one, are drawn as **plain small white dots** on the model
|
|
||||||
(clear in `mateconnector-planarpoints.png`: dots at every corner and edge midpoint). Candidate and
|
|
||||||
committed are deliberately different weights — dots propose, the circle-and-triad commits.
|
|
||||||
|
|
||||||
**FreeCAD 1.0** — verbatim from the wiki: *"Connectors are local coordinate systems and are marked by
|
|
||||||
a symbol with three axes (X, Y, Z) and a circle representing the XY-plane."* Same core as Onshape —
|
|
||||||
circle plus triad — **without** the quadrant.
|
|
||||||
|
|
||||||
**Fusion 360** — the joint origin glyph, plus a documented icon language for *candidates*: *"A circle
|
|
||||||
denotes a vertex, and a triangle denotes a midpoint."* Shape encodes what kind of point it is.
|
|
||||||
|
|
||||||
**Convergent core:** *circle for the XY plane + coloured triad*. Onshape alone adds the roll quadrant.
|
|
||||||
|
|
||||||
### What none of them draw — and it is exactly what was asked for
|
|
||||||
|
|
||||||
**Nothing in any vendor's glyph says which connector is the reference and which one is about to
|
|
||||||
move.** Both ends of a mate are drawn identically. That is confusion C5 ("which part moves?") left
|
|
||||||
unsolved in the visual language, and it is why the honest recommendation earlier was a live ghost —
|
|
||||||
the ghost compensates for a glyph that does not carry the information.
|
|
||||||
|
|
||||||
So the two things asked for split cleanly, and only one of them is solved upstream:
|
|
||||||
|
|
||||||
- **Verse** (*verso* — which way it points): **solved**. Z has a colour and a direction.
|
|
||||||
- **Polarity** (which end receives, which end inserts; who is anchored, who travels): **unsolved
|
|
||||||
everywhere.** This is open ground, and getting it right is a genuine improvement rather than a
|
|
||||||
deviation to justify.
|
|
||||||
|
|
||||||
### Our starting point
|
|
||||||
|
|
||||||
**We draw nothing.** `resolve_datum_coordsys()` (`CadDocument.cpp:1749`) has exactly one consumer in
|
|
||||||
the entire tree — `McpControl.cpp:1310`, the agent socket. A mate connector is today visible only to
|
|
||||||
a program. The glyph is unbuilt, so there is no migration cost to designing it properly now.
|
|
||||||
|
|
||||||
### Proposed glyph: the magnet
|
|
||||||
|
|
||||||
Adopt Onshape's proven core, then add the missing polarity with a metaphor that carries its own
|
|
||||||
instructions.
|
|
||||||
|
|
||||||
```
|
|
||||||
▲ solid cone on +Z ONLY ← verse
|
|
||||||
|
|
|
||||||
────●──── ← the disc = XY plane, ● = exact origin
|
|
||||||
▨ quadrant filled ← roll / clocking, steps 90°
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rule 1 — verse: draw +Z and never −Z.** A single stem with a cone head, on the positive side only.
|
|
||||||
No stem below the disc. A double-headed axis is the one thing that guarantees the question gets asked;
|
|
||||||
an arrow that exists on one side only cannot be misread. Length is asymmetric on purpose.
|
|
||||||
|
|
||||||
**Rule 2 — roll: keep Onshape's quadrant.** Filled sector = the +X/+Y quadrant. It rotates in 90°
|
|
||||||
steps with the reorient control (A6/R7). This is aligned *and* it is the only in-glyph answer to
|
|
||||||
"where is X?", which matters because Fastened and Slider lock the clocking.
|
|
||||||
|
|
||||||
**Rule 3 — polarity: solid cone travels, open collar receives.**
|
|
||||||
- The **driven** connector (B, on the body that will move) draws a **solid filled cone** — the plug.
|
|
||||||
- The **fixed** connector (A) draws an **open ring / hollow cone outline** — the socket.
|
|
||||||
|
|
||||||
Same silhouette, so they read as a matched pair; opposite fill, so which one is about to jump is
|
|
||||||
answerable at a glance and without a legend. Plug-into-socket is the one mechanical metaphor every
|
|
||||||
user of this tool already has in their hands.
|
|
||||||
|
|
||||||
**Rule 4 — the pair reads as a magnet.** Draw a dashed line joining the two origins the moment both
|
|
||||||
are picked. Two poles, one field line. And because a magnet's north seeks a south, **"facing" becomes
|
|
||||||
the self-evident default** — which quietly settles open decision D1 (§9) on visual grounds rather than
|
|
||||||
on a convention nobody can look up. If the glyph looks like a magnet, nobody has to be told that two
|
|
||||||
faces which touch have opposed normals.
|
|
||||||
|
|
||||||
**Rule 5 — three states, three weights.**
|
|
||||||
|
|
||||||
| State | Drawing |
|
|
||||||
|---|---|
|
|
||||||
| **Candidate** (hover) | small dot only — Onshape's white dots; shape may encode kind, Fusion-style |
|
|
||||||
| **Picked** | full glyph: disc + quadrant + cone |
|
|
||||||
| **Degenerate roll** (C8/R8) | the quadrant is drawn **hollow/hatched** — "roll undefined, pick a direction" |
|
|
||||||
|
|
||||||
That last row is worth the trouble: it turns R8 from a message nobody reads into a mark you cannot
|
|
||||||
miss, and it costs one branch in the renderer.
|
|
||||||
|
|
||||||
**Rule 6 — do not reuse the existing triad.** The bed-centre world triad
|
|
||||||
(`DesignCanvas.cpp:65`, `set_axes_at_bed_center`) and the move gizmo are already three-coloured arrows.
|
|
||||||
The connector must not be a fourth set of RGB arrows or the viewport becomes unreadable. The disc and
|
|
||||||
the quadrant are what distinguish it; keep the arms short, and consider drawing only Z on the
|
|
||||||
committed glyph, with X/Y implied by the quadrant.
|
|
||||||
|
|
||||||
### Built and judged in the viewport, not in a mock
|
|
||||||
|
|
||||||
The browser mock that first accompanied this section was the wrong instrument and its proportions
|
|
||||||
were meaningless: **every gizmo in this codebase is sized in SCREEN PIXELS** via `upp = 1/zoom`
|
|
||||||
(`render_shell_gizmo` uses `15.0 * upp`, `render_hole_gizmo` `9.0 * upp` for its cube). A connector
|
|
||||||
is a symbol, not a part — it must not shrink with the model. Nothing about that is visible in SVG.
|
|
||||||
|
|
||||||
The glyph was therefore implemented and driven on the rig. Screenshots: `g-0*.png`, left in the workspace `artifacts/shots/` and not moved into the repo.
|
|
||||||
Five findings, none of which a mock could have produced:
|
|
||||||
|
|
||||||
**F1 — Three axis arms lose to one.** Rendered side by side (`ORCA_CAD_GLYPH=A` vs default), the
|
|
||||||
Onshape-style RGB trio crowds a 22 px disc: the arrowheads are as large as the disc, they bury the
|
|
||||||
gold quadrant, and at an oblique angle the three heads pile into a coloured smudge. Worse, **it is
|
|
||||||
indistinguishable from the move gizmo and the bed triad**, which are already RGB arrow trios in this
|
|
||||||
viewport. One-sided Z wins on evidence, not taste. (`g-01-zoom.png` vs `g-02-zoom.png`.)
|
|
||||||
|
|
||||||
**F2 — Polarity works, and colour does more of the work than fill.** A filled blue head against an
|
|
||||||
open grey outline head is readable instantly at 22 px (`g-03-zoom.png`). But the fill difference is
|
|
||||||
the *second* cue; the colour split carries it. Keep both — fill survives greyscale and colour-blind
|
|
||||||
palettes, colour survives small size.
|
|
||||||
|
|
||||||
**F3 — Depth off floats, depth on tears.** With `GL_DEPTH_TEST` off, connectors on faces pointing
|
|
||||||
*away* from the camera still drew their discs over the solid, so the part looked covered in frames
|
|
||||||
that were really on its back. Turning depth on fixed that and immediately caused **z-fighting**: the
|
|
||||||
disc is exactly coplanar with its face, and came out as a broken dotted arc. The fix is depth **on**
|
|
||||||
plus a sub-pixel lift along Z (`0.7 * upp`), scaled by `upp` so it never becomes a visible gap on
|
|
||||||
zoom-in. Both failure modes are in the images (`g-03` torn, `g-04` clean).
|
|
||||||
|
|
||||||
**F4 — The quadrant is the first thing to die at a grazing angle.** On a face seen nearly edge-on the
|
|
||||||
disc foreshortens to a sliver and the fan collapses into a blob (`g-01-zoom.png`, lower-right glyph).
|
|
||||||
The roll is exactly the information that is hardest to read when you most need it. Not yet solved —
|
|
||||||
see the open item below.
|
|
||||||
|
|
||||||
**F5 — Roll-undefined in red is too loud.** It works, but it makes the *least* important connector
|
|
||||||
the most eye-catching thing on screen. Amber, or the same grey with a hatched quadrant, is enough.
|
|
||||||
|
|
||||||
Also surfaced while testing, and unrelated to the glyph: `add_mate` accepted a mate between two
|
|
||||||
connectors **on the same body**, which is meaningless, and duly transformed the body relative to
|
|
||||||
itself. Concrete instance of gap G6.
|
|
||||||
|
|
||||||
**Still untested:** a true grazing view (the view-cube click missed), a connector on a curved face,
|
|
||||||
and behaviour when a connector overlaps the move gizmo. F4 is the open design question — the disc may
|
|
||||||
need to billboard its *quadrant* while keeping the disc in-plane, which is a compromise no surveyed
|
|
||||||
vendor makes and which should be tried before being adopted.
|
|
||||||
|
|
||||||
### What this costs
|
|
||||||
|
|
||||||
A renderer for `resolve_datum_coordsys()` — which does not exist and has to be written whatever glyph
|
|
||||||
is chosen — plus one dashed line and three fill states. No kernel work. It is the same piece of work
|
|
||||||
as G3 (live preview), and doing them together is what makes the mate card honest.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8c. The "faceted ridge dome" proposal — built, rendered, judged
|
|
||||||
|
|
||||||
A colleague proposed replacing the flat disc with an **asymmetric low-poly solid**: a faceted
|
|
||||||
prismatic wedge with a dominant longitudinal ridge that **slopes** from a tall steep back to a long
|
|
||||||
shallow front, plus a male protrusion / female pocket pair with a 0.2 mm clearance.
|
|
||||||
|
|
||||||
It was built rather than discussed. `faceted_ridge_key.scad` (this folder) (6 vertices, 7 faces),
|
|
||||||
verified as a closed manifold, exported through OpenSCAD, and flat-shaded from five directions with
|
|
||||||
`render_key.py` / `render_stl.py`. Sheets: `rk-sheet.png`, `cmp-sheet.png`.
|
|
||||||
|
|
||||||
### The verdict: the shape is right, the male/female polarity cue is not
|
|
||||||
|
|
||||||
**It solves F4, decisively.** The grazing view — where the flat disc dies, its quadrant collapsing to
|
|
||||||
a blob — is the view where this shape is *most* legible: the tall back and long shallow front are
|
|
||||||
unmistakable in silhouette. At a grazing angle the silhouette IS the information, and this solid's
|
|
||||||
silhouette is maximally informative there. That is a real, evidence-backed win over what is currently
|
|
||||||
in the code.
|
|
||||||
|
|
||||||
**Down the mating axis (+Z) it also reads well**, which matters because that is the natural viewing
|
|
||||||
direction when you are looking at a face you intend to mate.
|
|
||||||
|
|
||||||
**One degenerate view, and it is not the one I predicted.** I expected the ±X views (along the ridge)
|
|
||||||
to be silhouette-ambiguous, resolved only by shading. Wrong: front and back are clearly *different* —
|
|
||||||
the front shows several facets, the back is a **single flat featureless triangle**. So they are not
|
|
||||||
confusable, but the view from directly behind the tall end tells you nothing about roll or slope.
|
|
||||||
A second blind spot remains untested: from below the base, where the protrusion is hidden behind its
|
|
||||||
own face.
|
|
||||||
|
|
||||||
**The female half fails, and much harder than expected.** Rendered with flat shading and no outlines —
|
|
||||||
the honest test, since a viewport draws no black edges — a recessed pocket is *invisible*: iso and
|
|
||||||
grazing show a plain block with a hairline; straight down the axis shows a **completely blank
|
|
||||||
rectangle**. The interior faces are lit almost identically to the top face and are occluded by the rim
|
|
||||||
from most angles. As a polarity cue, male/female therefore works in exactly one direction and returns
|
|
||||||
nothing in the other.
|
|
||||||
|
|
||||||
> **Conclusion: do not overload shape with all three jobs.** Let the solid carry **verse and roll**,
|
|
||||||
> where it is excellent, and carry **polarity on a second channel** — colour plus the filled/open head
|
|
||||||
> that already tested well at 22 px (F2). Drawing the fixed connector as an outline/wireframe of the
|
|
||||||
> same solid is the variant worth trying; drawing it as a pocket is not.
|
|
||||||
|
|
||||||
### Two premises in the brief are wrong
|
|
||||||
|
|
||||||
**"Avoid curved surfaces to optimise rendering computations / rapid mesh processing."** Not a reason
|
|
||||||
for a viewport glyph. There are 2–20 connectors on screen, the renderer pushes `GLModel` triangles
|
|
||||||
directly, and it performs no CSG or mesh processing at all. **The real argument for flat facets is
|
|
||||||
legibility**: hard normals give distinct value steps between adjacent facets, and the renders confirm
|
|
||||||
that is exactly what makes the shape readable from an arbitrary angle. Keep the constraint, fix the
|
|
||||||
justification. (For a *printed* part the original justification is sound for a different reason: flat
|
|
||||||
facets slice without the stair-stepping a tessellated curve produces.)
|
|
||||||
|
|
||||||
**"0.2 mm clearance for smooth mechanical mating."** Meaningless for a glyph. A symbol mates with
|
|
||||||
nothing, and every gizmo here is sized in screen pixels via `upp`, so a millimetre tolerance has no
|
|
||||||
referent. This is the strongest signal that **the brief was written for a physical printed part**,
|
|
||||||
not for a viewport symbol — as are "scannable" and "mechanical mating". See the open question below.
|
|
||||||
|
|
||||||
### Two defects the build caught that discussion would not have
|
|
||||||
|
|
||||||
1. **The flank quads are not planar.** Written as `[0,3,5,4]` and `[1,4,5,2]` the base edge and the
|
|
||||||
ridge edge are skew, so the four corners do not share a plane — my own first draft asserted the
|
|
||||||
opposite in a comment. Left as quads, the tessellator picks the fold direction, the "flat facet"
|
|
||||||
promise is broken by an unspecified crease, and two exporters can disagree about the shape. Fixed
|
|
||||||
by triangulating explicitly (7 faces, Euler 6 − 11 + 7 = 2).
|
|
||||||
2. **The pocket punched through its own plate.** A 4.5 mm key against a 3 mm demo plate gives a
|
|
||||||
through-hole, not a pocket. Minimum stock = height + clearance + pocket depth + a wall.
|
|
||||||
|
|
||||||
Also worth recording: the first female render was misleading because the debug renderer outlined
|
|
||||||
*every* triangle, so a flat top face triangulated by CGAL looked like a faceted dome. The instrument
|
|
||||||
lied before the geometry did. Conclusions were only drawn after outlines were removed.
|
|
||||||
|
|
||||||
### Second opinion, and the one disagreement worth resolving
|
|
||||||
|
|
||||||
Kimi reviewed the proposal independently and **rejected it for the viewport**. It agreed on the two
|
|
||||||
wrong premises, agreed the female pocket is unreadable, and added the useful framing that a
|
|
||||||
screen-constant symbol and a model-constant part feature are two different design spaces that cannot
|
|
||||||
be served by one geometry. It also noted correctly that there is **no single scalar** that removes
|
|
||||||
ambiguity from every view: you need one asymmetry in the base plane (for top-down roll) and one out
|
|
||||||
of plane (the ridge slope, for front/back). Our base is scalene, so it has both.
|
|
||||||
|
|
||||||
Its central objection was numeric and testable: *"at 22 px with 6–8 facets each facet is 3–7 px wide,
|
|
||||||
that is at the aliasing limit … minimum useful size is roughly 32–48 px, which is not compatible with
|
|
||||||
a 22 px screen-constant symbol."* My own renders were ~300 px, so the claim was unaddressed by my
|
|
||||||
evidence and would have killed the concept if true.
|
|
||||||
|
|
||||||
**Rendered at 22, 32 and 48 px (`size-test.png`), it is false for this shape.** At 22 px all three
|
|
||||||
views still read: the grazing view shows the tall back and shallow front unmistakably, and the
|
|
||||||
down-axis view keeps a strong dark/light split. The reason Kimi's arithmetic does not apply is that
|
|
||||||
this solid presents only **four or five large facets with high value contrast**, not eight small ones —
|
|
||||||
the silhouette does most of the work, and silhouettes survive downsampling far better than facet
|
|
||||||
detail does.
|
|
||||||
|
|
||||||
*Honest limit on that result:* the test renderer has no anti-aliasing, no perspective, one directional
|
|
||||||
light, and no background. Readable at 22 px against white is not the same as readable at 22 px on top
|
|
||||||
of a shaded gold part next to the move gizmo. That case still needs the rig.
|
|
||||||
|
|
||||||
**Where I do not follow Kimi:** its recommendation is to **billboard** the existing flat glyph so it
|
|
||||||
never turns edge-on. That kills F4 by construction, but a billboarded frame cannot show the frame's
|
|
||||||
orientation *in place* — which is the entire reason the disc is a disc and not a dot — and it is what
|
|
||||||
no surveyed CAD system does; Onshape, Fusion and FreeCAD all draw the frame in the geometry. Worth
|
|
||||||
prototyping as an option, not worth adopting on argument.
|
|
||||||
|
|
||||||
### Open question for Tommaso
|
|
||||||
|
|
||||||
**Is this a viewport glyph or a printable alignment feature?** The vertex logic is identical either
|
|
||||||
way; only the units and the clearance change, and the `.scad` file states both readings. But the
|
|
||||||
answer decides whether `clr`/`depth` are real millimetres or meaningless, and whether the geometry
|
|
||||||
scales with the model or stays screen-constant. The brief's own language points at "physical", the
|
|
||||||
conversation it arrived in points at "glyph".
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Decisions for you
|
|
||||||
|
|
||||||
**D1 — Invert the default direction to Facing?** [DEVIATION, R5]
|
|
||||||
It changes the meaning of every stored document containing a mate. Options: (a) invert and migrate,
|
|
||||||
writing `direction=Aligned` where `mate_flip` was false; (b) invert only for new mates and store
|
|
||||||
`direction` explicitly from now on. (b) is safer and costs one field. Note this project has taken one
|
|
||||||
such semantic hit knowingly before — the `en4` fix — and the golden fixture survived, so the
|
|
||||||
migration path is a known quantity. **If G3 (live preview) lands first, this matters much less.**
|
|
||||||
|
|
||||||
**D2 — How far to take origin candidates?** [R9]
|
|
||||||
Four kinds is the Fusion-aligned recommendation. Two (face centroid + arc centre) would cover "sit on
|
|
||||||
a face" and "go down a hole" — most printed-part assembly — at a third of the work. Where do you want
|
|
||||||
to stop?
|
|
||||||
|
|
||||||
**D3 — Ball mate: in or out?**
|
|
||||||
In four of five frame-based systems, so including it is the aligned choice. Out is defensible for
|
|
||||||
printable mechanical parts. Cheap either way — align origins, leave orientation free. Kimi's review
|
|
||||||
argued **out**: a true ball joint is hard to print and hard to use without a roll reference, and a
|
|
||||||
Fastened connector at the ball centre approximates it.
|
|
||||||
|
|
||||||
**D3a — Should Planar be dropped?** [dissent worth recording]
|
|
||||||
Kimi's independent review recommended **removing Planar** and shipping four types, on the grounds that
|
|
||||||
"slide on a flat surface" is rarely how printed mechanisms work — you usually want a rail or a hinge —
|
|
||||||
and that Planar is the type most likely to confuse a user who expected "put this flat on that" and got
|
|
||||||
a part free to slide. It further ranked the honest minimum as **three**: Fastened, Revolute, Slider,
|
|
||||||
with Cylindrical useful and decomposable.
|
|
||||||
**I do not agree, and the reason is alignment.** Planar appears in every frame-based system surveyed,
|
|
||||||
it is a genuine lower pair, it is already implemented and tested, and removing it is a document-format
|
|
||||||
change made in exchange for nothing. The confusion Kimi names is real but it is a *feedback* problem —
|
|
||||||
it is exactly what R17 (show the DOF budget) and R13 (say that free DOF are preserved) exist to fix.
|
|
||||||
Recorded here because it is a legitimate reading of the same evidence and the call is yours.
|
|
||||||
|
|
||||||
**D4 — Is refusing a second mate per body acceptable?** [DEVIATION, R11 — the big one]
|
|
||||||
It is the honest consequence of having no solver, and it is what makes the tool predictable. But **no
|
|
||||||
mainstream system behaves this way**, so it is the point where an experienced user's intuition will
|
|
||||||
break. It means a part cannot be constrained by two independent relationships — "in this hole *and*
|
|
||||||
resting on this shoulder" must be expressed by placing one connector correctly rather than by two
|
|
||||||
mates. If that trade is unacceptable, the answer is a solver, and the scope of this document changes
|
|
||||||
entirely.
|
|
||||||
|
|
||||||
There is a strong argument that the trade is not merely acceptable but *correct for this product*:
|
|
||||||
the Design tab lives inside a slicer, and most of its users are positioning parts for printing rather
|
|
||||||
than building working mechanisms. For layout-and-export, tree-order composition is genuinely enough,
|
|
||||||
and adding a solver to look like Onshape would buy complexity nobody asked for. The rule to publish is
|
|
||||||
then simple and defensible: **one mate per moving body, acyclic, no relations between mates** — with
|
|
||||||
R18's loud refusals carrying the honesty.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sources
|
|
||||||
|
|
||||||
**Onshape** — [Mate Connector](https://cad.onshape.com/help/Content/PartStudio/mate_connector.htm) ·
|
|
||||||
[Mates](https://cad.onshape.com/help/Content/Assembly/mates.htm) ·
|
|
||||||
[Fastened](https://cad.onshape.com/help/Content/Assembly/fastened_mate.htm) ·
|
|
||||||
[Revolute](https://cad.onshape.com/help/Content/Assembly/revolute_mate.htm) ·
|
|
||||||
[Slider](https://cad.onshape.com/help/Content/Assembly/slider_mate.htm) ·
|
|
||||||
[Cylindrical](https://cad.onshape.com/help/Content/Assembly/cylindrical_mate.htm) ·
|
|
||||||
[Planar](https://cad.onshape.com/help/Content/Assembly/planar_mate.htm) ·
|
|
||||||
[Ball](https://cad.onshape.com/help/Content/Assembly/ball_mate.htm) ·
|
|
||||||
[Parallel](https://cad.onshape.com/help/Content/Assembly/parallel_mate.htm) ·
|
|
||||||
[Tangent](https://cad.onshape.com/help/Content/Assembly/tangent_mate.htm) ·
|
|
||||||
[Pin Slot](https://cad.onshape.com/help/Content/Assembly/pin_slot_mate.htm) ·
|
|
||||||
[5 things you can do with mate connectors in Part Studios](https://www.onshape.com/en/resource-center/tech-tips/tech-tip-5-things-you-can-do-with-mate-connectors-in-onshape-part-studios)
|
|
||||||
|
|
||||||
**Onshape forum** — [The concept behind Mates Z Axes](https://forum.onshape.com/discussion/22828/the-concept-behind-mates-z-axes) (C1/D3) ·
|
|
||||||
[Implicit mate connectors act differently than explicit ones](https://forum.onshape.com/discussion/15736/implicit-mate-connectors-act-differently-than-explicit-ones) (C4) ·
|
|
||||||
[Efficiently set mate connectors](https://forum.onshape.com/discussion/13133/efficiently-set-mate-connectors)
|
|
||||||
|
|
||||||
**Fusion 360** — [Joint types](https://help.autodesk.com/cloudhelp/ENU/Fusion-Assemble/files/GUID-8818AE31-958A-4A59-989B-9875A174C67A.htm) ·
|
|
||||||
[Joint origins](https://help.autodesk.com/view/fusion360/ENU/?guid=ASM-JOINT-ORIGIN) ·
|
|
||||||
[Joints vs. Mates in Fusion](https://www.autodesk.com/products/fusion-360/blog/joints-mates-moving-fusion/) ·
|
|
||||||
[Joint tips — snap points and Ctrl cycling](https://mgfx.co.za/blog/engineering-manufacturing-design/fusion-360-joint-tips/)
|
|
||||||
|
|
||||||
**Inventor** — [Create Joints Reference](https://help.autodesk.com/cloudhelp/2026/ENU/Inventor-Help/files/GUID-6AA68E8F-7C97-4806-8483-3941DE915E70.htm) ·
|
|
||||||
[Use Joint to define and manage relationships](https://knowledge.autodesk.com/support/inventor-products/learn-explore/caas/CloudHelp/cloudhelp/2014/ENU/Inventor/files/GUID-21DC3336-5C51-42C1-90FB-4299CD66E0C6-htm.html) (type inference, D2)
|
|
||||||
|
|
||||||
**FreeCAD 1.0** — [Assembly Workbench](https://wiki.freecad.org/Assembly_Workbench) ·
|
|
||||||
[Fixed Joint properties](https://wiki.freecad.org/Assembly_CreateJointFixed)
|
|
||||||
|
|
||||||
**Creo** — [About Predefined Constraint Sets](https://support.ptc.com/help/creo/creo_pma/r12/usascii/assembly/asm/About_Predefined_Constraint_Sets.html)
|
|
||||||
|
|
||||||
**Siemens NX** — [Assembly constraints](https://learnnx.com/lesson/siemens-nx-assemblies-assembly-constraints/)
|
|
||||||
|
|
||||||
**SOLIDWORKS** — [Mate References](https://help.solidworks.com/2025/English/SolidWorks/sldworks/c_Mate_References_Overview_SWassy.htm) ·
|
|
||||||
[Creating and using mate references](https://blogs.solidworks.com/tech/2019/07/creating-and-using-mate-references.html)
|
|
||||||
|
|
||||||
**Theory** — [Hervé, The Lie group of rigid body displacements, a fundamental tool for mechanism design](https://www.sciencedirect.com/science/article/abs/pii/S0094114X98000512) ·
|
|
||||||
[Joint kinematics — the six lower pairs and their DOF](https://erc-bpgc.github.io/handbook/mechanical/Joint%20Kinematics/) ·
|
|
||||||
[ISO 10303-105 — Kinematics (STEP integrated resource)](https://www.iso.org/standard/78589.html)
|
|
||||||
|
|
||||||
**Internal** — `en4` (closed 2026-07-26, fixes C2 here) · `CadDocument.cpp:1669`
|
|
||||||
`datum_frame` · `CadDocument.cpp:2961` `apply_mate` · `CadDocument.cpp:1302` `add_mate`
|
|
||||||
|
|
||||||
**Second opinion** — an independent review by Kimi Code (2026-08-05) contributed the
|
|
||||||
vendors-ship-both caveat (§1), the explicit-dropdown option for origin choice (D1), the expanded
|
|
||||||
refusal list (R11a), the retrofit list (§8), and the dissents recorded at D3/D3a. One of its claims —
|
|
||||||
that Onshape mandates *"exactly one Mate between any two instances"* — **was checked against the
|
|
||||||
source and is wrong**; the correction is recorded at R11 because it is a misreading that would
|
|
||||||
otherwise turn our largest deviation into a false agreement.
|
|
||||||
|
Before Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,30 +0,0 @@
|
|||||||
// Emitted by doc/design/mate-connectors/emit_glyph_table.py from bear.step — do not hand-edit.
|
|
||||||
// Normalised to the part's bounding span and centred: the renderer scales by one radius.
|
|
||||||
static const Vec2d kBearOutline[] = { // 12 verts, RDP eps 0.030, CCW
|
|
||||||
{+0.3842, +0.3294}, {+0.3156, +0.4002}, {+0.2424, +0.3294},
|
|
||||||
{-0.2524, +0.3294}, {-0.3377, +0.3877}, {-0.3693, +0.3298},
|
|
||||||
{-0.3256, +0.2631}, {-0.4893, -0.3337}, {-0.3960, -0.4002},
|
|
||||||
{+0.4151, -0.4002}, {+0.5000, -0.3154}, {+0.3156, +0.2631},
|
|
||||||
};
|
|
||||||
static const Vec2d kBearChin[] = { // the CHIN BAR, flat. The muzzle is relief — see kBearCrest.
|
|
||||||
{-0.2682, -0.3578}, {+0.2628, -0.3578}, {+0.2237, -0.1786},
|
|
||||||
};
|
|
||||||
// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (wi3z).
|
|
||||||
static const Vec3d kBearMarks[] = {
|
|
||||||
{-0.1997, +0.1760, +0.0590},
|
|
||||||
{+0.1947, +0.1760, +0.0590},
|
|
||||||
{+0.2797, +0.0760, +0.0380},
|
|
||||||
};
|
|
||||||
// THE MUZZLE, lifted off the mesh: a tapered wedge, base quad + crest edge, 6 facets.
|
|
||||||
// This is the only feature standing along +Z and the only one still legible edge-on.
|
|
||||||
static const double kBearPlateZ = +0.0360;
|
|
||||||
static const Vec2d kBearSnoutBase[] = { // CCW from the nose end
|
|
||||||
{-0.0727, -0.2417},
|
|
||||||
{+0.0630, -0.2417},
|
|
||||||
{+0.0259, +0.1939},
|
|
||||||
{-0.0356, +0.1939},
|
|
||||||
};
|
|
||||||
static const Vec3d kBearCrest[] = { // nose (tall) -> tail (short)
|
|
||||||
{-0.0048, -0.1793, +0.2073},
|
|
||||||
{-0.0048, +0.1605, +0.1279},
|
|
||||||
};
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -1,299 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Mate connector glyph — polarity and verse</title>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--ground: #eceef1;
|
|
||||||
--panel: #f8f9fb;
|
|
||||||
--panel-edge: #d3d8df;
|
|
||||||
--ink: #171a1f;
|
|
||||||
--ink-soft: #5a626e;
|
|
||||||
--ink-faint: #8b93a0;
|
|
||||||
--viewport: #9aa0a8; /* the grey a CAD viewport actually is */
|
|
||||||
--viewport-2: #7f858d;
|
|
||||||
--axis-z: #2f6fed;
|
|
||||||
--axis-x: #d94a3d;
|
|
||||||
--axis-y: #3aa757;
|
|
||||||
--quadrant: #e8a317;
|
|
||||||
--anchor: #6b7280;
|
|
||||||
--driven: #2f6fed;
|
|
||||||
--warn: #c2410c;
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--ground: #14171c;
|
|
||||||
--panel: #1b1f26;
|
|
||||||
--panel-edge: #2b313a;
|
|
||||||
--ink: #e8eaee;
|
|
||||||
--ink-soft: #a6aeba;
|
|
||||||
--ink-faint: #6e7784;
|
|
||||||
--viewport: #4a5058;
|
|
||||||
--viewport-2: #3a3f46;
|
|
||||||
--axis-z: #6ea2ff;
|
|
||||||
--axis-x: #ff7a6d;
|
|
||||||
--axis-y: #5fd07f;
|
|
||||||
--quadrant: #ffc247;
|
|
||||||
--anchor: #9aa3b0;
|
|
||||||
--driven: #6ea2ff;
|
|
||||||
--warn: #fb923c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
:root[data-theme="dark"] {
|
|
||||||
--ground:#14171c; --panel:#1b1f26; --panel-edge:#2b313a; --ink:#e8eaee;
|
|
||||||
--ink-soft:#a6aeba; --ink-faint:#6e7784; --viewport:#4a5058; --viewport-2:#3a3f46;
|
|
||||||
--axis-z:#6ea2ff; --axis-x:#ff7a6d; --axis-y:#5fd07f; --quadrant:#ffc247;
|
|
||||||
--anchor:#9aa3b0; --driven:#6ea2ff; --warn:#fb923c;
|
|
||||||
}
|
|
||||||
:root[data-theme="light"] {
|
|
||||||
--ground:#eceef1; --panel:#f8f9fb; --panel-edge:#d3d8df; --ink:#171a1f;
|
|
||||||
--ink-soft:#5a626e; --ink-faint:#8b93a0; --viewport:#9aa0a8; --viewport-2:#7f858d;
|
|
||||||
--axis-z:#2f6fed; --axis-x:#d94a3d; --axis-y:#3aa757; --quadrant:#e8a317;
|
|
||||||
--anchor:#6b7280; --driven:#2f6fed; --warn:#c2410c;
|
|
||||||
}
|
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
body {
|
|
||||||
margin: 0; padding: 40px 24px 72px;
|
|
||||||
background: var(--ground); color: var(--ink);
|
|
||||||
font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
||||||
}
|
|
||||||
.wrap { max-width: 1000px; margin: 0 auto; display: flex; flex-direction: column; gap: 28px; }
|
|
||||||
header { display: flex; flex-direction: column; gap: 6px; }
|
|
||||||
h1 { font-size: 26px; line-height: 1.25; margin: 0; letter-spacing: -0.01em; text-wrap: balance; }
|
|
||||||
.sub { color: var(--ink-soft); max-width: 62ch; margin: 0; }
|
|
||||||
.eyebrow {
|
|
||||||
font-size: 11px; letter-spacing: 0.12em; text-transform: uppercase;
|
|
||||||
color: var(--ink-faint); font-weight: 600;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase;
|
|
||||||
color: var(--ink-faint); margin: 16px 0 0; font-weight: 600;
|
|
||||||
}
|
|
||||||
.row { display: flex; flex-wrap: wrap; gap: 16px; }
|
|
||||||
.card {
|
|
||||||
background: var(--panel); border: 1px solid var(--panel-edge);
|
|
||||||
border-radius: 10px; padding: 18px; flex: 1 1 220px; min-width: 220px;
|
|
||||||
display: flex; flex-direction: column; gap: 10px;
|
|
||||||
}
|
|
||||||
.card.wide { flex: 1 1 100%; }
|
|
||||||
.stage { display: flex; align-items: center; justify-content: center; padding: 4px 0; }
|
|
||||||
.name { font-weight: 650; font-size: 15px; }
|
|
||||||
.note { color: var(--ink-soft); font-size: 13.5px; margin: 0; }
|
|
||||||
.k { color: var(--ink); font-weight: 600; }
|
|
||||||
table { border-collapse: collapse; width: 100%; font-size: 14px; }
|
|
||||||
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--panel-edge); vertical-align: top; }
|
|
||||||
th { color: var(--ink-faint); font-weight: 600; font-size: 12px; letter-spacing: 0.06em; text-transform: uppercase; }
|
|
||||||
code { font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--ink-soft); }
|
|
||||||
.legend { display: flex; flex-wrap: wrap; gap: 14px; font-size: 13px; color: var(--ink-soft); }
|
|
||||||
.swatch { display: inline-flex; align-items: center; gap: 7px; }
|
|
||||||
.dot { width: 11px; height: 11px; border-radius: 50%; display: inline-block; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="wrap">
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<div class="eyebrow">Orca Design · assembly</div>
|
|
||||||
<h1>Mate connector glyph — polarity and verse</h1>
|
|
||||||
<p class="sub">
|
|
||||||
Onshape's core (disc + roll quadrant + Z arrow) is adopted unchanged because it is proven and
|
|
||||||
aligned. The addition is <span class="k">polarity</span> — which connector is anchored and
|
|
||||||
which one travels — which no surveyed CAD system encodes in its glyph.
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<h2>The three jobs of the glyph</h2>
|
|
||||||
<div class="row">
|
|
||||||
<div class="card">
|
|
||||||
<div class="stage">
|
|
||||||
<svg width="150" height="130" viewBox="-75 -95 150 130" aria-label="Disc with origin dot">
|
|
||||||
<ellipse cx="0" cy="0" rx="42" ry="17" fill="none" stroke="var(--ink-soft)" stroke-width="2.5"/>
|
|
||||||
<circle cx="0" cy="0" r="3.6" fill="var(--ink)"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="name">Disc — the XY plane</div>
|
|
||||||
<p class="note">Says “I am a frame, and this is the plane I sit in.” The dot is the exact origin.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="stage">
|
|
||||||
<svg width="150" height="130" viewBox="-75 -95 150 130" aria-label="Disc with one quadrant filled">
|
|
||||||
<path d="M0,0 L42,0 A42,17 0 0 1 0,17 Z" fill="var(--quadrant)" opacity="0.9"/>
|
|
||||||
<ellipse cx="0" cy="0" rx="42" ry="17" fill="none" stroke="var(--ink-soft)" stroke-width="2.5"/>
|
|
||||||
<circle cx="0" cy="0" r="3.6" fill="var(--ink)"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="name">Quadrant — the roll</div>
|
|
||||||
<p class="note">
|
|
||||||
The filled sector is the +X/+Y quadrant. It steps 90° with the reorient control, so the
|
|
||||||
clocking that Fastened and Slider lock is <em>visible</em> before you commit.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="stage">
|
|
||||||
<svg width="150" height="130" viewBox="-75 -95 150 130" aria-label="Z arrow drawn only upward">
|
|
||||||
<path d="M0,0 L42,0 A42,17 0 0 1 0,17 Z" fill="var(--quadrant)" opacity="0.9"/>
|
|
||||||
<ellipse cx="0" cy="0" rx="42" ry="17" fill="none" stroke="var(--ink-soft)" stroke-width="2.5"/>
|
|
||||||
<line x1="0" y1="0" x2="0" y2="-58" stroke="var(--axis-z)" stroke-width="3.5" stroke-linecap="round"/>
|
|
||||||
<polygon points="0,-80 -9.5,-56 9.5,-56" fill="var(--axis-z)"/>
|
|
||||||
<circle cx="0" cy="0" r="3.6" fill="var(--ink)"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="name">Arrow — the verse</div>
|
|
||||||
<p class="note">
|
|
||||||
Drawn on <span class="k">+Z only</span>. Nothing below the disc. A double-headed axis is what
|
|
||||||
makes people ask which way it points; a one-sided arrow cannot be misread.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Polarity — the part nobody else draws</h2>
|
|
||||||
<div class="row">
|
|
||||||
<div class="card">
|
|
||||||
<div class="stage">
|
|
||||||
<svg width="170" height="150" viewBox="-85 -105 170 150" aria-label="Fixed connector, open collar">
|
|
||||||
<path d="M0,0 L42,0 A42,17 0 0 1 0,17 Z" fill="var(--quadrant)" opacity="0.55"/>
|
|
||||||
<ellipse cx="0" cy="0" rx="42" ry="17" fill="none" stroke="var(--anchor)" stroke-width="2.5"/>
|
|
||||||
<line x1="0" y1="0" x2="0" y2="-56" stroke="var(--anchor)" stroke-width="3" stroke-linecap="round"/>
|
|
||||||
<polygon points="0,-80 -9.5,-56 9.5,-56" fill="none" stroke="var(--anchor)" stroke-width="3" stroke-linejoin="round"/>
|
|
||||||
<ellipse cx="0" cy="-56" rx="9.5" ry="3.6" fill="none" stroke="var(--anchor)" stroke-width="2.2"/>
|
|
||||||
<circle cx="0" cy="0" r="3.6" fill="var(--anchor)"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="name">Fixed — the socket</div>
|
|
||||||
<p class="note">
|
|
||||||
Hollow head, muted colour. This body <span class="k">does not move</span>. It receives.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="stage">
|
|
||||||
<svg width="170" height="150" viewBox="-85 -105 170 150" aria-label="Driven connector, solid cone">
|
|
||||||
<path d="M0,0 L42,0 A42,17 0 0 1 0,17 Z" fill="var(--quadrant)" opacity="0.95"/>
|
|
||||||
<ellipse cx="0" cy="0" rx="42" ry="17" fill="none" stroke="var(--driven)" stroke-width="2.5"/>
|
|
||||||
<line x1="0" y1="0" x2="0" y2="-58" stroke="var(--driven)" stroke-width="3.5" stroke-linecap="round"/>
|
|
||||||
<polygon points="0,-80 -9.5,-56 9.5,-56" fill="var(--driven)"/>
|
|
||||||
<circle cx="0" cy="0" r="3.6" fill="var(--driven)"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="name">Driven — the plug</div>
|
|
||||||
<p class="note">
|
|
||||||
Solid head, active colour. This body <span class="k">is the one that jumps</span>. It inserts.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="stage">
|
|
||||||
<svg width="170" height="150" viewBox="-85 -105 170 150" aria-label="Degenerate roll, hatched quadrant">
|
|
||||||
<defs>
|
|
||||||
<pattern id="hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
|
|
||||||
<line x1="0" y1="0" x2="0" y2="6" stroke="var(--warn)" stroke-width="2"/>
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<path d="M0,0 L42,0 A42,17 0 0 1 0,17 Z" fill="url(#hatch)" opacity="0.85"/>
|
|
||||||
<ellipse cx="0" cy="0" rx="42" ry="17" fill="none" stroke="var(--warn)" stroke-width="2.5" stroke-dasharray="5 4"/>
|
|
||||||
<line x1="0" y1="0" x2="0" y2="-58" stroke="var(--axis-z)" stroke-width="3.5" stroke-linecap="round"/>
|
|
||||||
<polygon points="0,-80 -9.5,-56 9.5,-56" fill="var(--axis-z)"/>
|
|
||||||
<circle cx="0" cy="0" r="3.6" fill="var(--ink)"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="name">Roll undefined</div>
|
|
||||||
<p class="note">
|
|
||||||
Hatched quadrant, dashed disc: a circular face or a seam gave no usable direction. Says
|
|
||||||
“pick a direction” without a dialog.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>The pair reads as a magnet</h2>
|
|
||||||
<div class="card wide">
|
|
||||||
<div class="stage">
|
|
||||||
<svg width="620" height="230" viewBox="-310 -120 620 230" aria-label="Two connectors facing each other on two plates">
|
|
||||||
<!-- lower plate (fixed) -->
|
|
||||||
<path d="M-260,52 L-60,10 L60,44 L-140,86 Z" fill="var(--viewport)" stroke="var(--viewport-2)" stroke-width="1.5"/>
|
|
||||||
<!-- upper plate (driven) -->
|
|
||||||
<path d="M-60,-96 L140,-138 L260,-104 L60,-62 Z" fill="var(--viewport)" stroke="var(--viewport-2)" stroke-width="1.5" opacity="0.55"/>
|
|
||||||
|
|
||||||
<!-- dashed field line between origins -->
|
|
||||||
<line x1="-100" y1="48" x2="100" y2="-79" stroke="var(--ink-faint)" stroke-width="2" stroke-dasharray="7 6"/>
|
|
||||||
|
|
||||||
<!-- FIXED connector, pointing up (+Z out of the lower plate) -->
|
|
||||||
<g transform="translate(-100,48)">
|
|
||||||
<path d="M0,0 L38,0 A38,15 0 0 1 0,15 Z" fill="var(--quadrant)" opacity="0.5"/>
|
|
||||||
<ellipse cx="0" cy="0" rx="38" ry="15" fill="none" stroke="var(--anchor)" stroke-width="2.4"/>
|
|
||||||
<line x1="0" y1="0" x2="0" y2="-48" stroke="var(--anchor)" stroke-width="3" stroke-linecap="round"/>
|
|
||||||
<polygon points="0,-70 -9,-48 9,-48" fill="none" stroke="var(--anchor)" stroke-width="3" stroke-linejoin="round"/>
|
|
||||||
<ellipse cx="0" cy="-48" rx="9" ry="3.4" fill="none" stroke="var(--anchor)" stroke-width="2"/>
|
|
||||||
<circle cx="0" cy="0" r="3.4" fill="var(--anchor)"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- DRIVEN connector, pointing down (+Z out of the upper plate's underside) -->
|
|
||||||
<g transform="translate(100,-79) rotate(180)">
|
|
||||||
<path d="M0,0 L38,0 A38,15 0 0 1 0,15 Z" fill="var(--quadrant)" opacity="0.9"/>
|
|
||||||
<ellipse cx="0" cy="0" rx="38" ry="15" fill="none" stroke="var(--driven)" stroke-width="2.4"/>
|
|
||||||
<line x1="0" y1="0" x2="0" y2="-50" stroke="var(--driven)" stroke-width="3.4" stroke-linecap="round"/>
|
|
||||||
<polygon points="0,-70 -9,-48 9,-48" fill="var(--driven)"/>
|
|
||||||
<circle cx="0" cy="0" r="3.4" fill="var(--driven)"/>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<text x="-100" y="102" text-anchor="middle" font-size="13" fill="var(--ink-soft)">fixed · receives</text>
|
|
||||||
<text x="100" y="-100" text-anchor="middle" font-size="13" fill="var(--ink-soft)">driven · inserts</text>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<p class="note">
|
|
||||||
Two arrows nose to nose. Because a magnet's north seeks a south, <span class="k">“facing” is the
|
|
||||||
self-evident default</span> — which settles open decision D1 on visual grounds instead of a
|
|
||||||
convention nobody can look up. Nothing has to be remembered: the picture is the rule.
|
|
||||||
The dashed line is what makes the two glyphs read as one object.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>States</h2>
|
|
||||||
<div class="card wide">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>State</th><th>Drawing</th><th>Why</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><span class="k">Candidate</span> (hover)</td>
|
|
||||||
<td>small dot only</td>
|
|
||||||
<td>Onshape draws plain white dots at every corner and midpoint. Dots propose; the full glyph commits.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class="k">Picked</span></td>
|
|
||||||
<td>disc + quadrant + cone</td>
|
|
||||||
<td>The committed frame, with roll and verse both readable.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><span class="k">Roll undefined</span></td>
|
|
||||||
<td>hatched quadrant, dashed disc</td>
|
|
||||||
<td>Turns requirement R8 from a message nobody reads into a mark you cannot miss.</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Constraints on the drawing</h2>
|
|
||||||
<div class="card wide">
|
|
||||||
<p class="note">
|
|
||||||
<span class="k">Do not make it a fourth RGB triad.</span> The bed-centre world triad
|
|
||||||
(<code>DesignCanvas.cpp:65</code>) and the move gizmo are already three coloured arrows. The disc
|
|
||||||
and the quadrant are what tell a connector apart from those — keep the arms short, and consider
|
|
||||||
drawing only Z on the committed glyph, with X and Y implied by the quadrant.
|
|
||||||
</p>
|
|
||||||
<div class="legend">
|
|
||||||
<span class="swatch"><i class="dot" style="background:var(--quadrant)"></i> roll quadrant</span>
|
|
||||||
<span class="swatch"><i class="dot" style="background:var(--axis-z)"></i> Z / driven</span>
|
|
||||||
<span class="swatch"><i class="dot" style="background:var(--anchor)"></i> fixed</span>
|
|
||||||
<span class="swatch"><i class="dot" style="background:var(--warn)"></i> roll undefined</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Does the connector pair let two hosts sit COPLANAR, or does it hold them apart?
|
|
||||||
#
|
|
||||||
# The male's flat back is the plane Y=0 and all its relief rises to +Y. So Y=0 is the natural
|
|
||||||
# mating datum: everything the male adds lives on one side of it. The test below builds two dummy
|
|
||||||
# host plates that meet on that plane -- one with the male FUSED on, one with the cavity CUT in --
|
|
||||||
# and measures whether they touch, interfere, or stand apart.
|
|
||||||
#
|
|
||||||
# It also emits the artifact that makes this work in practice: a CUTTER solid (the male grown by
|
|
||||||
# the clearance) that you subtract from any host. A standalone female block cannot keep two hosts
|
|
||||||
# coplanar, because its own floor material stands between them; a cavity can.
|
|
||||||
#
|
|
||||||
# Run: /snap/bin/freecad.cmd coplanar_test.py
|
|
||||||
|
|
||||||
import os
|
|
||||||
import FreeCAD as App
|
|
||||||
import Part
|
|
||||||
from FreeCAD import Vector
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
MALE = os.path.join(HERE, "bear.step")
|
|
||||||
CLEAR = 0.20
|
|
||||||
|
|
||||||
male = Part.Shape(); male.read(MALE); male = male.Solids[0]
|
|
||||||
bb = male.BoundBox
|
|
||||||
print(f"male relief: Y {bb.YMin:.3f} .. {bb.YMax:.3f} -> datum plane Y=0, all relief on +Y")
|
|
||||||
|
|
||||||
# the flat back face, and proof it is the whole silhouette sitting on Y=0
|
|
||||||
back = max((f for f in male.Faces
|
|
||||||
if abs(f.CenterOfMass.y) < 1e-6 and abs(abs(f.normalAt(0, 0).y) - 1) < 1e-6),
|
|
||||||
key=lambda f: f.Area)
|
|
||||||
print(f"back face : {back.Area:.1f} mm2 on Y=0 -- this is the contact surface")
|
|
||||||
|
|
||||||
# ---- the cutter: the male grown by the clearance, poking 0.2 mm proud so the boolean is clean
|
|
||||||
cutter = male.makeOffsetShape(CLEAR, 1e-6, False, False, 0, 2, False).Solids[0]
|
|
||||||
cb = cutter.BoundBox
|
|
||||||
print(f"cutter : Y {cb.YMin:.3f} .. {cb.YMax:.3f}, {cutter.Volume/1000:.2f} cm3")
|
|
||||||
|
|
||||||
# ---- two dummy hosts meeting on Y = 0
|
|
||||||
W, H = 120.0, 100.0
|
|
||||||
hostA = Part.makeBox(W, 10.0, H, Vector(-W/2, -10.0, -15.0)) # occupies Y -10..0
|
|
||||||
hostB = Part.makeBox(W, 30.0, H, Vector(-W/2, 0.0, -15.0)) # occupies Y 0..30
|
|
||||||
|
|
||||||
partA = hostA.fuse(male) # male stands proud of A's face
|
|
||||||
partB = hostB.cut(cutter) # cavity sunk into B from its face
|
|
||||||
|
|
||||||
print(f"\npart A (host + male) : {partA.Volume/1000:.2f} cm3")
|
|
||||||
print(f"part B (host - cutter) : {partB.Volume/1000:.2f} cm3")
|
|
||||||
|
|
||||||
# ---- the question ------------------------------------------------------------------
|
|
||||||
inter = partA.common(partB)
|
|
||||||
iv = inter.Volume if inter.Solids else 0.0
|
|
||||||
gap = partA.distToShape(partB)[0]
|
|
||||||
print(f"\nRESULT interference A vs B : {iv:.6f} mm3 (0 = they do not collide)")
|
|
||||||
print(f"RESULT closest approach : {gap:.4f} mm (0 = the host faces are touching)")
|
|
||||||
|
|
||||||
# are the two host faces actually on the same plane?
|
|
||||||
fa = [f for f in partA.Faces if abs(f.CenterOfMass.y) < 1e-9 and abs(abs(f.normalAt(0,0).y)-1) < 1e-6]
|
|
||||||
fb = [f for f in partB.Faces if abs(f.CenterOfMass.y) < 1e-9 and abs(abs(f.normalAt(0,0).y)-1) < 1e-6]
|
|
||||||
print(f"RESULT A has {len(fa)} face(s) lying exactly on Y=0, total {sum(f.Area for f in fa):.1f} mm2")
|
|
||||||
print(f"RESULT B has {len(fb)} face(s) lying exactly on Y=0, total {sum(f.Area for f in fb):.1f} mm2")
|
|
||||||
print("RESULT -> the hosts meet on Y=0: COPLANAR" if fa and fb and iv < 1e-3
|
|
||||||
else "RESULT -> NOT coplanar")
|
|
||||||
|
|
||||||
doc = App.newDocument("Cutter")
|
|
||||||
o = doc.addObject("Part::Feature", "BearConnector_Cutter"); o.Shape = cutter
|
|
||||||
doc.recompute()
|
|
||||||
Part.export([o], os.path.join(HERE, "BearConnector_Cutter.step"))
|
|
||||||
print(f"\nwrote BearConnector_Cutter.step -- subtract this from any host to get the socket")
|
|
||||||
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,140 +0,0 @@
|
|||||||
// Faceted ridge key — asymmetric male/female alignment feature, flat facets only.
|
|
||||||
//
|
|
||||||
// 6 vertices, 7 faces, one closed manifold. Euler check: V - E + F = 6 - 11 + 7 = 2.
|
|
||||||
// No spheres, no cylinders, no splines, no fillets.
|
|
||||||
//
|
|
||||||
// THE FLANKS ARE TRIANGULATED EXPLICITLY, and that is not cosmetic. Written as quads
|
|
||||||
// [0,3,5,4] and [1,4,5,2] they are NOT planar — the base edge and the ridge edge are
|
|
||||||
// skew, so the four corners do not share a plane. A checker caught this after the first
|
|
||||||
// draft claimed the opposite. Left as quads, the tessellator picks the fold direction for
|
|
||||||
// you, which means the "flat facet" promise is broken by an unspecified crease and two
|
|
||||||
// exporters can disagree about the shape. Splitting them here fixes the crease at
|
|
||||||
// back-bottom -> front-ridge, which keeps the rear peak's triangle large and clean.
|
|
||||||
//
|
|
||||||
// FRAME CONVENTION (matches the CAD mate connector it is derived from):
|
|
||||||
// +Z the mating axis — the feature protrudes along it
|
|
||||||
// +X the roll reference — the ridge runs along it, low end forward
|
|
||||||
// +Y completes the right-handed frame
|
|
||||||
//
|
|
||||||
// WHAT BREAKS WHICH SYMMETRY
|
|
||||||
// rotational about Z ....... the ridge (elongation along X)
|
|
||||||
// 180 deg about Z .......... the ridge SLOPE: tall steep back, long shallow front
|
|
||||||
// mirror across XZ ......... deliberately NOT broken. Handedness is fixed by convention,
|
|
||||||
// so +Y is implied once Z and X are known. Breaking it would
|
|
||||||
// add a facet and buy nothing.
|
|
||||||
//
|
|
||||||
// KNOWN AMBIGUITY, stated rather than hidden: viewed exactly ALONG the ridge (+/-X,
|
|
||||||
// orthographic), the silhouette is the same isoceles triangle from front and back. Front
|
|
||||||
// and back are then distinguished by SHADING only — the long shallow front face catches
|
|
||||||
// light differently from the steep back face. If the target renderer is flat-shaded with a
|
|
||||||
// single headlight, verify this case before committing to the shape.
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- parameters
|
|
||||||
L = 12.0; // overall length along the ridge (X)
|
|
||||||
W = 4.0; // half-width at the BACK
|
|
||||||
tf = 0.45; // front taper: front half-width = W * tf
|
|
||||||
H = 4.5; // peak height at the rear <-- the single dimension controlling asymmetry
|
|
||||||
pr = 0.22; // rear ridge position, fraction of L from the back
|
|
||||||
pf = 0.62; // front ridge position, fraction of L from the back
|
|
||||||
hf = 0.35; // front ridge height, fraction of H
|
|
||||||
|
|
||||||
// Clearance is a PHYSICAL quantity and only means anything if this is a printed part.
|
|
||||||
// See the note at the bottom: for a viewport glyph it is meaningless.
|
|
||||||
clr = 0.20; // per-face clearance, mm
|
|
||||||
depth = 0.40; // extra pocket depth so the male never bottoms out before it seats
|
|
||||||
|
|
||||||
Wf = W * tf;
|
|
||||||
xr0 = -L/2 + L * pr;
|
|
||||||
xr1 = -L/2 + L * pf;
|
|
||||||
Hf = H * hf;
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- geometry
|
|
||||||
// Vertex order is fixed and referenced by the face table; do not reorder.
|
|
||||||
// 0 back-left 1 back-right 2 front-right 3 front-left
|
|
||||||
// 4 REAR PEAK (tall) 5 front ridge (low)
|
|
||||||
function ridge_pts(l, w, wf, h, hfr, x0, x1) = [
|
|
||||||
[-l/2, -w, 0 ], // 0
|
|
||||||
[-l/2, w, 0 ], // 1
|
|
||||||
[ l/2, wf, 0 ], // 2
|
|
||||||
[ l/2, -wf, 0 ], // 3
|
|
||||||
[ x0, 0, h ], // 4 rear peak
|
|
||||||
[ x1, 0, hfr] // 5 front ridge, low
|
|
||||||
];
|
|
||||||
|
|
||||||
// OpenSCAD wants each face wound CLOCKWISE seen from OUTSIDE. The right-hand-rule
|
|
||||||
// outward-normal (CCW) form is given in the comment for anyone porting to STL/OCC,
|
|
||||||
// where the opposite convention is the usual one.
|
|
||||||
RIDGE_FACES = [
|
|
||||||
[3, 2, 1, 0], // base (CCW-outward: [0,1,2,3]) planar, all z=0
|
|
||||||
[1, 4, 0], // back (CCW-outward: [0,4,1]) steep
|
|
||||||
[5, 3, 0], // flank -Y a (CCW-outward: [0,3,5])
|
|
||||||
[4, 5, 0], // flank -Y b (CCW-outward: [0,5,4])
|
|
||||||
[5, 4, 1], // flank +Y a (CCW-outward: [1,4,5])
|
|
||||||
[2, 5, 1], // flank +Y b (CCW-outward: [1,5,2])
|
|
||||||
[5, 2, 3] // front (CCW-outward: [3,2,5]) long, shallow
|
|
||||||
];
|
|
||||||
|
|
||||||
module ridge_key(l = L, w = W, wf = Wf, h = H, hfr = Hf, x0 = xr0, x1 = xr1) {
|
|
||||||
polyhedron(points = ridge_pts(l, w, wf, h, hfr, x0, x1),
|
|
||||||
faces = RIDGE_FACES,
|
|
||||||
convexity = 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// MALE: the protrusion, nominal size.
|
|
||||||
module ridge_key_male() { ridge_key(); }
|
|
||||||
|
|
||||||
// FEMALE: the pocket. Grown by `clr` on every side and sunk `depth` deeper.
|
|
||||||
//
|
|
||||||
// HONEST LIMITATION: this grows the key by scaling its defining dimensions, which is NOT a
|
|
||||||
// true uniform surface offset — on the shallow front face the normal clearance comes out
|
|
||||||
// smaller than `clr`, because that face is far from perpendicular to every axis it is
|
|
||||||
// scaled along. A true offset needs minkowski() with a small cube, which is exact and slow,
|
|
||||||
// or an explicit per-face plane push, which is exact and fiddly. For a keying feature whose
|
|
||||||
// job is angular registration rather than a press fit, the approximation is the right trade
|
|
||||||
// — but do not quote this pocket as holding 0.2 mm everywhere, because it does not.
|
|
||||||
module ridge_key_female() {
|
|
||||||
translate([0, 0, -depth])
|
|
||||||
ridge_key(l = L + 2*clr,
|
|
||||||
w = W + clr,
|
|
||||||
wf = Wf + clr,
|
|
||||||
h = H + clr + depth,
|
|
||||||
hfr = Hf + clr + depth,
|
|
||||||
x0 = xr0,
|
|
||||||
x1 = xr1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- demo
|
|
||||||
// Left: the male key on its plate. Right: the plate with the pocket cut.
|
|
||||||
PLATE = [30, 18, 3];
|
|
||||||
|
|
||||||
module plate_with_male() {
|
|
||||||
translate([-PLATE[0]/2, -PLATE[1]/2, -PLATE[2]]) cube(PLATE);
|
|
||||||
ridge_key_male();
|
|
||||||
}
|
|
||||||
|
|
||||||
module plate_with_female() {
|
|
||||||
difference() {
|
|
||||||
translate([-PLATE[0]/2, -PLATE[1]/2, -PLATE[2]]) cube(PLATE);
|
|
||||||
ridge_key_female();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
translate([-20, 0, 0]) plate_with_male();
|
|
||||||
translate([ 20, 0, 0]) plate_with_female();
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------- note on the two readings
|
|
||||||
// This file is written for the PHYSICAL reading: a printable alignment key, where `clr` and
|
|
||||||
// `depth` are real millimetres and flat facets genuinely help — they slice without the
|
|
||||||
// stair-stepping a tessellated curve produces, and they print without support on the
|
|
||||||
// shallow front face.
|
|
||||||
//
|
|
||||||
// If the intent is instead the VIEWPORT GLYPH for a CAD mate connector, then:
|
|
||||||
// - `clr` and `depth` are meaningless: a symbol does not mate with anything;
|
|
||||||
// - all dimensions must become SCREEN PIXELS scaled by upp = 1/zoom, because every gizmo
|
|
||||||
// in that viewport is screen-constant and must not shrink with the model;
|
|
||||||
// - "low-poly for rendering performance" is not a real reason at ~2-20 glyphs per frame.
|
|
||||||
// The real reason to keep flat facets there is LEGIBILITY: hard normals give distinct
|
|
||||||
// value steps between facets, and that is what lets a 22-px solid read as an oriented
|
|
||||||
// object instead of a grey blob.
|
|
||||||
// The vertex logic above is identical under both readings. Only the units and the clearance
|
|
||||||
// change.
|
|
||||||
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,226 +0,0 @@
|
|||||||
solid OpenSCAD_Model
|
|
||||||
facet normal 1 -0 0
|
|
||||||
outer loop
|
|
||||||
vertex 15 -9 0
|
|
||||||
vertex 15 9 -8
|
|
||||||
vertex 15 9 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 1 0 0
|
|
||||||
outer loop
|
|
||||||
vertex 15 9 -8
|
|
||||||
vertex 15 -9 0
|
|
||||||
vertex 15 -9 -8
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex 15 9 0
|
|
||||||
vertex 5.3246 1.63218 0
|
|
||||||
vertex 15 -9 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex 15 9 0
|
|
||||||
vertex -4.79494 3.42759 0
|
|
||||||
vertex 5.3246 1.63218 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex 15 9 0
|
|
||||||
vertex -5.97725 3.87059 0
|
|
||||||
vertex -4.79494 3.42759 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex -5.97725 3.87059 0
|
|
||||||
vertex -15 9 0
|
|
||||||
vertex -5.97725 -3.87059 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex -15 9 0
|
|
||||||
vertex -5.97725 3.87059 0
|
|
||||||
vertex 15 9 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex 5.3246 -1.63218 0
|
|
||||||
vertex 15 -9 0
|
|
||||||
vertex 5.3246 1.63218 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex -4.79494 -3.42759 0
|
|
||||||
vertex 15 -9 0
|
|
||||||
vertex 5.3246 -1.63218 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex -5.97725 -3.87059 0
|
|
||||||
vertex 15 -9 0
|
|
||||||
vertex -4.79494 -3.42759 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex -5.97725 -3.87059 0
|
|
||||||
vertex -15 -9 0
|
|
||||||
vertex 15 -9 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex -15 -9 0
|
|
||||||
vertex -5.97725 -3.87059 0
|
|
||||||
vertex -15 9 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0 -1
|
|
||||||
outer loop
|
|
||||||
vertex -15 -9 -8
|
|
||||||
vertex 15 9 -8
|
|
||||||
vertex 15 -9 -8
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0 0 -1
|
|
||||||
outer loop
|
|
||||||
vertex 15 9 -8
|
|
||||||
vertex -15 -9 -8
|
|
||||||
vertex -15 9 -8
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -1 0 0
|
|
||||||
outer loop
|
|
||||||
vertex -15 -9 -8
|
|
||||||
vertex -15 9 0
|
|
||||||
vertex -15 9 -8
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -1 -0 0
|
|
||||||
outer loop
|
|
||||||
vertex -15 9 0
|
|
||||||
vertex -15 -9 -8
|
|
||||||
vertex -15 -9 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 1 -0
|
|
||||||
outer loop
|
|
||||||
vertex 15 9 -8
|
|
||||||
vertex -15 9 0
|
|
||||||
vertex 15 9 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 1 0
|
|
||||||
outer loop
|
|
||||||
vertex -15 9 0
|
|
||||||
vertex 15 9 -8
|
|
||||||
vertex -15 9 -8
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 -1 0
|
|
||||||
outer loop
|
|
||||||
vertex -15 -9 -8
|
|
||||||
vertex 15 -9 0
|
|
||||||
vertex -15 -9 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 -1 -0
|
|
||||||
outer loop
|
|
||||||
vertex 15 -9 0
|
|
||||||
vertex -15 -9 -8
|
|
||||||
vertex 15 -9 -8
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex -6.2 4.2 -0.4
|
|
||||||
vertex 6.2 -2 -0.4
|
|
||||||
vertex 6.2 2 -0.4
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0 0 1
|
|
||||||
outer loop
|
|
||||||
vertex 6.2 -2 -0.4
|
|
||||||
vertex -6.2 4.2 -0.4
|
|
||||||
vertex -6.2 -4.2 -0.4
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0.873667 0 -0.486524
|
|
||||||
outer loop
|
|
||||||
vertex -5.97725 -3.87059 0
|
|
||||||
vertex -6.2 4.2 -0.4
|
|
||||||
vertex -5.97725 3.87059 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal 0.873667 0 -0.486524
|
|
||||||
outer loop
|
|
||||||
vertex -6.2 4.2 -0.4
|
|
||||||
vertex -5.97725 -3.87059 0
|
|
||||||
vertex -6.2 -4.2 -0.4
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0.107146 0.603912 -0.789816
|
|
||||||
outer loop
|
|
||||||
vertex 6.2 -2 -0.4
|
|
||||||
vertex -4.79494 -3.42759 0
|
|
||||||
vertex 5.3246 -1.63218 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0.107147 0.603918 -0.789812
|
|
||||||
outer loop
|
|
||||||
vertex -4.79494 -3.42759 0
|
|
||||||
vertex 6.2 -2 -0.4
|
|
||||||
vertex -6.2 -4.2 -0.4
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0.304068 0.811519 -0.498978
|
|
||||||
outer loop
|
|
||||||
vertex -4.79494 -3.42759 0
|
|
||||||
vertex -6.2 -4.2 -0.4
|
|
||||||
vertex -5.97725 -3.87059 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0.304068 -0.811519 -0.498978
|
|
||||||
outer loop
|
|
||||||
vertex -5.97725 3.87059 0
|
|
||||||
vertex -6.2 4.2 -0.4
|
|
||||||
vertex -4.79494 3.42759 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0.107146 -0.603912 -0.789816
|
|
||||||
outer loop
|
|
||||||
vertex -4.79494 3.42759 0
|
|
||||||
vertex 6.2 2 -0.4
|
|
||||||
vertex 5.3246 1.63218 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0.107147 -0.603918 -0.789812
|
|
||||||
outer loop
|
|
||||||
vertex 6.2 2 -0.4
|
|
||||||
vertex -4.79494 3.42759 0
|
|
||||||
vertex -6.2 4.2 -0.4
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0.415603 0 -0.909546
|
|
||||||
outer loop
|
|
||||||
vertex 5.3246 -1.63218 0
|
|
||||||
vertex 6.2 2 -0.4
|
|
||||||
vertex 6.2 -2 -0.4
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
facet normal -0.415603 0 -0.909546
|
|
||||||
outer loop
|
|
||||||
vertex 6.2 2 -0.4
|
|
||||||
vertex 5.3246 -1.63218 0
|
|
||||||
vertex 5.3246 1.63218 0
|
|
||||||
endloop
|
|
||||||
endfacet
|
|
||||||
endsolid OpenSCAD_Model
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
// Female half alone, for the legibility test: is a recessed faceted pocket readable in a
|
|
||||||
// shaded view, or does a concave feature just read as a dark hole with no orientation?
|
|
||||||
use <faceted_ridge_key.scad>
|
|
||||||
|
|
||||||
// The plate must be THICKER than the key is tall, or the "pocket" is a through-hole. The
|
|
||||||
// first version used 3 mm against a 4.5 mm key and cut straight through — caught only by
|
|
||||||
// rendering it. Minimum stock = H + clearance + pocket depth + a wall to print against.
|
|
||||||
PLATE = [30, 18, 8];
|
|
||||||
difference() {
|
|
||||||
translate([-PLATE[0]/2, -PLATE[1]/2, -PLATE[2]]) cube(PLATE);
|
|
||||||
ridge_key_female();
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Measure the assembled fit between the supplied male and the generated female.
|
|
||||||
# This is the number that matters: the minimum gap in the seated position.
|
|
||||||
# Run: /snap/bin/freecad.cmd fit_check.py
|
|
||||||
import os
|
|
||||||
import Part
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
male = Part.Shape(); male.read(os.path.join(HERE, "bear.step"))
|
|
||||||
fem = Part.Shape(); fem.read(os.path.join(HERE, "BearConnector_Female.step"))
|
|
||||||
male, fem = male.Solids[0], fem.Solids[0]
|
|
||||||
|
|
||||||
d = male.distToShape(fem)
|
|
||||||
print(f"RESULT minimum gap male<->female, seated: {d[0]:.4f} mm (design clearance 0.20)")
|
|
||||||
|
|
||||||
c = male.common(fem)
|
|
||||||
print(f"RESULT interference volume: {(c.Volume if c.Solids else 0.0):.6f} mm3")
|
|
||||||
|
|
||||||
p = d[1][0][0]
|
|
||||||
print(f"RESULT tightest point on the male: ({p.x:.2f}, {p.y:.2f}, {p.z:.2f})")
|
|
||||||
print(f"RESULT male {male.Volume/1000:.2f} cm3 / female {fem.Volume/1000:.2f} cm3")
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,99 +0,0 @@
|
|||||||
"""Render the SIMPLIFIED glyph exactly as render_mate_face() draws it — x0kd.
|
|
||||||
|
|
||||||
This is the panel the study was missing. simplify_study.py measured a FLAT outline and
|
|
||||||
relief_sheet.py measured the FULL 1508-facet part; neither showed the simplified glyph WITH its
|
|
||||||
relief, which is what the code actually draws and the only thing that answers "is the snout still
|
|
||||||
protruding". Same facet list, same painter order, same camera-fixed lambert as the C++.
|
|
||||||
"""
|
|
||||||
import math, os
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
T = open(os.path.join(HERE, "bear_glyph_table.h")).read()
|
|
||||||
def grab(name, n):
|
|
||||||
body = T.split(name + "[] = {")[1].split("};")[0]
|
|
||||||
body = "\n".join(l.split("//")[0] for l in body.splitlines())
|
|
||||||
out = []
|
|
||||||
for tok in body.replace("\n", " ").split("},"):
|
|
||||||
tok = tok.strip().lstrip("{").strip()
|
|
||||||
if not tok: continue
|
|
||||||
v = [float(x) for x in tok.replace("{", "").split(",")[:n]]
|
|
||||||
if len(v) == n: out.append(tuple(v))
|
|
||||||
return out
|
|
||||||
OUT = grab("kBearOutline", 2)
|
|
||||||
CHIN = grab("kBearChin", 2) # NB: this table entry is the CHIN BAR, not the snout
|
|
||||||
MARKS = grab("kBearMarks", 3)
|
|
||||||
CREST = grab("kBearCrest", 3)
|
|
||||||
SBASE = grab("kBearSnoutBase", 2)
|
|
||||||
PLATE = float(T.split("kBearPlateZ = ")[1].split(";")[0])
|
|
||||||
|
|
||||||
|
|
||||||
def facets():
|
|
||||||
F = []
|
|
||||||
n = len(OUT)
|
|
||||||
for i in range(n): # plate sides -> the grazing silhouette
|
|
||||||
a, b = OUT[i], OUT[(i+1) % n]
|
|
||||||
F.append(([(a[0],a[1],0.0),(b[0],b[1],0.0),(b[0],b[1],PLATE),(a[0],a[1],PLATE)], "body", True))
|
|
||||||
F.append(([(x,y,PLATE) for x,y in OUT], "body", True)) # plate top
|
|
||||||
zm = PLATE + 0.004
|
|
||||||
for cx,cy,r in MARKS: # eyes + cheek dot
|
|
||||||
F.append(([(cx+r*math.cos(2*math.pi*i/12), cy+r*math.sin(2*math.pi*i/12), zm) for i in range(12)], "mark", False))
|
|
||||||
F.append(([(x,y,zm) for x,y in CHIN], "mark", False)) # chin bar
|
|
||||||
A, B = CREST # THE MUZZLE: base quad + crest
|
|
||||||
nl=(SBASE[0][0],SBASE[0][1],PLATE); nr=(SBASE[1][0],SBASE[1][1],PLATE)
|
|
||||||
tr=(SBASE[2][0],SBASE[2][1],PLATE); tl=(SBASE[3][0],SBASE[3][1],PLATE)
|
|
||||||
F += [([nl,tl,B,A],"body",True), # left flank
|
|
||||||
([nr,A,B,tr],"body",True), # right flank
|
|
||||||
([nl,A,nr],"body",True), # nose cap, sloping because the base overhangs the crest
|
|
||||||
([tr,B,tl],"body",True)] # tail cap
|
|
||||||
return F
|
|
||||||
FACETS = facets()
|
|
||||||
|
|
||||||
BODY=(0.42,0.46,0.52); MARK=(0.126,0.138,0.156)
|
|
||||||
def render(px, elev_deg, ss=8):
|
|
||||||
S=px*ss; a=math.radians(elev_deg); ca,sa=math.cos(a),math.sin(a)
|
|
||||||
# camera orbits down; the connector's +Z (relief) tips toward the horizon
|
|
||||||
xf=lambda p:(p[0], p[1]*sa + p[2]*ca, -p[1]*ca + p[2]*sa)
|
|
||||||
light=(-0.70,0.30,0.45)
|
|
||||||
img=Image.new("RGB",(S,S),(24,27,32)); d=ImageDraw.Draw(img)
|
|
||||||
tris=[]
|
|
||||||
for pts,kind,shade in FACETS:
|
|
||||||
q=[xf(p) for p in pts]
|
|
||||||
tris.append((sum(v[2] for v in q)/len(q), q, kind, shade))
|
|
||||||
tris.sort(key=lambda t:t[0]) # far first
|
|
||||||
for _,q,kind,shade in tris:
|
|
||||||
(x0,y0,z0),(x1,y1,z1),(x2,y2,z2)=q[0],q[1],q[2]
|
|
||||||
ux,uy,uz=x1-x0,y1-y0,z1-z0; vx,vy,vz=x2-x0,y2-y0,z2-z0
|
|
||||||
nx,ny,nz=uy*vz-uz*vy, uz*vx-ux*vz, ux*vy-uy*vx
|
|
||||||
nn=math.sqrt(nx*nx+ny*ny+nz*nz) or 1.0
|
|
||||||
nx,ny,nz=nx/nn,ny/nn,nz/nn
|
|
||||||
if nz<0: nx,ny,nz=-nx,-ny,-nz
|
|
||||||
base=BODY if kind=="body" else MARK
|
|
||||||
k=(0.42+0.58*max(0.0,nx*light[0]+ny*light[1]+nz*light[2])) if shade else 1.0
|
|
||||||
col=tuple(min(255,int(255*c*k)) for c in base)
|
|
||||||
d.polygon([(S/2+p[0]*S*0.92, S/2-p[1]*S*0.92) for p in q], fill=col)
|
|
||||||
return img.resize((px,px), Image.LANCZOS)
|
|
||||||
|
|
||||||
SIZES=[22,32,48]; ELEVS=[(90,"flat on"),(47,"47"),(16,"16"),(6,"6")]
|
|
||||||
pad,cell=8,58
|
|
||||||
W=pad+len(SIZES)*len(ELEVS)*cell+pad; H=pad+cell+pad
|
|
||||||
sheet=Image.new("RGB",(W,H),(24,27,32))
|
|
||||||
for ci,(e,_) in enumerate(ELEVS):
|
|
||||||
for si,px in enumerate(SIZES):
|
|
||||||
g=render(px,e)
|
|
||||||
sheet.paste(g, (pad+(ci*len(SIZES)+si)*cell+(cell-px)//2, pad+(cell-px)//2))
|
|
||||||
sheet.resize((W*2,H*2), Image.NEAREST).save(os.path.join(HERE,"glyph-preview.png"))
|
|
||||||
|
|
||||||
# how much of the glyph is the snout: render with and without the tent and diff
|
|
||||||
def render_no_tent(px, elev):
|
|
||||||
global FACETS
|
|
||||||
keep=FACETS; FACETS=FACETS[:-4]
|
|
||||||
try: return render(px, elev)
|
|
||||||
finally: FACETS=keep
|
|
||||||
print(f"{'elev':>8} {'lit px@32':>10} {'snout px':>9} {'snout share':>12}")
|
|
||||||
for e,_ in ELEVS:
|
|
||||||
a=render(32,e); b=render_no_tent(32,e)
|
|
||||||
la=sum(1 for p in a.get_flattened_data() if p!=(24,27,32))
|
|
||||||
diff=sum(1 for p,q in zip(a.get_flattened_data(), b.get_flattened_data()) if p!=q)
|
|
||||||
print(f"{e:>8} {la:>10} {diff:>9} {100.0*diff/max(1,la):>11.1f}%")
|
|
||||||
print("WROTE glyph-preview.png")
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
# Mate-connector glyph probe — built as REAL solids on REAL mechanical geometry,
|
|
||||||
# so the shape can be judged in a 3D viewport instead of in a browser mock.
|
|
||||||
#
|
|
||||||
# Four polarity treatments, side by side on one bracket:
|
|
||||||
# A Onshape baseline ...... ring + roll quadrant + three short axis arms
|
|
||||||
# B solid cone ............ ring + quadrant + one-sided Z arrow, filled head (driven)
|
|
||||||
# C hollow collar ......... ring + quadrant + one-sided Z arrow, shell head (fixed)
|
|
||||||
# D pin / cup ............. polarity by RELIEF: a raised pin vs a sunk cup
|
|
||||||
#
|
|
||||||
# D is the one that only a 3D test can settle: in a shaded viewport, solid-vs-hollow is a
|
|
||||||
# weak cue that depends on angle and lighting, while convex-vs-concave is a strong one --
|
|
||||||
# and male/female is the mechanical language for polarity anyway.
|
|
||||||
#
|
|
||||||
# Scale note: in the real viewport gizmos are screen-constant (~15-40 px via upp = 1/zoom).
|
|
||||||
# At a zoom where a 60 mm part fills ~600 px, 40 px is about 4 mm, so R = 4.5 mm here.
|
|
||||||
|
|
||||||
import FreeCAD as App
|
|
||||||
import FreeCADGui as Gui
|
|
||||||
import Part
|
|
||||||
from FreeCAD import Vector
|
|
||||||
|
|
||||||
DOC = "GlyphProbe"
|
|
||||||
for d in list(App.listDocuments()):
|
|
||||||
App.closeDocument(d)
|
|
||||||
doc = App.newDocument(DOC)
|
|
||||||
|
|
||||||
R = 4.5 # disc radius, the module everything scales from
|
|
||||||
GOLD = (0.93, 0.66, 0.09)
|
|
||||||
BLUE = (0.18, 0.44, 0.93)
|
|
||||||
GREY = (0.42, 0.46, 0.52)
|
|
||||||
RED = (0.85, 0.29, 0.24)
|
|
||||||
GREEN = (0.23, 0.65, 0.35)
|
|
||||||
|
|
||||||
def add(name, shape, color, transparency=0):
|
|
||||||
o = doc.addObject("Part::Feature", name)
|
|
||||||
o.Shape = shape
|
|
||||||
o.ViewObject.ShapeColor = color
|
|
||||||
o.ViewObject.LineColor = color
|
|
||||||
o.ViewObject.PointColor = color
|
|
||||||
o.ViewObject.Transparency = transparency
|
|
||||||
return o
|
|
||||||
|
|
||||||
def frame(origin, zdir, xdir):
|
|
||||||
"""Right-handed placement matrix from origin + Z + X (X orthonormalised against Z)."""
|
|
||||||
z = Vector(*zdir); z.normalize()
|
|
||||||
xr = Vector(*xdir)
|
|
||||||
x = xr.sub(Vector(z).multiply(z.dot(xr))); x.normalize()
|
|
||||||
y = z.cross(x)
|
|
||||||
return App.Matrix(x.x, y.x, z.x, origin[0],
|
|
||||||
x.y, y.y, z.y, origin[1],
|
|
||||||
x.z, y.z, z.z, origin[2],
|
|
||||||
0, 0, 0, 1)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- the bracket
|
|
||||||
plate = Part.makeBox(120, 46, 8)
|
|
||||||
bore = Part.makeCylinder(7, 40, Vector(96, 23, -6)) # a real bore, curved face
|
|
||||||
boss = Part.makeCylinder(11, 7, Vector(96, 23, 8))
|
|
||||||
part = plate.fuse(boss).cut(bore)
|
|
||||||
add("Bracket", part, (0.60, 0.63, 0.66))
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- glyph pieces
|
|
||||||
def ring(t=None):
|
|
||||||
t = t or R * 0.10
|
|
||||||
return Part.makeCylinder(R, t).cut(Part.makeCylinder(R * 0.84, t))
|
|
||||||
|
|
||||||
def quadrant(t=None):
|
|
||||||
t = t or R * 0.10
|
|
||||||
return Part.makeCylinder(R * 0.84, t, Vector(0, 0, 0), Vector(0, 0, 1), 90)
|
|
||||||
|
|
||||||
def stem(L=None, r=None):
|
|
||||||
return Part.makeCylinder(r or R * 0.09, L or R * 2.3)
|
|
||||||
|
|
||||||
def solid_head():
|
|
||||||
return Part.makeCone(R * 0.32, 0, R * 0.80, Vector(0, 0, R * 2.3))
|
|
||||||
|
|
||||||
def shell_head():
|
|
||||||
outer = Part.makeCone(R * 0.32, 0, R * 0.80, Vector(0, 0, R * 2.3))
|
|
||||||
inner = Part.makeCone(R * 0.22, 0, R * 0.62, Vector(0, 0, R * 2.3))
|
|
||||||
return outer.cut(inner)
|
|
||||||
|
|
||||||
def short_axis(direction, L=None):
|
|
||||||
L = L or R * 1.15
|
|
||||||
return Part.makeCylinder(R * 0.07, L, Vector(0, 0, 0), Vector(*direction))
|
|
||||||
|
|
||||||
def place(shape, m):
|
|
||||||
s = shape.copy()
|
|
||||||
s.transformShape(m)
|
|
||||||
return s
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- the variants
|
|
||||||
def variant_A(tag, origin): # Onshape baseline
|
|
||||||
m = frame(origin, (0, 0, 1), (1, 0, 0))
|
|
||||||
add(tag + "_ring", place(ring(), m), GREY)
|
|
||||||
add(tag + "_quad", place(quadrant(), m), GOLD)
|
|
||||||
add(tag + "_x", place(short_axis((1, 0, 0)), m), RED)
|
|
||||||
add(tag + "_y", place(short_axis((0, 1, 0)), m), GREEN)
|
|
||||||
add(tag + "_z", place(short_axis((0, 0, 1), R * 1.6), m), BLUE)
|
|
||||||
|
|
||||||
def variant_B(tag, origin, zdir=(0, 0, 1)): # solid cone = driven
|
|
||||||
m = frame(origin, zdir, (1, 0, 0))
|
|
||||||
add(tag + "_ring", place(ring(), m), BLUE)
|
|
||||||
add(tag + "_quad", place(quadrant(), m), GOLD)
|
|
||||||
add(tag + "_body", place(stem().fuse(solid_head()), m), BLUE)
|
|
||||||
|
|
||||||
def variant_C(tag, origin, zdir=(0, 0, 1)): # hollow collar = fixed
|
|
||||||
m = frame(origin, zdir, (1, 0, 0))
|
|
||||||
add(tag + "_ring", place(ring(), m), GREY)
|
|
||||||
add(tag + "_quad", place(quadrant(), m), GOLD)
|
|
||||||
add(tag + "_body", place(stem().fuse(shell_head()), m), GREY)
|
|
||||||
|
|
||||||
def variant_D_pin(tag, origin, zdir=(0, 0, 1)): # polarity by relief: raised PIN
|
|
||||||
m = frame(origin, zdir, (1, 0, 0))
|
|
||||||
pin = Part.makeCylinder(R * 0.30, R * 1.5).fuse(
|
|
||||||
Part.makeCone(R * 0.30, 0, R * 0.55, Vector(0, 0, R * 1.5)))
|
|
||||||
add(tag + "_ring", place(ring(), m), BLUE)
|
|
||||||
add(tag + "_quad", place(quadrant(), m), GOLD)
|
|
||||||
add(tag + "_pin", place(pin, m), BLUE)
|
|
||||||
|
|
||||||
def variant_D_cup(tag, origin, zdir=(0, 0, 1)): # polarity by relief: sunk CUP
|
|
||||||
m = frame(origin, zdir, (1, 0, 0))
|
|
||||||
cup = Part.makeCylinder(R * 0.62, R * 0.9).cut(
|
|
||||||
Part.makeCylinder(R * 0.40, R * 0.9, Vector(0, 0, -0.01)))
|
|
||||||
add(tag + "_ring", place(ring(), m), GREY)
|
|
||||||
add(tag + "_quad", place(quadrant(), m), GOLD)
|
|
||||||
add(tag + "_cup", place(cup, m), GREY)
|
|
||||||
|
|
||||||
# four treatments across the plate, all on the same flat face, same Z
|
|
||||||
variant_A("A", (14, 30, 8))
|
|
||||||
variant_B("B", (40, 30, 8))
|
|
||||||
variant_C("C", (64, 30, 8))
|
|
||||||
variant_D_pin("Dpin", (14, 10, 8))
|
|
||||||
variant_D_cup("Dcup", (40, 10, 8))
|
|
||||||
|
|
||||||
# the hard cases, which is the whole reason for doing this in 3D:
|
|
||||||
variant_B("Bore", (96, 23, 15)) # on the boss above a bore
|
|
||||||
variant_B("Edge", (64, 0, 8), (0, -0.7071, 0.7071)) # tilted, on an edge, oblique Z
|
|
||||||
|
|
||||||
doc.recompute()
|
|
||||||
|
|
||||||
v = Gui.activeDocument().activeView()
|
|
||||||
v.viewIsometric()
|
|
||||||
Gui.SendMsgToActiveView("ViewFit")
|
|
||||||
App.Console.PrintMessage("glyph probe built: %d objects\n" % len(doc.Objects))
|
|
||||||
|
Before Width: | Height: | Size: 35 KiB |
@@ -1,112 +0,0 @@
|
|||||||
"""Give the bear a handedness mark that survives rasterisation — wi3z, Tommaso's call 2.
|
|
||||||
|
|
||||||
The study showed the left/right cue lives in sub-millimetre corner radii and is therefore invisible
|
|
||||||
at glyph size: one pixel is 2.6 mm at 32 px. Roll and verse are safe; handedness is not.
|
|
||||||
|
|
||||||
THE MEASURE IS THE QUESTION ITSELF. Render the glyph, render its mirror image, and count how many
|
|
||||||
pixels differ. If a human is to tell left from right, the two must differ on screen; a candidate
|
|
||||||
that scores near zero is invisible however elegant it looks in CAD. Reported as a percentage of the
|
|
||||||
glyph's own lit area, so the sizes are comparable.
|
|
||||||
"""
|
|
||||||
import json, math, os
|
|
||||||
from PIL import Image, ImageDraw, ImageChops
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
D = json.load(open(os.path.join(HERE, "bear_outline.json")))
|
|
||||||
def unit(pts):
|
|
||||||
p = [(x, -z) for x, z in pts]
|
|
||||||
return p
|
|
||||||
outer = unit(D["outer"]); holes = [unit(h["pts"]) for h in D["holes"]]
|
|
||||||
ALL = outer + [p for h in holes for p in h]
|
|
||||||
xs=[p[0] for p in ALL]; ys=[p[1] for p in ALL]
|
|
||||||
CX,CY = (min(xs)+max(xs))/2,(min(ys)+max(ys))/2
|
|
||||||
SPAN = max(max(xs)-min(xs), max(ys)-min(ys))
|
|
||||||
U = lambda pts: [((x-CX)/SPAN,(y-CY)/SPAN) for x,y in pts]
|
|
||||||
OUT = U(outer)
|
|
||||||
EYES = [U(h) for h,m in zip(holes, D["holes"]) if m["d"] < 20]
|
|
||||||
MUZ = U([h for h,m in zip(holes, D["holes"]) if m["d"] >= 20][0])
|
|
||||||
|
|
||||||
def rdp(pts, eps):
|
|
||||||
if len(pts) < 3: return pts
|
|
||||||
ax,ay=pts[0]; bx,by=pts[-1]; dx,dy=bx-ax,by-ay
|
|
||||||
n=math.hypot(dx,dy); best,bi=-1.0,0
|
|
||||||
for i in range(1,len(pts)-1):
|
|
||||||
px,py=pts[i]
|
|
||||||
d=abs(dx*(ay-py)-(ax-px)*dy)/n if n>1e-12 else math.hypot(px-ax,py-ay)
|
|
||||||
if d>best: best,bi=d,i
|
|
||||||
if best<=eps: return [pts[0],pts[-1]]
|
|
||||||
return rdp(pts[:bi+1],eps)[:-1]+rdp(pts[bi:],eps)
|
|
||||||
def simp(pts,eps):
|
|
||||||
r=rdp(pts+[pts[0]],eps); return r[:-1]
|
|
||||||
|
|
||||||
BASE = simp(OUT, .030) # the 22-vertex outline the study settled on
|
|
||||||
def centroid(p): return (sum(q[0] for q in p)/len(p), sum(q[1] for q in p)/len(p))
|
|
||||||
def circ(cx,cy,r,n=16): return [(cx+r*math.cos(2*math.pi*i/n), cy+r*math.sin(2*math.pi*i/n)) for i in range(n)]
|
|
||||||
EYE_D = []
|
|
||||||
for e in EYES:
|
|
||||||
c=centroid(e); r=(max(p[0] for p in e)-min(p[0] for p in e))/2
|
|
||||||
EYE_D.append((c[0],c[1],r))
|
|
||||||
EYE_D.sort() # [0] = left (x<0), [1] = right
|
|
||||||
|
|
||||||
TOP = max(p[1] for p in BASE)
|
|
||||||
H = TOP - min(p[1] for p in BASE)
|
|
||||||
def ear_tip(sign):
|
|
||||||
cands=[p for p in BASE if p[1] > TOP-0.18*H and (p[0]*sign) > 0]
|
|
||||||
return max(cands, key=lambda p: p[0]*sign) if cands else None
|
|
||||||
LT, RT = ear_tip(-1), ear_tip(+1)
|
|
||||||
|
|
||||||
def notch(tip, sign, k=0.085):
|
|
||||||
"""A wedge bitten out of one ear — background-filled, exactly how the eyes are already drawn."""
|
|
||||||
x,y = tip
|
|
||||||
return [(x, y+0.02), (x - sign*k, y - k*0.55), (x + sign*k*0.15, y - k*1.05)]
|
|
||||||
|
|
||||||
CANDS = {
|
|
||||||
"H0 none": dict(cuts=[], eyes=EYE_D),
|
|
||||||
"H1 notch R ear": dict(cuts=[notch(RT, +1)], eyes=EYE_D),
|
|
||||||
"H2 notch both": dict(cuts=[notch(RT, +1), notch(LT, -1, 0.045)], eyes=EYE_D),
|
|
||||||
"H3 cheek dot": dict(cuts=[circ(EYE_D[1][0]+0.085, EYE_D[1][1]-0.10, 0.038)], eyes=EYE_D),
|
|
||||||
"H4 uneven eyes": dict(cuts=[], eyes=[EYE_D[0], (EYE_D[1][0], EYE_D[1][1], EYE_D[1][2]*1.55)]),
|
|
||||||
}
|
|
||||||
|
|
||||||
def render(c, px, ss=8, mirror=False):
|
|
||||||
S=px*ss; img=Image.new("L",(S,S),0); d=ImageDraw.Draw(img)
|
|
||||||
m = lambda p: (S/2 + (-p[0] if mirror else p[0])*S*0.92, S/2 - p[1]*S*0.92)
|
|
||||||
d.polygon([m(p) for p in BASE], fill=255)
|
|
||||||
d.polygon([m(p) for p in MUZ], fill=0)
|
|
||||||
for cx,cy,r in c["eyes"]:
|
|
||||||
a=m((cx-r,cy+r)); b=m((cx+r,cy-r))
|
|
||||||
d.ellipse([min(a[0],b[0]), min(a[1],b[1]), max(a[0],b[0]), max(a[1],b[1])], fill=0)
|
|
||||||
for cut in c["cuts"]:
|
|
||||||
d.polygon([m(p) for p in cut], fill=0)
|
|
||||||
return img.resize((px,px), Image.LANCZOS)
|
|
||||||
|
|
||||||
SIZES=[22,32,48]
|
|
||||||
print(f"{'candidate':16} " + " ".join(f"{s}px" for s in SIZES) + " (pixels differing from own mirror, % of lit area)")
|
|
||||||
print("-"*84)
|
|
||||||
scores={}
|
|
||||||
for name,c in CANDS.items():
|
|
||||||
row=[]
|
|
||||||
for px in SIZES:
|
|
||||||
a=render(c,px); b=render(c,px,mirror=True)
|
|
||||||
diff=ImageChops.difference(a,b)
|
|
||||||
nd=sum(1 for v in diff.getdata() if v>40)
|
|
||||||
lit=sum(1 for v in a.getdata() if v>40) or 1
|
|
||||||
row.append(100.0*nd/lit)
|
|
||||||
scores[name]=row
|
|
||||||
print(f"{name:16} " + " ".join(f"{v:5.1f}" for v in row))
|
|
||||||
|
|
||||||
pad,cell=8,58
|
|
||||||
W=pad+len(SIZES)*2*cell+pad; Hh=pad+len(CANDS)*cell+pad
|
|
||||||
sheet=Image.new("RGB",(W,Hh),(24,27,32))
|
|
||||||
for r,(name,c) in enumerate(CANDS.items()):
|
|
||||||
for mi,mir in enumerate((False,True)):
|
|
||||||
for si,px in enumerate(SIZES):
|
|
||||||
g=render(c,px,mirror=mir)
|
|
||||||
tile=Image.new("RGB",(px,px),(24,27,32))
|
|
||||||
tile.paste(Image.new("RGB",(px,px),(237,168,23)),(0,0),g)
|
|
||||||
x=pad+(mi*len(SIZES)+si)*cell+(cell-px)//2
|
|
||||||
y=pad+r*cell+(cell-px)//2
|
|
||||||
sheet.paste(tile,(x,y))
|
|
||||||
sheet.resize((W*2,Hh*2), Image.NEAREST).save(os.path.join(HERE,"handedness-sheet.png"))
|
|
||||||
print("\nleft block = as drawn, right block = mirrored. rows: " + ", ".join(CANDS))
|
|
||||||
print("WROTE handedness-sheet.png")
|
|
||||||
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,135 +0,0 @@
|
|||||||
# Build the complementary FEMALE for BearConnector.step.
|
|
||||||
#
|
|
||||||
# Method: take the supplied male B-rep as-is, grow it by a uniform clearance, and subtract that
|
|
||||||
# from a block. Working on the real solid rather than re-modelling the bear is the whole point —
|
|
||||||
# the pocket is then exactly complementary by construction, including every deliberate asymmetry.
|
|
||||||
#
|
|
||||||
# The offset uses join=2 (Intersection), which extends the adjacent planes and meets them at a
|
|
||||||
# sharp corner. For a faceted part that is the correct join: the arc join would round every convex
|
|
||||||
# edge and blunt the very cues the design depends on.
|
|
||||||
#
|
|
||||||
# THE MALE'S NATIVE FRAME: the flat back is the plane Y=0 and the relief rises to Y=+17.27.
|
|
||||||
# X and Z carry the face (83.34 x 66.69). The frame is kept exactly as supplied so that male and
|
|
||||||
# female drop into the same assembly without anyone having to re-orient one of them.
|
|
||||||
# Insertion is therefore along +Y, and the pocket must OPEN on the Y=0 plane.
|
|
||||||
#
|
|
||||||
# A first version of this script assumed the relief ran along +Z, built the block around the wrong
|
|
||||||
# axis, and produced a sealed cavity with no way in. It passed a "male does not intersect female"
|
|
||||||
# check, because that only tests the seated position and says nothing about whether the part can
|
|
||||||
# get there. The straight-pull test below is what catches it.
|
|
||||||
#
|
|
||||||
# Run: /snap/bin/freecad.cmd make_female.py
|
|
||||||
|
|
||||||
import os, sys, math
|
|
||||||
import FreeCAD as App
|
|
||||||
import Part
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
MALE = os.path.join(HERE, "bear.step")
|
|
||||||
OUT_STEP = os.path.join(HERE, "BearConnector_Female.step")
|
|
||||||
|
|
||||||
CLEAR = 0.20 # per-face clearance, mm
|
|
||||||
WALL = 4.0 # material around the pocket, mm
|
|
||||||
FLOOR = 3.0 # material behind the deepest point of the pocket, mm
|
|
||||||
|
|
||||||
male = Part.Shape(); male.read(MALE)
|
|
||||||
if len(male.Solids) != 1:
|
|
||||||
print(f"FAIL: expected 1 solid in the male, found {len(male.Solids)}"); sys.exit(1)
|
|
||||||
male = male.Solids[0]
|
|
||||||
bb = male.BoundBox
|
|
||||||
print(f"male : {bb.XLength:.2f} (X) x {bb.YLength:.2f} (Y) x {bb.ZLength:.2f} (Z) mm, "
|
|
||||||
f"{len(male.Faces)} faces, {male.Volume/1000:.2f} cm3")
|
|
||||||
print(f" relief runs Y {bb.YMin:.2f} .. {bb.YMax:.2f} -> insertion along +Y, mouth at Y={bb.YMin:.2f}")
|
|
||||||
|
|
||||||
# ---- 1. can the male even be withdrawn along the insertion axis? ----------------------
|
|
||||||
# Ray-cast a grid along +Y through the tessellated male and count crossings. A straight pull is
|
|
||||||
# possible only if no ray enters the solid more than once; a second entry is an undercut.
|
|
||||||
verts, facets = male.tessellate(0.15)
|
|
||||||
V = [(v.x, v.y, v.z) for v in verts]
|
|
||||||
worst, undercut_pts = 0, 0
|
|
||||||
NX = NZ = 90
|
|
||||||
for i in range(NX):
|
|
||||||
x = bb.XMin + (i + 0.5) * bb.XLength / NX
|
|
||||||
for j in range(NZ):
|
|
||||||
z = bb.ZMin + (j + 0.5) * bb.ZLength / NZ
|
|
||||||
hits = 0
|
|
||||||
for (ia, ib, ic) in facets: # ray (x, *, z) along +Y vs triangle
|
|
||||||
ax, ay, az = V[ia]; bx, by, bz = V[ib]; cx, cy, cz = V[ic]
|
|
||||||
# 2D point-in-triangle in the XZ plane
|
|
||||||
d = (bz - cz) * (ax - cx) + (cx - bx) * (az - cz)
|
|
||||||
if abs(d) < 1e-12: continue
|
|
||||||
u = ((bz - cz) * (x - cx) + (cx - bx) * (z - cz)) / d
|
|
||||||
v = ((cz - az) * (x - cx) + (ax - cx) * (z - cz)) / d
|
|
||||||
if u < 0 or v < 0 or u + v > 1: continue
|
|
||||||
hits += 1
|
|
||||||
worst = max(worst, hits)
|
|
||||||
if hits > 2: undercut_pts += 1
|
|
||||||
print(f"pull : max crossings along +Y = {worst}, undercut samples = {undercut_pts}/{NX*NZ}")
|
|
||||||
if undercut_pts:
|
|
||||||
print("FAIL: the male has an undercut along +Y; a straight pocket cannot release it")
|
|
||||||
sys.exit(1)
|
|
||||||
print(" no undercut -> a straight-pull pocket works")
|
|
||||||
|
|
||||||
# ---- 2. grow the male by the clearance -----------------------------------------------
|
|
||||||
grown = None
|
|
||||||
for join, name in ((2, "Intersection"), (1, "Tangent"), (0, "Arc")):
|
|
||||||
try:
|
|
||||||
g = male.makeOffsetShape(CLEAR, 1e-6, False, False, 0, join, False)
|
|
||||||
if g.isValid() and g.Solids:
|
|
||||||
grown = g.Solids[0]; print(f"offset: join={name}, {grown.Volume/1000:.2f} cm3"); break
|
|
||||||
except Exception as e:
|
|
||||||
print(f"offset: join={name} failed -- {e}")
|
|
||||||
if grown is None:
|
|
||||||
print("FAIL: could not offset the male; refusing to emit a zero-clearance pocket"); sys.exit(1)
|
|
||||||
|
|
||||||
# ---- 3. the block: walls in X and Z, depth in +Y, OPEN at the Y=0 mouth ---------------
|
|
||||||
gb = grown.BoundBox
|
|
||||||
y_mouth = bb.YMin # the male's flat back plane
|
|
||||||
depth = gb.YMax - y_mouth
|
|
||||||
block = Part.makeBox(gb.XLength + 2*WALL, depth + FLOOR, gb.ZLength + 2*WALL,
|
|
||||||
App.Vector(gb.XMin - WALL, y_mouth, gb.ZMin - WALL))
|
|
||||||
print(f"block : {gb.XLength + 2*WALL:.2f} x {depth + FLOOR:.2f} x {gb.ZLength + 2*WALL:.2f} mm, "
|
|
||||||
f"mouth on the Y={y_mouth:.2f} plane")
|
|
||||||
|
|
||||||
female = block.cut(grown)
|
|
||||||
|
|
||||||
# ---- 4. verify --------------------------------------------------------------------------
|
|
||||||
ok = True
|
|
||||||
if not female.isValid(): print("FAIL: invalid shape"); ok = False
|
|
||||||
if len(female.Solids) != 1: print(f"FAIL: {len(female.Solids)} solids"); ok = False
|
|
||||||
|
|
||||||
clash = male.common(female)
|
|
||||||
cv = clash.Volume if clash.Solids else 0.0
|
|
||||||
print(f"check : male ∩ female = {cv:.6f} mm3 (seated fit, must be ~0)")
|
|
||||||
if cv > 1e-3: print("FAIL: male collides with female"); ok = False
|
|
||||||
|
|
||||||
# the mouth must actually be open: the pocket has to reach the Y=y_mouth face of the block
|
|
||||||
mouth_face_area = 0.0
|
|
||||||
for f in female.Faces:
|
|
||||||
c = f.CenterOfMass
|
|
||||||
if abs(c.y - y_mouth) < 1e-6:
|
|
||||||
mouth_face_area += f.Area
|
|
||||||
solid_mouth = (gb.XLength + 2*WALL) * (gb.ZLength + 2*WALL)
|
|
||||||
open_area = solid_mouth - mouth_face_area
|
|
||||||
print(f"check : mouth plane -- material {mouth_face_area:.1f} mm2, opening {open_area:.1f} mm2 "
|
|
||||||
f"({100*open_area/solid_mouth:.1f}% of the face)")
|
|
||||||
if open_area < 100:
|
|
||||||
print("FAIL: the pocket is sealed -- the male cannot be inserted"); ok = False
|
|
||||||
|
|
||||||
cavity = block.Volume - female.Volume
|
|
||||||
print(f"check : cavity {cavity/1000:.2f} cm3 vs male {male.Volume/1000:.2f} cm3 "
|
|
||||||
f"-> clearance shell {(cavity-male.Volume)/1000:.2f} cm3")
|
|
||||||
if cavity < male.Volume: print("FAIL: cavity smaller than the male"); ok = False
|
|
||||||
|
|
||||||
if not ok:
|
|
||||||
print("\nREFUSING to write the STEP"); sys.exit(1)
|
|
||||||
|
|
||||||
doc = App.newDocument("Female")
|
|
||||||
obj = doc.addObject("Part::Feature", "BearConnector_Female")
|
|
||||||
obj.Shape = female
|
|
||||||
doc.recompute()
|
|
||||||
Part.export([obj], OUT_STEP)
|
|
||||||
fb = female.BoundBox
|
|
||||||
print(f"\nwrote {OUT_STEP}")
|
|
||||||
print(f"female: {fb.XLength:.2f} x {fb.YLength:.2f} x {fb.ZLength:.2f} mm, "
|
|
||||||
f"{len(female.Faces)} faces, {female.Volume/1000:.2f} cm3")
|
|
||||||
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,71 +0,0 @@
|
|||||||
"""The muzzle has to READ, not just be present — wi3z.
|
|
||||||
|
|
||||||
Faithfully scaled, the part's ridge is 11.3 mm on an 83 mm face: 13.6 % of the width. At glyph
|
|
||||||
size that is a scratch. A glyph is a symbol, not a scale model, so the question is how much
|
|
||||||
emphasis it takes before the only +Z feature actually reads. Variants, all with the same crest
|
|
||||||
geometry, differing only in width and colour.
|
|
||||||
"""
|
|
||||||
import math, os, importlib.util
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
spec=importlib.util.spec_from_file_location("gp","glyph_preview.py")
|
|
||||||
gp=importlib.util.module_from_spec(spec); spec.loader.exec_module(gp)
|
|
||||||
|
|
||||||
OUT, CHIN, MARKS, CREST, SBASE, PLATE = gp.OUT, gp.CHIN, gp.MARKS, gp.CREST, gp.SBASE, gp.PLATE
|
|
||||||
BODY=(0.42,0.46,0.52); MARK=(0.126,0.138,0.156); GOLD=(0.93,0.66,0.09)
|
|
||||||
|
|
||||||
def facets(widen=1.0, muzzle_gold=False):
|
|
||||||
F=[]; n=len(OUT)
|
|
||||||
for i in range(n):
|
|
||||||
a,b=OUT[i],OUT[(i+1)%n]
|
|
||||||
F.append(([(a[0],a[1],0.0),(b[0],b[1],0.0),(b[0],b[1],PLATE),(a[0],a[1],PLATE)],BODY,True))
|
|
||||||
F.append(([(x,y,PLATE) for x,y in OUT],BODY,True))
|
|
||||||
zm=PLATE+0.004
|
|
||||||
for cx,cy,r in MARKS:
|
|
||||||
F.append(([(cx+r*math.cos(2*math.pi*i/12),cy+r*math.sin(2*math.pi*i/12),zm) for i in range(12)],MARK,False))
|
|
||||||
F.append(([(x,y,zm) for x,y in CHIN],MARK,False))
|
|
||||||
A,B=CREST
|
|
||||||
w=lambda p:(p[0]*widen,p[1],PLATE)
|
|
||||||
nl,nr,tr,tl=(w(SBASE[0]),w(SBASE[1]),w(SBASE[2]),w(SBASE[3]))
|
|
||||||
col = GOLD if muzzle_gold else BODY
|
|
||||||
F+=[([nl,tl,B,A],col,True),([nr,A,B,tr],col,True),
|
|
||||||
([nl,A,nr],col,True), ([tr,B,tl],col,True)]
|
|
||||||
return F
|
|
||||||
|
|
||||||
def render(F, px, elev, ss=8):
|
|
||||||
S=px*ss; a=math.radians(elev); ca,sa=math.cos(a),math.sin(a)
|
|
||||||
xf=lambda p:(p[0],p[1]*sa+p[2]*ca,-p[1]*ca+p[2]*sa)
|
|
||||||
light=(-0.70,0.30,0.45)
|
|
||||||
img=Image.new("RGB",(S,S),(24,27,32)); d=ImageDraw.Draw(img)
|
|
||||||
tris=sorted(((sum(v[2] for v in [xf(q) for q in pts])/len(pts),[xf(q) for q in pts],c,sh)
|
|
||||||
for pts,c,sh in F), key=lambda t:t[0])
|
|
||||||
for _,q,base,shade in tris:
|
|
||||||
(x0,y0,z0),(x1,y1,z1),(x2,y2,z2)=q[0],q[1],q[2]
|
|
||||||
ux,uy,uz=x1-x0,y1-y0,z1-z0; vx,vy,vz=x2-x0,y2-y0,z2-z0
|
|
||||||
nx,ny,nz=uy*vz-uz*vy,uz*vx-ux*vz,ux*vy-uy*vx
|
|
||||||
L=math.sqrt(nx*nx+ny*ny+nz*nz) or 1.0; nx,ny,nz=nx/L,ny/L,nz/L
|
|
||||||
if nz<0: nx,ny,nz=-nx,-ny,-nz
|
|
||||||
k=(0.42+0.58*max(0.0,nx*light[0]+ny*light[1]+nz*light[2])) if shade else 1.0
|
|
||||||
d.polygon([(S/2+p[0]*S*0.92,S/2-p[1]*S*0.92) for p in q],
|
|
||||||
fill=tuple(min(255,int(255*c*k)) for c in base))
|
|
||||||
return img.resize((px,px),Image.LANCZOS)
|
|
||||||
|
|
||||||
VAR=[("V1 faithful", 1.0, False),
|
|
||||||
("V2 gold muzzle", 1.0, True),
|
|
||||||
("V3 gold + 1.8x wide",1.8, True),
|
|
||||||
("V4 body + 1.8x wide",1.8, False)]
|
|
||||||
big=Image.new("RGB",(4*250+30,4*140+30),(24,27,32))
|
|
||||||
for r,(name,wd,gold) in enumerate(VAR):
|
|
||||||
F=facets(wd,gold)
|
|
||||||
for c,e in enumerate((90,47,16,6)):
|
|
||||||
big.paste(render(F,120,e),(15+c*250+60,15+r*140+10))
|
|
||||||
big.save("/tmp/muzzle-variants.png")
|
|
||||||
for name,wd,gold in VAR:
|
|
||||||
F=facets(wd,gold); F0=[f for f in F][:-4]
|
|
||||||
row=[]
|
|
||||||
for e in (90,16,6):
|
|
||||||
a=render(F,32,e); b=render(F0,32,e)
|
|
||||||
la=sum(1 for p in a.get_flattened_data() if p!=(24,27,32))
|
|
||||||
df=sum(1 for p,q in zip(a.get_flattened_data(),b.get_flattened_data()) if p!=q)
|
|
||||||
row.append(f"{100.0*df/max(1,la):5.1f}%")
|
|
||||||
print(f"{name:22} muzzle share at 90/16/6 deg: " + " ".join(row))
|
|
||||||
print("WROTE /tmp/muzzle-variants.png")
|
|
||||||
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,99 +0,0 @@
|
|||||||
"""Flat glyph vs 3D relief, at the elevations that killed the disc — wi3z.
|
|
||||||
|
|
||||||
The flat study collapsed at 16 deg because anything drawn IN the connector's plane foreshortens by
|
|
||||||
sin(elevation). This renders the SAME bear as its real relief (1508 facets off the supplied male)
|
|
||||||
with a simple lambert shade, so the silhouette does the work at a grazing angle. Two rows, same
|
|
||||||
sizes, same elevations, so the comparison is direct.
|
|
||||||
"""
|
|
||||||
import json, math, os
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
M = json.load(open(os.path.join(HERE, "bear_mesh.json")))
|
|
||||||
V, F = M["v"], M["f"]
|
|
||||||
|
|
||||||
# Part frame: face carried by X (right) and Z (down-negative), relief along +Y.
|
|
||||||
P = [(v[0], -v[2], v[1]) for v in V] # -> (x right, y up, z out of the face)
|
|
||||||
xs=[p[0] for p in P]; ys=[p[1] for p in P]; zs=[p[2] for p in P]
|
|
||||||
CX,CY,CZ = (min(xs)+max(xs))/2, (min(ys)+max(ys))/2, (min(zs)+max(zs))/2
|
|
||||||
SPAN = max(max(xs)-min(xs), max(ys)-min(ys))
|
|
||||||
P = [((x-CX)/SPAN, (y-CY)/SPAN, (z-CZ)/SPAN) for x,y,z in P]
|
|
||||||
|
|
||||||
def shade(px, elev_deg, supersample=8):
|
|
||||||
"""Camera orbits down from straight-on (90) to grazing (small). Rotate about the screen x-axis."""
|
|
||||||
S = px*supersample
|
|
||||||
a = math.radians(elev_deg)
|
|
||||||
ca, sa = math.cos(a), math.sin(a)
|
|
||||||
# view: rotate the model so the face normal tips away from the camera
|
|
||||||
def xf(p):
|
|
||||||
x,y,z = p
|
|
||||||
return (x, y*sa + z*ca, -y*ca + z*sa) # third component = depth toward camera
|
|
||||||
Q = [xf(p) for p in P]
|
|
||||||
img = Image.new("L", (S,S), 0)
|
|
||||||
d = ImageDraw.Draw(img)
|
|
||||||
order = []
|
|
||||||
for tri in F:
|
|
||||||
a3 = [Q[i] for i in tri]
|
|
||||||
order.append((sum(v[2] for v in a3)/3.0, tri, a3))
|
|
||||||
order.sort(key=lambda t: t[0]) # painter: far first
|
|
||||||
light = (-0.35, 0.55, 0.76)
|
|
||||||
for _, tri, a3 in order:
|
|
||||||
(x0,y0,z0),(x1,y1,z1),(x2,y2,z2) = a3
|
|
||||||
ux,uy,uz = x1-x0, y1-y0, z1-z0
|
|
||||||
vx,vy,vz = x2-x0, y2-y0, z2-z0
|
|
||||||
nx,ny,nz = uy*vz-uz*vy, uz*vx-ux*vz, ux*vy-uy*vx
|
|
||||||
n = math.sqrt(nx*nx+ny*ny+nz*nz) or 1.0
|
|
||||||
nx,ny,nz = nx/n, ny/n, nz/n
|
|
||||||
if nz < 0: nx,ny,nz = -nx,-ny,-nz # face the camera
|
|
||||||
lam = max(0.0, nx*light[0] + ny*light[1] + nz*light[2])
|
|
||||||
val = int(70 + 185*lam)
|
|
||||||
pts = [(S/2 + x*S*0.92, S/2 - y*S*0.92) for x,y,_ in a3]
|
|
||||||
d.polygon(pts, fill=val)
|
|
||||||
return img.resize((px,px), Image.LANCZOS)
|
|
||||||
|
|
||||||
# flat outline, for the side-by-side
|
|
||||||
D = json.load(open(os.path.join(HERE, "bear_outline.json")))
|
|
||||||
def unit(pts):
|
|
||||||
p=[(x,-z) for x,z in pts]
|
|
||||||
return [((x-CX)/SPAN,(y-CY)/SPAN) for x,y in p]
|
|
||||||
OUT = unit(D["outer"])
|
|
||||||
HOLES = [unit(h["pts"]) for h in D["holes"]]
|
|
||||||
|
|
||||||
def flat(px, elev_deg, supersample=8):
|
|
||||||
S=px*supersample
|
|
||||||
img=Image.new("L",(S,S),0); d=ImageDraw.Draw(img)
|
|
||||||
k=math.sin(math.radians(elev_deg))
|
|
||||||
m=lambda p:(S/2+p[0]*S*0.92, S/2-p[1]*S*0.92*k)
|
|
||||||
d.polygon([m(p) for p in OUT], fill=255)
|
|
||||||
for h in HOLES: d.polygon([m(p) for p in h], fill=0)
|
|
||||||
return img.resize((px,px), Image.LANCZOS)
|
|
||||||
|
|
||||||
SIZES=[22,32,48]; ELEVS=[(90,"flat on"),(47,"47"),(16,"16"),(6,"6")]
|
|
||||||
pad,cell=8,58
|
|
||||||
W=pad+len(SIZES)*len(ELEVS)*cell+pad; H=pad+2*cell+pad
|
|
||||||
sheet=Image.new("RGB",(W,H),(24,27,32))
|
|
||||||
for r,fn in enumerate((flat, shade)):
|
|
||||||
for ci,(elev,_) in enumerate(ELEVS):
|
|
||||||
for si,px in enumerate(SIZES):
|
|
||||||
g=fn(px,elev)
|
|
||||||
tile=Image.new("RGB",(px,px),(24,27,32))
|
|
||||||
if fn is flat:
|
|
||||||
tile.paste(Image.new("RGB",(px,px),(237,168,23)),(0,0),g)
|
|
||||||
else:
|
|
||||||
gg=g.convert("L")
|
|
||||||
tile=Image.merge("RGB",(gg.point(lambda v:min(255,int(v*1.00))),
|
|
||||||
gg.point(lambda v:int(v*0.71)),
|
|
||||||
gg.point(lambda v:int(v*0.16))))
|
|
||||||
x=pad+(ci*len(SIZES)+si)*cell+(cell-px)//2
|
|
||||||
y=pad+r*cell+(cell-px)//2
|
|
||||||
sheet.paste(tile,(x,y))
|
|
||||||
sheet.resize((W*2,H*2), Image.NEAREST).save(os.path.join(HERE,"relief-sheet.png"))
|
|
||||||
|
|
||||||
# how much ink survives — the same measure used on the disc glyph
|
|
||||||
print(f"{'elev':>6} {'flat px@32':>11} {'relief px@32':>13}")
|
|
||||||
for elev,_ in ELEVS:
|
|
||||||
f32=flat(32,elev); s32=shade(32,elev)
|
|
||||||
fi=sum(1 for v in f32.getdata() if v>40)
|
|
||||||
si=sum(1 for v in s32.getdata() if v>40)
|
|
||||||
print(f"{elev:>6} {fi:>11} {si:>13}")
|
|
||||||
print("WROTE relief-sheet.png")
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Export the real male's relief as a triangle mesh, so the grazing test uses the actual geometry.
|
|
||||||
import os, json
|
|
||||||
import Part
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
s = Part.Shape(); s.read(os.path.join(HERE, "bear.step"))
|
|
||||||
verts, facets = s.Solids[0].tessellate(0.25)
|
|
||||||
V = [[round(p.x,4), round(p.y,4), round(p.z,4)] for p in verts]
|
|
||||||
json.dump({"v": V, "f": facets}, open(os.path.join(HERE, "bear_mesh.json"), "w"))
|
|
||||||
print(f"verts {len(V)} facets {len(facets)}")
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Flat-shade the faceted ridge key from several camera directions.
|
|
||||||
|
|
||||||
The point is not a pretty picture. It is one question: does a low-poly solid, flat-shaded,
|
|
||||||
let a human read its orientation from an arbitrary viewpoint -- and specifically, is the
|
|
||||||
view ALONG the ridge ambiguous between front and back, as the geometry suggests it must be
|
|
||||||
in silhouette?
|
|
||||||
|
|
||||||
Flat shading (one normal per facet, no smoothing) is deliberate: it is what the concept
|
|
||||||
claims to rely on, and it is what a CAD viewport with hard normals actually produces.
|
|
||||||
"""
|
|
||||||
import numpy as np
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
|
|
||||||
# ---- the key, same numbers as faceted_ridge_key.scad
|
|
||||||
L, W, tf, H, pr, pf, hf = 12.0, 4.0, 0.45, 4.5, 0.22, 0.62, 0.35
|
|
||||||
Wf, xr0, xr1, Hf = W * tf, -L / 2 + L * pr, -L / 2 + L * pf, H * hf
|
|
||||||
|
|
||||||
V = np.array([(-L/2, -W, 0), (-L/2, W, 0), (L/2, Wf, 0), (L/2, -Wf, 0),
|
|
||||||
(xr0, 0, H), (xr1, 0, Hf)], dtype=float)
|
|
||||||
F = [[0, 1, 2, 3], [0, 4, 1], [0, 3, 5], [0, 5, 4], [1, 4, 5], [1, 5, 2], [3, 2, 5]]
|
|
||||||
|
|
||||||
LIGHT = np.array([0.35, -0.5, 0.78]) # a headlight-ish key light
|
|
||||||
LIGHT /= np.linalg.norm(LIGHT)
|
|
||||||
|
|
||||||
|
|
||||||
def look_at(eye, target, up=(0, 0, 1)):
|
|
||||||
f = np.array(target, float) - np.array(eye, float)
|
|
||||||
f /= np.linalg.norm(f)
|
|
||||||
up = np.array(up, float)
|
|
||||||
if abs(np.dot(f, up)) > 0.999:
|
|
||||||
up = np.array([0, 1, 0], float)
|
|
||||||
r = np.cross(f, up); r /= np.linalg.norm(r)
|
|
||||||
u = np.cross(r, f)
|
|
||||||
return r, u, f
|
|
||||||
|
|
||||||
|
|
||||||
def render(eye, target, path, size=(620, 460), scale=26.0, label=""):
|
|
||||||
r, u, f = look_at(eye, target)
|
|
||||||
eye = np.array(eye, float)
|
|
||||||
cam = np.stack([r, u, f]) # world -> camera rows
|
|
||||||
P = (V - eye) @ cam.T # orthographic: x,y screen, z depth
|
|
||||||
|
|
||||||
w, h = size
|
|
||||||
img = Image.new("RGB", size, (238, 240, 243))
|
|
||||||
d = ImageDraw.Draw(img)
|
|
||||||
|
|
||||||
def to_px(p):
|
|
||||||
return (w / 2 + p[0] * scale, h / 2 - p[1] * scale)
|
|
||||||
|
|
||||||
faces = []
|
|
||||||
for face in F:
|
|
||||||
pts = V[face]
|
|
||||||
n = np.cross(pts[1] - pts[0], pts[2] - pts[0])
|
|
||||||
n /= np.linalg.norm(n)
|
|
||||||
centre = pts.mean(axis=0)
|
|
||||||
if np.dot(n, centre - eye) > 0: # back-face cull
|
|
||||||
continue
|
|
||||||
depth = P[face][:, 2].mean()
|
|
||||||
lam = max(0.0, float(np.dot(n, LIGHT)))
|
|
||||||
shade = 0.22 + 0.78 * lam # flat: ONE value for the whole facet
|
|
||||||
col = tuple(int(255 * shade * c) for c in (0.86, 0.72, 0.35))
|
|
||||||
faces.append((depth, [to_px(P[i]) for i in face], col))
|
|
||||||
|
|
||||||
for _, poly, col in sorted(faces, key=lambda t: -t[0]): # painter's algorithm
|
|
||||||
d.polygon(poly, fill=col)
|
|
||||||
|
|
||||||
if label:
|
|
||||||
d.rectangle([8, 8, 8 + 9 * len(label), 30], fill=(255, 255, 255))
|
|
||||||
d.text((14, 14), label, fill=(20, 20, 20))
|
|
||||||
img.save(path)
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
t = (0, 0, H * 0.35)
|
|
||||||
views = [
|
|
||||||
((26, -22, 20), "iso: the reference view"),
|
|
||||||
((30, 0, 6), "ALONG +X (from the FRONT, low end)"),
|
|
||||||
((-30, 0, 6), "ALONG -X (from the BACK, tall end)"),
|
|
||||||
((0, 0, 34), "ALONG +Z (straight down the mating axis)"),
|
|
||||||
((2, -32, 5), "ALONG -Y (broadside, grazing)"),
|
|
||||||
]
|
|
||||||
for i, (eye, lab) in enumerate(views):
|
|
||||||
print(render(eye, t, f"rk-{i}.png", label=lab))
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Flat-shade an ASCII/binary STL from several directions.
|
|
||||||
|
|
||||||
Used to answer one question with a picture instead of an argument: does a RECESSED faceted
|
|
||||||
pocket read as an oriented feature, or does a concave feature collapse into a dark hole?
|
|
||||||
"""
|
|
||||||
import struct
|
|
||||||
import sys
|
|
||||||
import numpy as np
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
|
|
||||||
LIGHT = np.array([0.35, -0.5, 0.78]); LIGHT /= np.linalg.norm(LIGHT)
|
|
||||||
|
|
||||||
|
|
||||||
def load_stl(path):
|
|
||||||
data = open(path, "rb").read()
|
|
||||||
if data[:5] == b"solid" and b"facet" in data[:2000]:
|
|
||||||
tris, cur = [], []
|
|
||||||
for line in data.decode("ascii", "ignore").splitlines():
|
|
||||||
s = line.split()
|
|
||||||
if s and s[0] == "vertex":
|
|
||||||
cur.append([float(x) for x in s[1:4]])
|
|
||||||
if len(cur) == 3:
|
|
||||||
tris.append(cur); cur = []
|
|
||||||
return np.array(tris, dtype=float)
|
|
||||||
n = struct.unpack("<I", data[80:84])[0]
|
|
||||||
tris = np.empty((n, 3, 3), dtype=float)
|
|
||||||
off = 84
|
|
||||||
for i in range(n):
|
|
||||||
v = struct.unpack("<12f", data[off:off + 48])
|
|
||||||
tris[i] = np.array(v[3:12]).reshape(3, 3)
|
|
||||||
off += 50
|
|
||||||
return tris
|
|
||||||
|
|
||||||
|
|
||||||
def render(tris, eye, target, path, size=(620, 460), scale=14.0, label=""):
|
|
||||||
eye = np.array(eye, float); target = np.array(target, float)
|
|
||||||
f = target - eye; f /= np.linalg.norm(f)
|
|
||||||
up = np.array([0, 0, 1.0])
|
|
||||||
if abs(np.dot(f, up)) > 0.999: up = np.array([0, 1.0, 0])
|
|
||||||
r = np.cross(f, up); r /= np.linalg.norm(r)
|
|
||||||
u = np.cross(r, f)
|
|
||||||
cam = np.stack([r, u, f])
|
|
||||||
|
|
||||||
w, h = size
|
|
||||||
img = Image.new("RGB", size, (238, 240, 243)); d = ImageDraw.Draw(img)
|
|
||||||
faces = []
|
|
||||||
for t in tris:
|
|
||||||
n = np.cross(t[1] - t[0], t[2] - t[0])
|
|
||||||
ln = np.linalg.norm(n)
|
|
||||||
if ln < 1e-12: continue
|
|
||||||
n /= ln
|
|
||||||
c = t.mean(axis=0)
|
|
||||||
if np.dot(n, c - eye) > 0: continue # cull back faces
|
|
||||||
P = (t - eye) @ cam.T
|
|
||||||
lam = max(0.0, float(np.dot(n, LIGHT)))
|
|
||||||
shade = 0.20 + 0.80 * lam
|
|
||||||
col = tuple(int(255 * shade * ch) for ch in (0.86, 0.72, 0.35))
|
|
||||||
poly = [(w / 2 + p[0] * scale, h / 2 - p[1] * scale) for p in P]
|
|
||||||
faces.append((P[:, 2].mean(), poly, col))
|
|
||||||
for _, poly, col in sorted(faces, key=lambda x: -x[0]):
|
|
||||||
d.polygon(poly, fill=col)
|
|
||||||
if label:
|
|
||||||
d.rectangle([8, 8, 8 + 9 * len(label), 30], fill=(255, 255, 255))
|
|
||||||
d.text((14, 14), label, fill=(20, 20, 20))
|
|
||||||
img.save(path)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
tris = load_stl(sys.argv[1])
|
|
||||||
print("triangles:", len(tris))
|
|
||||||
views = [((26, -22, 20), "iso"), ((0, 0, 34), "straight down +Z"),
|
|
||||||
((4, -30, 9), "grazing"), ((-28, -10, 12), "from the tall end")]
|
|
||||||
for i, (eye, lab) in enumerate(views):
|
|
||||||
render(tris, eye, (0, 0, 0), f"fem-{i}.png", label=f"FEMALE POCKET — {lab}")
|
|
||||||
print(f"fem-{i}.png")
|
|
||||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 49 KiB |
@@ -1,142 +0,0 @@
|
|||||||
"""Reduce the bear face to the fewest marks that still read at glyph size — wi3z.
|
|
||||||
|
|
||||||
Geometry comes from bear_outline.json, which extract_outline.py pulled off the supplied male
|
|
||||||
B-rep's back plate: the outer wire IS the silhouette, the inner wires are the two eyes and the
|
|
||||||
muzzle opening. Nothing here is traced by eye.
|
|
||||||
|
|
||||||
The glyph is drawn IN the connector's plane, so a grazing view foreshortens it along one axis by
|
|
||||||
sin(elevation) — exactly what collapsed the disc's roll quadrant to 3 pixels at 10 deg. Every
|
|
||||||
candidate is therefore rendered at three elevations as well as three pixel sizes.
|
|
||||||
"""
|
|
||||||
import json, math, os
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
D = json.load(open(os.path.join(HERE, "bear_outline.json")))
|
|
||||||
|
|
||||||
def norm(pts):
|
|
||||||
"""Part frame (X right, Z down-negative) -> glyph frame (x right, y up), centred, unit height."""
|
|
||||||
p = [(x, -z) for x, z in pts]
|
|
||||||
return p
|
|
||||||
|
|
||||||
outer = norm(D["outer"])
|
|
||||||
holes = [norm(h["pts"]) for h in D["holes"]]
|
|
||||||
# the two Ø9.8 wires are the eyes; the wide one is the muzzle
|
|
||||||
eyes = [h for h, meta in zip(holes, D["holes"]) if meta["d"] < 20]
|
|
||||||
muzzle = [h for h, meta in zip(holes, D["holes"]) if meta["d"] >= 20]
|
|
||||||
|
|
||||||
ALL = outer + [p for h in holes for p in h]
|
|
||||||
xs = [p[0] for p in ALL]; ys = [p[1] for p in ALL]
|
|
||||||
CX, CY = (min(xs)+max(xs))/2, (min(ys)+max(ys))/2
|
|
||||||
SPAN = max(max(xs)-min(xs), max(ys)-min(ys))
|
|
||||||
def to_unit(pts): return [((x-CX)/SPAN, (y-CY)/SPAN) for x, y in pts]
|
|
||||||
|
|
||||||
def rdp(pts, eps):
|
|
||||||
"""Douglas-Peucker. Vertex count is the honest measure of 'how simplified'."""
|
|
||||||
if len(pts) < 3: return pts
|
|
||||||
ax, ay = pts[0]; bx, by = pts[-1]
|
|
||||||
dx, dy = bx-ax, by-ay
|
|
||||||
n = math.hypot(dx, dy)
|
|
||||||
best, bi = -1.0, 0
|
|
||||||
for i in range(1, len(pts)-1):
|
|
||||||
px, py = pts[i]
|
|
||||||
d = abs(dx*(ay-py) - (ax-px)*dy)/n if n > 1e-12 else math.hypot(px-ax, py-ay)
|
|
||||||
if d > best: best, bi = d, i
|
|
||||||
if best <= eps:
|
|
||||||
return [pts[0], pts[-1]]
|
|
||||||
return rdp(pts[:bi+1], eps)[:-1] + rdp(pts[bi:], eps)
|
|
||||||
|
|
||||||
def simp_closed(pts, eps):
|
|
||||||
r = rdp(pts + [pts[0]], eps)
|
|
||||||
return r[:-1]
|
|
||||||
|
|
||||||
def centroid(pts):
|
|
||||||
return (sum(p[0] for p in pts)/len(pts), sum(p[1] for p in pts)/len(pts))
|
|
||||||
|
|
||||||
U_OUT = to_unit(outer)
|
|
||||||
U_EYE = [to_unit(e) for e in eyes]
|
|
||||||
U_MUZ = [to_unit(m) for m in muzzle]
|
|
||||||
|
|
||||||
def eye_dots(scale=1.0):
|
|
||||||
out = []
|
|
||||||
for e in U_EYE:
|
|
||||||
cx, cy = centroid(e)
|
|
||||||
r = max(max(p[0] for p in e)-min(p[0] for p in e),
|
|
||||||
max(p[1] for p in e)-min(p[1] for p in e))/2*scale
|
|
||||||
out.append((cx, cy, r))
|
|
||||||
return out
|
|
||||||
|
|
||||||
def muzzle_tri():
|
|
||||||
"""The muzzle reduced to one filled triangle: its two lower corners and its apex."""
|
|
||||||
m = U_MUZ[0]
|
|
||||||
lo = min(p[1] for p in m); hi = max(p[1] for p in m)
|
|
||||||
bottom = [p for p in m if p[1] < lo + 0.06*(hi-lo)]
|
|
||||||
apex = max(m, key=lambda p: p[1])
|
|
||||||
return [min(bottom), max(bottom), apex]
|
|
||||||
|
|
||||||
CANDIDATES = {
|
|
||||||
"C0 full": dict(out=U_OUT, eyes=eye_dots(), muz=U_MUZ[0]),
|
|
||||||
"C1 eps .004": dict(out=simp_closed(U_OUT, .004), eyes=eye_dots(), muz=simp_closed(U_MUZ[0], .004)),
|
|
||||||
"C2 eps .012": dict(out=simp_closed(U_OUT, .012), eyes=eye_dots(), muz=muzzle_tri()),
|
|
||||||
"C3 eps .030": dict(out=simp_closed(U_OUT, .030), eyes=eye_dots(1.15), muz=muzzle_tri()),
|
|
||||||
"C4 no eyes": dict(out=simp_closed(U_OUT, .012), eyes=[], muz=muzzle_tri()),
|
|
||||||
}
|
|
||||||
|
|
||||||
def sym_report(pts, tol=0.02):
|
|
||||||
"""Trivial symmetry group is the property doing the work. If a simplification restores a
|
|
||||||
mirror or a 180 deg rotation, that simplification is wrong."""
|
|
||||||
def match(tf):
|
|
||||||
t = [tf(p) for p in pts]
|
|
||||||
hit = 0
|
|
||||||
for q in t:
|
|
||||||
if min(math.hypot(q[0]-p[0], q[1]-p[1]) for p in pts) <= tol: hit += 1
|
|
||||||
return hit, len(pts)
|
|
||||||
return {
|
|
||||||
"mirror-x": match(lambda p: (-p[0], p[1])),
|
|
||||||
"mirror-y": match(lambda p: ( p[0], -p[1])),
|
|
||||||
"rot-180": match(lambda p: (-p[0], -p[1])),
|
|
||||||
}
|
|
||||||
|
|
||||||
def render(c, px, elev_deg, supersample=8):
|
|
||||||
S = px*supersample
|
|
||||||
img = Image.new("L", (S, S), 0)
|
|
||||||
d = ImageDraw.Draw(img)
|
|
||||||
k = math.sin(math.radians(elev_deg))
|
|
||||||
def m(p):
|
|
||||||
return (S/2 + p[0]*S*0.92, S/2 - p[1]*S*0.92*k)
|
|
||||||
d.polygon([m(p) for p in c["out"]], fill=255)
|
|
||||||
if c["muz"]: d.polygon([m(p) for p in c["muz"]], fill=0)
|
|
||||||
for cx, cy, r in c["eyes"]:
|
|
||||||
a = m((cx-r, cy+r)); b = m((cx+r, cy-r))
|
|
||||||
d.ellipse([a[0], a[1], b[0], b[1]], fill=0)
|
|
||||||
return img.resize((px, px), Image.LANCZOS)
|
|
||||||
|
|
||||||
print(f"{'candidate':14} {'verts':>6} {'marks':>6} symmetry (matched/total, lower is better)")
|
|
||||||
print("-"*78)
|
|
||||||
for name, c in CANDIDATES.items():
|
|
||||||
s = sym_report(c["out"])
|
|
||||||
marks = 1 + (1 if c["muz"] else 0) + len(c["eyes"])
|
|
||||||
sym = " ".join(f"{k} {v[0]}/{v[1]}" for k, v in s.items())
|
|
||||||
print(f"{name:14} {len(c['out']):6} {marks:6} {sym}")
|
|
||||||
|
|
||||||
SIZES = [22, 32, 48]
|
|
||||||
ELEVS = [(90, "flat on"), (47, "47 deg"), (16, "16 deg"), (6, "6 deg")]
|
|
||||||
pad, cell = 8, 56
|
|
||||||
W = pad + len(SIZES)*len(ELEVS)*cell + pad
|
|
||||||
H = pad + len(CANDIDATES)*cell + pad
|
|
||||||
sheet = Image.new("RGB", (W, H), (24, 27, 32))
|
|
||||||
for r, (name, c) in enumerate(CANDIDATES.items()):
|
|
||||||
for ci, (elev, _) in enumerate(ELEVS):
|
|
||||||
for si, px in enumerate(SIZES):
|
|
||||||
g = render(c, px, elev)
|
|
||||||
tile = Image.new("RGB", (px, px), (24, 27, 32))
|
|
||||||
gold = Image.new("RGB", (px, px), (237, 168, 23))
|
|
||||||
tile.paste(gold, (0, 0), g)
|
|
||||||
x = pad + (ci*len(SIZES)+si)*cell + (cell-px)//2
|
|
||||||
y = pad + r*cell + (cell-px)//2
|
|
||||||
sheet.paste(tile, (x, y))
|
|
||||||
sheet = sheet.resize((W*2, H*2), Image.NEAREST)
|
|
||||||
sheet.save(os.path.join(HERE, "simplify-sheet.png"))
|
|
||||||
print("\ncolumns: " + " | ".join(f"{e[1]} @ 22/32/48px" for e in ELEVS))
|
|
||||||
print("rows: " + ", ".join(CANDIDATES))
|
|
||||||
print("WROTE simplify-sheet.png")
|
|
||||||
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 293 B |
|
Before Width: | Height: | Size: 204 B |
|
Before Width: | Height: | Size: 238 B |
|
Before Width: | Height: | Size: 359 B |
|
Before Width: | Height: | Size: 263 B |
|
Before Width: | Height: | Size: 314 B |
|
Before Width: | Height: | Size: 502 B |
|
Before Width: | Height: | Size: 344 B |
|
Before Width: | Height: | Size: 464 B |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@@ -1,74 +0,0 @@
|
|||||||
# Trim the boxy frame off the female so its outer shape is the bear face itself.
|
|
||||||
#
|
|
||||||
# Method: take the male's flat back face (the plane Y=0 -- that face IS the bear silhouette),
|
|
||||||
# offset its OUTER wire outward in 2D, extrude the result along the insertion axis, and keep only
|
|
||||||
# the part of the female inside it. Everything outside is the block frame and goes away.
|
|
||||||
#
|
|
||||||
# NOTE ON THE NUMBER. The pocket's side walls stand at +0.20 mm from the male outline, because that
|
|
||||||
# is the clearance. A trim boundary at +0.10 mm therefore falls INSIDE them by 0.10 mm and removes
|
|
||||||
# the side wall entirely rather than leaving a thin one. The script runs the requested value and
|
|
||||||
# then measures what is actually left, so the outcome is a number rather than an opinion; it also
|
|
||||||
# emits a second variant at an offset that leaves a printable wall, for comparison.
|
|
||||||
#
|
|
||||||
# Run: /snap/bin/freecad.cmd trim_female.py
|
|
||||||
|
|
||||||
import os, sys
|
|
||||||
import FreeCAD as App
|
|
||||||
import Part
|
|
||||||
from FreeCAD import Vector
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
MALE = os.path.join(HERE, "bear.step")
|
|
||||||
FEMALE = os.path.join(HERE, "BearConnector_Female.step")
|
|
||||||
|
|
||||||
REQUESTED = 0.10 # as asked
|
|
||||||
CLEARANCE = 0.20 # what the pocket was built with
|
|
||||||
SAFE_WALL = 1.60 # a wall that survives an FDM nozzle: clearance + ~1.4 mm
|
|
||||||
|
|
||||||
male = Part.Shape(); male.read(MALE); male = male.Solids[0]
|
|
||||||
fem = Part.Shape(); fem.read(FEMALE); fem = fem.Solids[0]
|
|
||||||
print(f"female in : {fem.Volume/1000:.2f} cm3, {len(fem.Faces)} faces")
|
|
||||||
|
|
||||||
# --- the bear silhouette: the male's flat back face at Y = 0
|
|
||||||
back = None
|
|
||||||
for f in male.Faces:
|
|
||||||
n = f.normalAt(0, 0)
|
|
||||||
if abs(f.CenterOfMass.y) < 1e-6 and abs(abs(n.y) - 1.0) < 1e-6:
|
|
||||||
if back is None or f.Area > back.Area:
|
|
||||||
back = f
|
|
||||||
if back is None:
|
|
||||||
print("FAIL: could not find the flat back face at Y=0"); sys.exit(1)
|
|
||||||
print(f"silhouette: back face area {back.Area:.1f} mm2, {len(back.Wires)} wires "
|
|
||||||
f"(outer + {len(back.Wires)-1} holes: eyes and mouth)")
|
|
||||||
|
|
||||||
fb = fem.BoundBox
|
|
||||||
y0, y1 = fb.YMin - 5.0, fb.YMax + 5.0
|
|
||||||
|
|
||||||
def trimmed(offset):
|
|
||||||
"""keep only the part of the female inside the silhouette grown by `offset`"""
|
|
||||||
wire = back.OuterWire
|
|
||||||
grown = wire.makeOffset2D(offset, join=2, fill=False, openResult=False, intersection=True)
|
|
||||||
face = Part.Face(Part.Wire(grown.Edges))
|
|
||||||
prism = face.extrude(Vector(0, y1 - y0, 0))
|
|
||||||
prism.translate(Vector(0, y0 - face.CenterOfMass.y, 0))
|
|
||||||
return fem.common(prism)
|
|
||||||
|
|
||||||
for tag, off, out in (("requested", REQUESTED, "BearConnector_Female_Trimmed.step"),
|
|
||||||
("safe wall", SAFE_WALL, "BearConnector_Female_Trimmed_wall.step")):
|
|
||||||
r = trimmed(off)
|
|
||||||
if not r.Solids:
|
|
||||||
print(f"\n{tag} (+{off:.2f} mm): NOTHING LEFT"); continue
|
|
||||||
wall = off - CLEARANCE
|
|
||||||
# is there any material left at the level of the pocket's side wall?
|
|
||||||
sec = r.section(Part.makePlane(400, 400, Vector(-200, 1.5, -200), Vector(0, 1, 0)))
|
|
||||||
perim = sum(e.Length for e in sec.Edges)
|
|
||||||
print(f"\n{tag} (+{off:.2f} mm) wall = {wall:+.2f} mm")
|
|
||||||
print(f" volume {r.Volume/1000:.2f} cm3, {len(r.Solids)} solid(s), {len(r.Faces)} faces")
|
|
||||||
print(f" section through the pocket wall at Y=1.5: {perim:.1f} mm of edge")
|
|
||||||
if wall <= 0:
|
|
||||||
print(f" -> the trim cuts {abs(wall):.2f} mm INSIDE the pocket wall: no side wall remains")
|
|
||||||
doc = App.newDocument(tag.replace(" ", "_"))
|
|
||||||
o = doc.addObject("Part::Feature", "Female")
|
|
||||||
o.Shape = r; doc.recompute()
|
|
||||||
Part.export([o], os.path.join(HERE, out))
|
|
||||||
print(f" wrote {out}")
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Check both trimmed females still fit the male, and export STLs for a visual comparison.
|
|
||||||
# Run: /snap/bin/freecad.cmd verify_trimmed.py
|
|
||||||
import os
|
|
||||||
import Mesh, Part
|
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
male = Part.Shape(); male.read(os.path.join(HERE, "bear.step")); male = male.Solids[0]
|
|
||||||
|
|
||||||
for name in ("BearConnector_Female_Trimmed", "BearConnector_Female_Trimmed_wall"):
|
|
||||||
p = os.path.join(HERE, name + ".step")
|
|
||||||
s = Part.Shape(); s.read(p); s = s.Solids[0]
|
|
||||||
d = male.distToShape(s)[0]
|
|
||||||
c = male.common(s)
|
|
||||||
cv = c.Volume if c.Solids else 0.0
|
|
||||||
bb = s.BoundBox
|
|
||||||
print(f"{name}")
|
|
||||||
print(f" {bb.XLength:.2f} x {bb.YLength:.2f} x {bb.ZLength:.2f} mm, {s.Volume/1000:.2f} cm3, "
|
|
||||||
f"{len(s.Faces)} faces, valid={s.isValid()}")
|
|
||||||
print(f" gap to male {d:.4f} mm, interference {cv:.6f} mm3")
|
|
||||||
m = Mesh.Mesh(); m.addFacets([tuple(t) for t in s.tessellate(0.12)[1]] and
|
|
||||||
[(s.tessellate(0.12)[0][a], s.tessellate(0.12)[0][b],
|
|
||||||
s.tessellate(0.12)[0][c2])
|
|
||||||
for a, b, c2 in s.tessellate(0.12)[1]])
|
|
||||||
m.write(os.path.join(HERE, name + ".stl"))
|
|
||||||
print(f" wrote {name}.stl ({m.CountFacets} facets)")
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
# The Design tab
|
|
||||||
|
|
||||||
Object-driven parametric CAD inside the slicer. Point at geometry; the geometry offers the
|
|
||||||
verbs that apply to it. Selection comes first and the tool consumes it. Draw a sketch,
|
|
||||||
constrain it, turn it into a solid, refine it, and send it straight to Prepare — without
|
|
||||||
leaving for another application and coming back through an STL.
|
|
||||||
|
|
||||||
The model is a **recipe**, not a mesh. Every action becomes a feature in a tree that is
|
|
||||||
replayed from the start whenever anything changes, so editing a dimension you set twenty
|
|
||||||
steps ago rebuilds everything downstream. The geometry kernel is OCCT, which the slicer
|
|
||||||
already ships for STEP import.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
1. Open the **Design** tab.
|
|
||||||
2. Click a face or a reference plane in the viewport, then press `Shift+S` (Sketch). The offer
|
|
||||||
opens with the sketch tools on it.
|
|
||||||
3. Draw a closed profile, then press **✓ Confirm** in the floating action bar.
|
|
||||||
4. With the sketch selected, press `Shift+E` (Extrude).
|
|
||||||
5. Press **Commit to Plate** to hand the solid to Prepare.
|
|
||||||
|
|
||||||
The status line under the toolbar is the thing to watch: it says what the current tool is
|
|
||||||
waiting for. When no plane is picked it reads *"Click a face or a reference plane in the
|
|
||||||
viewport, then a sketch tool"*; once one is picked it reads *"Sketching on <face> — pick a
|
|
||||||
tool"*. It is also where a refusal explains itself.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Selecting
|
|
||||||
|
|
||||||
- One left-click selects what is under the cursor. There is no click-cycling through
|
|
||||||
face → edge → body.
|
|
||||||
- A click near a corner takes the corner, not the face behind it.
|
|
||||||
- Left-drag sweeps a rubber band, and a rubber band takes the whole body.
|
|
||||||
- An open sketch line can be clicked, even where it bounds a region.
|
|
||||||
- Double-click a sketch stroke to edit it — the gesture belongs on the geometry.
|
|
||||||
- Editing a dimension's value **updates** that dimension instead of adding a second one next
|
|
||||||
to it.
|
|
||||||
- The floating chrome that belongs to a sketch leaves with the sketch when it ends.
|
|
||||||
- Sketching happens on the face you clicked, first click.
|
|
||||||
- A sketch whose entities form no wire **fails** instead of extruding a default box. A
|
|
||||||
subtraction that removes nothing is reported as an error instead of a silent success.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The offer
|
|
||||||
|
|
||||||
Right-click on the geometry, released without moving the mouse (an 8 px budget — a
|
|
||||||
right-drag that orbits the camera does not open it). Left-click still only selects, so
|
|
||||||
pointing at things stays quiet.
|
|
||||||
|
|
||||||
The offer also opens by itself the moment you press Sketch on a face or plane, showing the
|
|
||||||
sketch tools — the app hands you the tools directly.
|
|
||||||
|
|
||||||
**Eight families, always in this fixed order:** Create, Add material, Remove, Dress-up,
|
|
||||||
Repeat, Transform, Reference, Modify.
|
|
||||||
|
|
||||||
- A family with at least one applicable verb shows it. Several applicable verbs collapse
|
|
||||||
into a submenu under the family name.
|
|
||||||
- A family with nothing applicable is **shown greyed in place, with the reason** — e.g.
|
|
||||||
*"Create — Click a face or a reference plane in the viewport, then a sketch tool"*. It is
|
|
||||||
not hidden. A control that cannot be used still says what it is and what you would have to
|
|
||||||
do first.
|
|
||||||
- Inside a sketch the offer shows the sketch verbs; outside it shows the feature verbs.
|
|
||||||
|
|
||||||
**Document-level actions never enter the offer**, because they act on the document and not
|
|
||||||
on a selection: Import STEP, Import mesh, Text, SVG, Export STEP, Commit to Plate, Undo,
|
|
||||||
Redo, Variables, Section view, Origin planes, World axes. They live in the toolbar.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Keyboard
|
|
||||||
|
|
||||||
Single letters drive sketch tools **while a sketch is open**; Shift+letter drives feature
|
|
||||||
tools and single letters drive view toggles **when no sketch is open**. The two maps are
|
|
||||||
selected by the mode, not by whether a sketch session is running.
|
|
||||||
|
|
||||||
### Sketch (while a sketch is open)
|
|
||||||
|
|
||||||
| Key | Tool |
|
|
||||||
|---|---|
|
|
||||||
| `L` | Line — click start, then end |
|
|
||||||
| `R` | Rectangle — click two opposite corners |
|
|
||||||
| `C` | Circle — click centre, then radius |
|
|
||||||
| `A` | Arc — click start, end, then a point |
|
|
||||||
| `S` | Slot — two centreline ends, then width |
|
|
||||||
| `E` | Ellipse — centre, major end, minor point |
|
|
||||||
| `B` | Spline — click control points |
|
|
||||||
| `P` | Point — click to place |
|
|
||||||
| `G` | Polygon — click centre, then a vertex |
|
|
||||||
| `D` | Dimension — click 2 points or an entity |
|
|
||||||
| `T` | Trim — click a segment to trim it |
|
|
||||||
| `X` | Extend — click a line/arc to extend it |
|
|
||||||
| `O` | Offset — pick an entity, drag the distance |
|
|
||||||
| `M` | Mirror — pick axis, then entities |
|
|
||||||
| `F` | Fillet — pick two lines, set the radius |
|
|
||||||
| `H` | Chamfer — pick two lines, set the distance |
|
|
||||||
| `K` | Constrain — finish the live sketch and enter constrain |
|
|
||||||
| `Q` | Construction toggle — draw the next entity as construction geometry |
|
|
||||||
| `Del` | Delete the selected sketch entity |
|
|
||||||
| `Esc` | Cancel the live tool |
|
|
||||||
|
|
||||||
### Feature (when no sketch is open)
|
|
||||||
|
|
||||||
| Key | Tool |
|
|
||||||
|---|---|
|
|
||||||
| `Shift+S` | Sketch |
|
|
||||||
| `Shift+E` | Extrude — extrude a profile, or push/pull a picked face |
|
|
||||||
| `Shift+R` | Revolve |
|
|
||||||
| `Shift+W` | Sweep |
|
|
||||||
| `Shift+L` | Loft |
|
|
||||||
| `Shift+N` | Pattern |
|
|
||||||
| `Shift+G` | Surface Extrude |
|
|
||||||
| `Shift+J` | Surface Revolve |
|
|
||||||
| `Shift+O` | Surface Loft |
|
|
||||||
| `Shift+Q` | Surface Fill |
|
|
||||||
| `Shift+U` | Surface Offset |
|
|
||||||
| `Shift+V` | Thicken Surface |
|
|
||||||
| `Shift+P` | Plane |
|
|
||||||
| `Shift+A` | Axis |
|
|
||||||
| `Shift+C` | Coord Sys |
|
|
||||||
| `Shift+Y` | Transform |
|
|
||||||
| `Shift+Z` | Mirror |
|
|
||||||
| `Shift+B` | Boolean |
|
|
||||||
| `Shift+X` | Cut |
|
|
||||||
| `Shift+F` | Fillet / Chamfer |
|
|
||||||
| `Shift+D` | Draft |
|
|
||||||
| `Shift+K` | Shell |
|
|
||||||
| `Shift+H` | Hole |
|
|
||||||
| `Shift+T` | Thread |
|
|
||||||
| `Shift+I` | Import STEP |
|
|
||||||
| `Shift+M` | Import mesh |
|
|
||||||
|
|
||||||
### View toggles (single letters, when no sketch is open)
|
|
||||||
|
|
||||||
| Key | Action |
|
|
||||||
|---|---|
|
|
||||||
| `Home` | Axonometric view, fitted to the model |
|
|
||||||
| `P` | Origin planes on/off |
|
|
||||||
| `A` | World axes on/off |
|
|
||||||
| `X` | Section view on/off |
|
|
||||||
|
|
||||||
While the section is on: `PageUp` / `PageDown` move the cut plane, `F` flips which half is
|
|
||||||
kept. With no section on, `F` is Place on Face — lay the picked face flat on the bed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Sketching
|
|
||||||
|
|
||||||
A sketch is a closed (or open) 2D profile on a plane or on a flat face of an existing body.
|
|
||||||
Press `Shift+S`, click the face or plane you want to sketch on, and draw. The toolbar and
|
|
||||||
the offer both carry the sketch tools.
|
|
||||||
|
|
||||||
**Entities:** line, polyline, rectangle (corner / centre / oblique / rounded), circle
|
|
||||||
(centre-radius / 2-point / 3-point), arc (centre-point / 3-point / tangent), ellipse and
|
|
||||||
elliptical arc, polygon (inscribed / circumscribed), slot (straight / arc), spline, point,
|
|
||||||
and text.
|
|
||||||
|
|
||||||
**Editing:** move, rotate, scale, trim, extend, offset, mirror, and linear or polar arrays.
|
|
||||||
|
|
||||||
**Constraints:** coincident, horizontal, vertical, parallel, perpendicular, tangent, equal,
|
|
||||||
concentric, midpoint, symmetric, fix, plus dimensional radius, diameter, distance and angle.
|
|
||||||
The solver reports the remaining degrees of freedom and tells you when a sketch is fully
|
|
||||||
constrained — or when a constraint conflicts with one already there.
|
|
||||||
|
|
||||||
Sketches stay editable. Selecting one in the feature tree reopens it with its dimensions
|
|
||||||
live.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Building solids
|
|
||||||
|
|
||||||
Grouped in the toolbar by what they do, one concept per drawer.
|
|
||||||
|
|
||||||
### Add material
|
|
||||||
| Tool | Shortcut | What it does |
|
|
||||||
|---|---|---|
|
|
||||||
| Extrude | `Shift+E` | Extrude a profile, or push/pull a face already on a body |
|
|
||||||
| Revolve | `Shift+R` | Revolve a profile about an axis |
|
|
||||||
| Sweep | `Shift+W` | Sweep a profile along a path — including a helix, for springs and augers |
|
|
||||||
| Loft | `Shift+L` | Skin between two or more profiles |
|
|
||||||
| Thicken | — | Offset a solid face into a thin plate as a new body |
|
|
||||||
| Rib | — | Grow a stiffening wall from an open sketch line, fused to a body |
|
|
||||||
|
|
||||||
Extrude offers blind, symmetric, two-sided, through-all and up-to-face end conditions, plus
|
|
||||||
a draft angle on the side wall, and can add, subtract, intersect or start a new body.
|
|
||||||
|
|
||||||
### Surface
|
|
||||||
Sheet bodies — surfaces with no thickness — for shapes that are easier to build as skins and
|
|
||||||
solidify afterwards.
|
|
||||||
|
|
||||||
| Tool | Shortcut |
|
|
||||||
|---|---|
|
|
||||||
| Surface Extrude | `Shift+G` |
|
|
||||||
| Surface Revolve | `Shift+J` |
|
|
||||||
| Surface Loft | `Shift+O` |
|
|
||||||
| Surface Fill | `Shift+Q` |
|
|
||||||
| Surface Offset | `Shift+U` |
|
|
||||||
| Thicken Surface | `Shift+V` |
|
|
||||||
|
|
||||||
Thicken Surface is how a sheet becomes a printable solid.
|
|
||||||
|
|
||||||
### Dress-up
|
|
||||||
| Tool | Shortcut |
|
|
||||||
|---|---|
|
|
||||||
| Fillet / Chamfer | `Shift+F` |
|
|
||||||
| Draft (taper a face) | `Shift+D` |
|
|
||||||
| Shell | `Shift+K` |
|
|
||||||
| Delete Face | — |
|
|
||||||
|
|
||||||
Delete Face removes faces and heals the solid — useful for stripping a feature off an
|
|
||||||
imported part.
|
|
||||||
|
|
||||||
### Holes
|
|
||||||
**Hole** (`Shift+H`) drills simple, counterbored or countersunk holes, with an ISO/ANSI
|
|
||||||
standards table so you can ask for an M6 clearance hole instead of computing a diameter.
|
|
||||||
**Thread** (`Shift+T`) cuts a real helical thread into a bore or onto a shaft.
|
|
||||||
|
|
||||||
### Placement
|
|
||||||
Operations that move a body without changing its shape: **Transform** (`Shift+Y`),
|
|
||||||
**Mirror** (`Shift+Z`), and **Mate** for assemblies.
|
|
||||||
|
|
||||||
### Combining
|
|
||||||
**Boolean** (`Shift+B`) unions, subtracts or intersects two bodies. **Cut** (`Shift+X`)
|
|
||||||
splits a body with a plane. **Pattern** (`Shift+N`) repeats a body linearly, in a circle, or
|
|
||||||
along a curve.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Reference geometry
|
|
||||||
|
|
||||||
Datum features carry no material; they exist to give later features something to attach to.
|
|
||||||
|
|
||||||
- **Plane** (`Shift+P`) — offset, tilted, midplane, tangent, through two edges, or coincident
|
|
||||||
- **Axis** (`Shift+A`) — two points, a face normal, a cylinder centreline, the intersection of
|
|
||||||
two planes, or along an edge
|
|
||||||
- **Coord Sys** (`Shift+C`) — a full frame, from a world point or from a face plus a
|
|
||||||
direction edge
|
|
||||||
- **Helix** — a helical curve to sweep along
|
|
||||||
- **Project** — project a body's edges onto a plane as sketch geometry
|
|
||||||
|
|
||||||
On the Coord Sys tool, picking a direction **edge** is worth the extra click: without one the
|
|
||||||
frame takes its X from the face's first edge, which is deterministic but not necessarily the
|
|
||||||
direction you meant.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Assemblies
|
|
||||||
|
|
||||||
**Mate** aligns two coordinate systems and moves one body onto the other. Five kinds:
|
|
||||||
|
|
||||||
| Kind | Leaves free |
|
|
||||||
|---|---|
|
|
||||||
| Fastened | nothing — 6 DOF locked |
|
|
||||||
| Planar | sliding in the plane |
|
|
||||||
| Revolute | rotation about the axis |
|
|
||||||
| Slider | sliding along the axis |
|
|
||||||
| Cylindrical | rotation *and* sliding |
|
|
||||||
|
|
||||||
**Check interference** reports every overlapping pair of solids with the overlapping volume,
|
|
||||||
so a clash is a number rather than an impression. Bodies that merely touch enclose no volume
|
|
||||||
and are not reported.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Variables and expressions
|
|
||||||
|
|
||||||
Define named variables and drive dimensions from them. Any numeric field accepts an
|
|
||||||
expression — `width/2`, `wall*3` — and everything re-evaluates on recompute. Change one
|
|
||||||
variable and the whole model follows.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Import and export
|
|
||||||
|
|
||||||
**Import STEP** brings in a real B-rep solid, not a mesh: its faces and edges can be filleted,
|
|
||||||
shelled and cut like anything modelled here.
|
|
||||||
|
|
||||||
**Import mesh** (STL/OBJ) converts triangles to a B-rep body and tells you honestly what it
|
|
||||||
got — whether the result is a closed solid or an open shell, with the boundary and
|
|
||||||
non-manifold edge counts. A large mesh becomes a large number of faces, which is slow to
|
|
||||||
edit; the importer warns before you commit to it.
|
|
||||||
|
|
||||||
**Export STEP** writes the model out for another CAD tool.
|
|
||||||
|
|
||||||
**Commit to Plate** sends the solid to Prepare for slicing. The whole feature recipe is saved
|
|
||||||
inside the 3MF, so reopening the project restores the editable model rather than a frozen
|
|
||||||
mesh.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## View controls
|
|
||||||
|
|
||||||
**Section view** (`X`) hides half the model so you can see inside — `PageUp`/`PageDown` move
|
|
||||||
the plane, `F` flips which half is kept. **Place on Face** (`F`, when section is off) lays a
|
|
||||||
picked face flat on the bed. Origin planes (`P`) and world axes (`A`) can be toggled on while
|
|
||||||
you orient yourself.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
Being straight about the edges, so nobody discovers them the hard way:
|
|
||||||
|
|
||||||
- **Rib** needs a sketch containing an explicit open line. A parametric rectangle sketch
|
|
||||||
carries no individual entities, so Rib cannot use one.
|
|
||||||
- **Surface Loft** and **Surface Fill** have kernel tests but have not been exercised by hand.
|
|
||||||
- Card wiring for 9 of the 16 late-wired tools has never been click-tested.
|
|
||||||
- Mate resolves by composing transforms directly. There is no 3D assembly solver, so mates
|
|
||||||
are applied in order rather than solved simultaneously, and mate limits are not implemented.
|
|
||||||
- Move-face and replace-face are not implemented — OCCT offers no clean primitive for them.
|
|
||||||
- There is no automated GUI test in CI. Every behaviour above is traced to code and to a
|
|
||||||
hand pass, not to a synthetic click.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Where the code lives
|
|
||||||
|
|
||||||
| Path | Role |
|
|
||||||
|---|---|
|
|
||||||
| `src/libslic3r/CAD/CadDocument.*` | the feature recipe and its replay |
|
|
||||||
| `src/libslic3r/CAD/GeometryEngine.*` | OCCT wrapper — faces, edges, booleans, healing |
|
|
||||||
| `src/libslic3r/CAD/SketchEngine.*` | profile → wire → solid |
|
|
||||||
| `src/libslic3r/CAD/SketchSolver.*` | constraint solving, over the vendored solver |
|
|
||||||
| `src/libslic3r/slvs/` | vendored 2D constraint solver (GPLv3) |
|
|
||||||
| `src/slic3r/GUI/CAD/DesignPanel.*` | the tab: toolbar, cards, feature tree |
|
|
||||||
| `src/slic3r/GUI/CAD/DesignCanvas.*` | viewport integration |
|
|
||||||
| `src/slic3r/GUI/CAD/DesignSketchTool.*` | in-canvas sketching |
|
|
||||||
|
|
||||||
Build with `-DSLIC3R_CAD=ON` (the default). With it OFF the tab is not compiled and the deps
|
|
||||||
prefix matches upstream exactly — see [cad_dependency_weight.md](cad_dependency_weight.md).
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Design (CAD) tab — upstream pull request
|
|
||||||
|
|
||||||
## What this adds
|
|
||||||
|
|
||||||
A sketch-first parametric CAD tab inside the slicer. The workflow is direct:
|
|
||||||
sketch → constrain → solid features → commit to plate. The whole feature recipe is
|
|
||||||
persisted inside the 3MF, so reopening restores an editable model rather than a frozen mesh.
|
|
||||||
|
|
||||||
- Kernel: OCCT, which upstream already links for STEP import — see
|
|
||||||
[cad_dependency_weight.md](docs/cad_dependency_weight.md)
|
|
||||||
- Constraint solver: vendored SolveSpace `libslvs` subset
|
|
||||||
- Interaction model: object-driven — point at geometry, the geometry offers the verbs that
|
|
||||||
apply to it; see [cad_ux_guidelines.md](docs/cad_ux_guidelines.md)
|
|
||||||
- Full user-facing documentation: [design_tab.md](docs/design_tab.md)
|
|
||||||
|
|
||||||
## Why it belongs in the slicer
|
|
||||||
|
|
||||||
Every round trip through an external CAD tool costs a file export, a re-import, and the
|
|
||||||
design intent that both steps discard. A part modified after slicing should return to its
|
|
||||||
feature history, not to a mesh. Keeping the CAD model inside the slicer preserves that
|
|
||||||
loop — the nozzle diameter, the build volume and the material are known at design time.
|
|
||||||
|
|
||||||
For the integration case in full: [design_tab_upstream_portability.md](docs/design_tab_upstream_portability.md).
|
|
||||||
|
|
||||||
## How it is built
|
|
||||||
|
|
||||||
The `SLIC3R_CAD` CMake flag (default ON) gates the entire tab. With it OFF the tab is not
|
|
||||||
compiled and the deps prefix matches upstream exactly — the dependency diff is one line in
|
|
||||||
OCCT's CMake: `BUILD_MODULE_ModelingAlgorithms=OFF → ON`.
|
|
||||||
|
|
||||||
Measured cost table: [cad_dependency_weight.md](docs/cad_dependency_weight.md).
|
|
||||||
|
|
||||||
## Diff shape
|
|
||||||
|
|
||||||
<!-- fork-specific: measured against this fork's upstream base; re-run the commands above after mirroring -->
|
|
||||||
|
|
||||||
Against merge-base `d6cb667b894f`:
|
|
||||||
|
|
||||||
306 files changed, 83032 insertions(+), 777 deletions(-)
|
|
||||||
|
|
||||||
350 commits, of which 284 are new files and 37 modify upstream files. 99.3 % of the diff
|
|
||||||
is new code. The negotiable surface is the 37 modified files.
|
|
||||||
|
|
||||||
## Tests
|
|
||||||
|
|
||||||
205 `TEST_CASE` blocks across 6 new test source files. This counts assertions written, not
|
|
||||||
assertions passed — a run needs a build.
|
|
||||||
|
|
||||||
`scripts/CAD/run-kernel-tests.sh` is the headless verification contract: it builds only
|
|
||||||
`libslic3r_tests` (not the GUI app), needs no display, and exit 0 means the CAD suite
|
|
||||||
passed. It now runs with **no exclusions** — both cases that used to be quarantined (the
|
|
||||||
circle-line tangency solver abort and the internal-thread reference) are fixed.
|
|
||||||
|
|
||||||
## Licensing
|
|
||||||
|
|
||||||
The vendored solver in `src/libslic3r/slvs/` is **GPL-3.0** (see `src/libslic3r/slvs/LICENSE`),
|
|
||||||
not LGPL. The combined work is distributable under AGPL-3.0. See the Licensing section of
|
|
||||||
[design_tab_upstream_portability.md](docs/design_tab_upstream_portability.md) for the
|
|
||||||
AGPLv3/GPLv3 compatibility argument; this point should be confirmed with upstream explicitly.
|
|
||||||
|
|
||||||
## Not verified
|
|
||||||
|
|
||||||
- Card wiring for 9 of the 16 late-wired tools was never click-tested.
|
|
||||||
- There is no automated GUI test in CI. A green kernel run says nothing about the GUI —
|
|
||||||
synthetic clicks never drift, so the test suite and the viewport are two separate realities.
|
|
||||||
- The click-test defect rate has **not converged**: a second pass found no new defects, but
|
|
||||||
four further days of work found five more. The earlier pass is not evidence of stability.
|
|
||||||
|
|
||||||
## Reviewer's map
|
|
||||||
|
|
||||||
See the [Where the code lives](docs/design_tab.md#where-the-code-lives) table in the user
|
|
||||||
doc for the file-to-role mapping, and [docs/ux/tool_atlas.json](docs/ux/tool_atlas.json) as
|
|
||||||
the generated-from source of `src/slic3r/GUI/CAD/DesignOffer.hpp`.
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
# Design (CAD) tab — upstream integration brief
|
|
||||||
|
|
||||||
**Question:** can the Design tab (sketch-first parametric CAD: sketch → constrain →
|
|
||||||
extrude/revolve/fillet/hole/thread/shell, multi-body, undo, 3MF persistence) land in
|
|
||||||
mainline OrcaSlicer?
|
|
||||||
|
|
||||||
**Answer: yes, and the ask is far smaller than previously believed.** OCCT is *already*
|
|
||||||
an OrcaSlicer dependency. We are not asking upstream to adopt a new library; we are
|
|
||||||
asking it to widen one it already builds, at a measured cost of **3.77 MiB on Windows**.
|
|
||||||
|
|
||||||
> ### Corrections to the 2026-06-21 assessment
|
|
||||||
> That revision was written before the persistence work landed and got two load-bearing
|
|
||||||
> facts wrong. Both are corrected here from direct measurement of the branch:
|
|
||||||
>
|
|
||||||
> 1. **"The real blocker: OCCT … a dependency mainline OrcaSlicer has never carried."**
|
|
||||||
> **False.** `deps/OCCT/` exists at the merge-base and upstream links it from
|
|
||||||
> `Format/STEP.cpp`, `Format/svg.cpp`, and `Shape/TextShape.cpp`. Our entire
|
|
||||||
> dependency diff is **one line**: `BUILD_MODULE_ModelingAlgorithms=OFF → ON`.
|
|
||||||
> 2. **"vendored SolveSpace solver … LGPL."** **False.** `src/libslic3r/slvs/LICENSE` is
|
|
||||||
> **GPL-3.0**, not LGPL. This is fine (see Licensing) but must not be misstated.
|
|
||||||
>
|
|
||||||
> It also claimed "no changes to Model" — no longer true; 3MF recipe persistence adds one
|
|
||||||
> `std::string` to `Model`.
|
|
||||||
|
|
||||||
## Measured shape of the change
|
|
||||||
|
|
||||||
Against merge-base `449a4cf9fc` (34 commits ahead):
|
|
||||||
|
|
||||||
| | files | lines |
|
|
||||||
|---|---:|---:|
|
|
||||||
| **New files** | 138 | +61,720 |
|
|
||||||
| **Modified upstream files** | 23 | +457 / −75 |
|
|
||||||
| **Deleted upstream files** | 0 | — |
|
|
||||||
|
|
||||||
The 62 kLOC headline is inflated by localization. The feature itself:
|
|
||||||
|
|
||||||
| area | LOC | files |
|
|
||||||
|---|---:|---:|
|
|
||||||
| kernel (`src/libslic3r/`) | 15,828 | 37 |
|
|
||||||
| GUI (`src/slic3r/`) | 19,544 | 14 |
|
|
||||||
| tests (Catch2) | 2,567 | 6 |
|
|
||||||
| i18n (unrelated; strip from the CAD PR) | 23,438 | 77 |
|
|
||||||
|
|
||||||
**99.3 % of the diff is new files.** The negotiable surface is 457 added lines across 23
|
|
||||||
files, and nothing upstream is deleted. The largest single hook is `GLCanvas3D.cpp`
|
|
||||||
(+110/−2): an `m_design_sketch_tool` member plus render/mouse/key hooks, **every one
|
|
||||||
already null-guarded** — which is why the compile-time gate below is cheap.
|
|
||||||
|
|
||||||
No changes to the slicing pipeline (Print/PrintObject/Layer/GCode), Tab, or the
|
|
||||||
printer-profile/config system.
|
|
||||||
|
|
||||||
## The dependency ask, precisely
|
|
||||||
|
|
||||||
Not "adopt OCCT" — **widen the existing OCCT build**:
|
|
||||||
|
|
||||||
```diff
|
|
||||||
- -DBUILD_MODULE_ModelingAlgorithms=OFF
|
|
||||||
+ -DBUILD_MODULE_ModelingAlgorithms=ON
|
|
||||||
```
|
|
||||||
|
|
||||||
Cost, measured from the shipped Windows artifact (42 OCCT DLLs, 45.43 MiB total):
|
|
||||||
|
|
||||||
| toolkit | size | note |
|
|
||||||
|---|---:|---|
|
|
||||||
| `TKFillet.dll` | 2.02 MiB | only exists with the flag ON |
|
|
||||||
| `TKOffset.dll` | 1.75 MiB | only exists with the flag ON |
|
|
||||||
| **delta** | **3.77 MiB** | Windows only (OCCT is Shared on Win, Static elsewhere) |
|
|
||||||
|
|
||||||
`TKBool` is *not* part of the delta — upstream's `DataExchange` already pulls it in
|
|
||||||
transitively. On macOS/Linux OCCT links statically, so the cost is only the code actually
|
|
||||||
referenced, not a 3.77 MiB floor.
|
|
||||||
|
|
||||||
**Unmeasured, and we should measure before the call:** clean-deps build-time delta with
|
|
||||||
the flag ON vs OFF, and the resulting CI runner-minute cost. Do not guess these at him.
|
|
||||||
|
|
||||||
## Licensing
|
|
||||||
|
|
||||||
- Vendored solver `src/libslic3r/slvs/` — **GPL-3.0**, 9,339 LOC, © Jonathan Westhues,
|
|
||||||
a self-contained subset of SolveSpace (`libslvs`). No external dependencies.
|
|
||||||
- OrcaSlicer — **AGPL-3.0** (`LICENSE.txt`).
|
|
||||||
|
|
||||||
GPLv3 §13 expressly permits combining a GPLv3 work with an AGPLv3 work; AGPLv3 §13 grants
|
|
||||||
the converse. The combined work is distributable under AGPL-3.0 with the solver's GPLv3
|
|
||||||
terms preserved. This is a favourable direction (GPLv3 → into an AGPLv3 project), but it
|
|
||||||
is a point to **confirm explicitly with upstream**, not to assert unilaterally.
|
|
||||||
|
|
||||||
Open question for SoftFever: keep the solver **vendored** (current: pinned, no submodule,
|
|
||||||
no external build) or move it to `deps/` as a fetched external? Vendoring costs us
|
|
||||||
upstream-sync burden; `deps/` costs build complexity.
|
|
||||||
|
|
||||||
## The one irreversible decision: the 3MF format
|
|
||||||
|
|
||||||
Persistence adds an **optional** archive entry and one field:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// Model.hpp
|
|
||||||
std::string cad_recipe; // empty for non-CAD projects
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
Metadata/orca_cad.bin // written only when cad_recipe is non-empty
|
|
||||||
```
|
|
||||||
|
|
||||||
Readers that do not know the entry ignore it; writers skip it entirely when empty. So
|
|
||||||
existing projects are bit-identical and old readers are unaffected. Good.
|
|
||||||
|
|
||||||
**But the moment upstream ships this, it owns forward-compatibility forever.** Three
|
|
||||||
things should be settled *before* the first release, because none can be changed after:
|
|
||||||
|
|
||||||
1. **Name.** Renamed to `Metadata/orca_cad.bin`.
|
|
||||||
2. **Encoding.** The recipe is an opaque **cereal `PortableBinaryArchive`** blob whose
|
|
||||||
layout is the field order of `CadFeature::serialize`. Portable across endianness and
|
|
||||||
word size — *not* across a field reorder. Append-only is currently a convention held by
|
|
||||||
discipline, not by any check.
|
|
||||||
3. **Embedded BRep.** `Import` features embed OCCT's ASCII BRep for the imported solid,
|
|
||||||
which couples saved project files to an OCCT BRep revision. Alternative: re-import from
|
|
||||||
the source STEP and store only a reference. Worth deciding deliberately.
|
|
||||||
|
|
||||||
**Concrete gap we should close before the call.** `test_caddocument.cpp` covers the
|
|
||||||
in-memory round-trip and correctly refuses a version-999 blob — but there is **no
|
|
||||||
checked-in v1 fixture on disk**. A reordered field in `CadFeature::serialize` would pass
|
|
||||||
the entire suite while silently breaking every previously-saved project. Ship a golden
|
|
||||||
`.bin` fixture generated today plus a test that loads it; that is the only thing that will
|
|
||||||
hold the format still once real users have files.
|
|
||||||
|
|
||||||
## Proposed PR decomposition
|
|
||||||
|
|
||||||
35 kLOC in one PR is not reviewable. Behind the flag, slices 1–4 are behaviour-neutral for
|
|
||||||
existing users:
|
|
||||||
|
|
||||||
1. **Build gate + OCCT flag + Windows packaging guard.** `-DSLIC3R_CAD=ON/OFF`, default
|
|
||||||
**OFF**. Flips `ModelingAlgorithms=ON`. Includes the guard that asserts every linked
|
|
||||||
OCCT toolkit has a shipped DLL (already on both forks: `546cef5f42`). ← *this is what
|
|
||||||
makes SoftFever's "parallel build" a one-line CI matrix entry.*
|
|
||||||
2. **Vendored `slvs` solver** + its Catch2 tests. No GUI, no OCCT.
|
|
||||||
3. **CAD kernel** (`CadDocument`, `SketchEngine`, `GeometryEngine`, `Sketch*`) + kernel
|
|
||||||
tests. Headless, no GUI.
|
|
||||||
4. **3MF recipe persistence** + golden-fixture regression test.
|
|
||||||
5. **GUI Design tab** (`DesignPanel`, `DesignCanvas`, `DesignSketchTool`, `GLGizmoSketch`)
|
|
||||||
+ the 23 upstream hooks.
|
|
||||||
|
|
||||||
## Agenda for the call
|
|
||||||
|
|
||||||
Questions only SoftFever can answer:
|
|
||||||
|
|
||||||
- Does OrcaSlicer *want* to be a CAD-integrated slicer? (Strategic; everything else is mechanical.)
|
|
||||||
- Default of `SLIC3R_CAD` at merge time, and when it flips ON.
|
|
||||||
- Vendored solver vs `deps/` external; and confirmation of the GPLv3/AGPLv3 combination.
|
|
||||||
- Project-file format: neutral name, encoding, embedded-BRep policy, and who owns v1 forward-compat.
|
|
||||||
- Undo/redo: the Design tab has its own stack; integrate with Orca's snapshot system or keep separate?
|
|
||||||
- Does he want the i18n work (Romanian, +23 kLOC) as a wholly separate PR? (Yes, almost certainly.)
|
|
||||||
|
|
||||||
## Verdict
|
|
||||||
|
|
||||||
Portability **high**. The prior "does upstream want OCCT" framing was wrong — OCCT is
|
|
||||||
already there. What remains is a 3.77 MiB dependency widening, a compile-time gate that
|
|
||||||
the existing null-guards make cheap, and one file-format decision that must be made before
|
|
||||||
the first release rather than after.
|
|
||||||
|
|
||||||
---
|
|
||||||
*Revised 2026-07-10 from direct measurement of `cad-mainline` @ `546cef5f42` vs upstream
|
|
||||||
merge-base `449a4cf9fc`. Supersedes the 2026-06-21 read-only assessment.*
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
# Design tab — interaction model
|
|
||||||
|
|
||||||
The contract for Esc, the right mouse button, and the states between them. Code that changes any
|
|
||||||
of the three changes this file in the same commit.
|
|
||||||
|
|
||||||
## 1. The state machine
|
|
||||||
|
|
||||||
`src/slic3r/GUI/CAD/DesignInteraction.hpp` — a four-level LIFO stack. The enum value *is* the
|
|
||||||
depth, so "which level does this press belong to" is a comparison rather than a chain of
|
|
||||||
special cases spread over three files.
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
enum class CadLevel : int {
|
|
||||||
Idle = 0, // nothing transient is up: Esc clears the selection
|
|
||||||
Tool = 1, // a feature card / armed sketch tool / constrain session: Esc exits it
|
|
||||||
Gesture = 2, // an uncommitted delta (entity being drawn, body being dragged): Esc reverts it
|
|
||||||
Transient = 3, // a value field or a popup menu: Esc closes just that
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CadInteractionState { // the four bits routing actually needs
|
|
||||||
bool value_field_open{false};
|
|
||||||
bool gesture_active{false};
|
|
||||||
bool tool_armed{false};
|
|
||||||
bool has_selection{false};
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr CadLevel cad_escape_level(const CadInteractionState& s)
|
|
||||||
{
|
|
||||||
if (s.value_field_open) return CadLevel::Transient;
|
|
||||||
if (s.gesture_active) return CadLevel::Gesture;
|
|
||||||
if (s.tool_armed) return CadLevel::Tool;
|
|
||||||
return CadLevel::Idle;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The rule is a `constexpr` free function over a POD, not a method on the panel, so the ordering
|
|
||||||
that is the entire contract is checkable without a window, a GL context or an event loop. Five
|
|
||||||
`static_assert`s in the header do exactly that, at compile time.
|
|
||||||
|
|
||||||
**Strict invariant.** No level of Esc deletes a feature, discards a sketch that holds geometry,
|
|
||||||
or rolls history back. Destroying work needs a gesture that says so:
|
|
||||||
|
|
||||||
| To destroy | Gesture |
|
|
||||||
|---|---|
|
|
||||||
| a feature | Delete / Backspace on an explicit selection |
|
|
||||||
| a drawn sketch | the ribbon's ✗ Cancel, which asks first |
|
|
||||||
| the last committed change | Ctrl+Z |
|
|
||||||
|
|
||||||
## 2. Event routing
|
|
||||||
|
|
||||||
**`OnKeyDown(WXK_ESCAPE)`** — `DesignPanel`'s `wxEVT_CHAR_HOOK`, one line:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
if (key == WXK_ESCAPE) { escape(); return; }
|
|
||||||
```
|
|
||||||
|
|
||||||
Every Esc in the tab goes through it, whatever holds focus. `DesignPanel::escape_level()` answers
|
|
||||||
the four questions of `CadInteractionState` about this panel; `DesignPanel::escape()` acts on the
|
|
||||||
one level that answer names, and on no other:
|
|
||||||
|
|
||||||
| Level | What one press does | What it must not touch |
|
|
||||||
|---|---|---|
|
|
||||||
| `Transient` | close the value field (`cancel_value` / `inline_cancel`) | the tool, which stays armed |
|
|
||||||
| `Gesture` | drop the clicks of the entity being drawn, or put a moved body back at the pose it had when the gizmo appeared | everything already committed |
|
|
||||||
| `Tool` | discard a feature card's *candidate*; drop an armed sketch tool to Select; end Constrain | committed features; entities already drawn |
|
|
||||||
| `Idle` | clear the selection (model and sketch); leave a sketch session **only if it is empty** | a sketch holding geometry — it is left through Finish or Cancel |
|
|
||||||
|
|
||||||
A sketch *session* is deliberately not a `Tool`. It is the environment the Idle level lives in,
|
|
||||||
which is what makes the destructive path unrepresentable rather than merely unlikely.
|
|
||||||
|
|
||||||
**`OnRightDown` / `OnRightUp`** — `DesignCanvas::set_on_context_menu`, bound after `GLCanvas3D`'s
|
|
||||||
own handlers so it can consume the event before them:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
RIGHT_DOWN: remember the press position and the clock, then Skip() // the canvas still seeds the orbit
|
|
||||||
|
|
||||||
RIGHT_UP: terminated = sketch_tool.take_right_consumed(); // read-and-clear, always
|
|
||||||
is_click = drift <= 3 px && dt <= 200 ms; // both budgets, or it was navigation
|
|
||||||
if (callback && !terminated && !inline_busy && is_click) {
|
|
||||||
select_at_screen(press.x, press.y); // raycast at the PRESS, not the release
|
|
||||||
on_context_menu(ClientToScreen(press));
|
|
||||||
return; // consumed
|
|
||||||
}
|
|
||||||
Skip(); // orbit / pan / the handlers underneath
|
|
||||||
```
|
|
||||||
|
|
||||||
Two independent budgets because the two failure modes are independent: drift alone still popped a
|
|
||||||
menu at the end of a slow, careful orbit. `take_right_consumed()` is how a right-click that
|
|
||||||
already meant something to the armed sketch tool (terminate a chain, drop an edit-op) declines to
|
|
||||||
also mean "open a menu".
|
|
||||||
|
|
||||||
## 3. Transition table
|
|
||||||
|
|
||||||
`sel` = something is picked. Blank = the input does nothing at that state.
|
|
||||||
|
|
||||||
| State | Left-click | Right-click | Esc | Enter |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| **Idle — model view** | pick / escalate the pick | offer menu for what is under the cursor | clear the selection | — |
|
|
||||||
| **Idle — sketch, empty** | pick | sketch offer menu | leave the session (nothing to lose) | Finish sketch |
|
|
||||||
| **Idle — sketch, drawn** | pick | sketch offer menu | clear the selection; status says the sketch is kept | Finish sketch |
|
|
||||||
| **Tool — feature card** | pick the card's next reference | offer menu | discard the candidate, close the card | commit the feature |
|
|
||||||
| **Tool — sketch tool armed** | place the first point | drop the tool to Select | drop the tool to Select | — |
|
|
||||||
| **Tool — constrain** | pick an entity | offer menu | end the session | apply |
|
|
||||||
| **Gesture — drawing** | place the next point | terminate the chain (keep what is drawn) | drop the in-progress entity, tool stays armed | commit the entity as drawn |
|
|
||||||
| **Gesture — moving a body** | drop the body here | end the move | revert to the pose at move-start | keep the placement |
|
|
||||||
| **Transient — value field** | — | — | close the field, tool stays armed | commit the value, advance the chain |
|
|
||||||
| **Transient — popup menu** | run the entry | — | close the menu | run the highlighted entry |
|
|
||||||
| **any** | — | — | *never* deletes, discards or rolls back | — |
|
|
||||||
|
|
||||||
Right-hold-and-drag is not in the table on purpose: past 3 px or 200 ms it is navigation, and
|
|
||||||
navigation does not transition the state machine.
|
|
||||||
|
|
||||||
## 4. Visual scaffolding
|
|
||||||
|
|
||||||
Entering a sketch changes three things at once, so the state is legible from across the room:
|
|
||||||
|
|
||||||
- **Banner.** A teal strip across the top of the viewport: `Editing: Sketch N · N = look normal to
|
|
||||||
the plane · Finish or Cancel in the toolbar`. Indicator only — Confirm and Cancel stay on the one
|
|
||||||
ribbon action bar, per the Design UX contract. It is a sibling above the canvas, not a floating
|
|
||||||
child over it: a child window over a `wxGLCanvas` is a native window on GTK and does not reliably
|
|
||||||
stack over GL, and this banner's job is to be unmissable rather than clever.
|
|
||||||
- **The printer bed is muted.** A plate grid and a sketch grid are the same visual language, and
|
|
||||||
reading one as the other is how a sketch gets drawn against the wrong reference. The view
|
|
||||||
checkbox remains the stored preference and is restored on the way out; ticking it mid-sketch
|
|
||||||
still shows the bed, because that is a deliberate act and this is only a default.
|
|
||||||
- **`N` looks normal to the plane**, keeping the current zoom, with the plane's own y axis as up.
|
|
||||||
Sketch key map only — in Feature mode the navigator orb owns orientation.
|
|
||||||
|
|
||||||
## 5. Context menu content
|
|
||||||
|
|
||||||
The offer is generated from `docs/CAD/ux/tool_atlas.json`; its 8-row shape and permanent row
|
|
||||||
indices are ratified and are not changed here. Checked against the per-context vocabularies asked
|
|
||||||
for in the 2026-09-05 interaction brief, the atlas already carries all of them except two, both on
|
|
||||||
a planar face:
|
|
||||||
|
|
||||||
| Asked for | Status |
|
|
||||||
|---|---|
|
|
||||||
| Revolve on a planar face | **not offered, and should not be**: `revolve` accepts `sk_loop` only, because the kernel takes a sketch profile — a face is not one |
|
|
||||||
| Offset Face | offered as **Thicken** (`thicken`, accepts `face_planar`); `surf_offset` is the sheet-body verb and accepts `body_sheet` |
|
|
||||||
|
|
||||||
View and document actions — Zoom to Fit, View Isometric, Clear Selection, Finish Sketch, Normal to
|
|
||||||
Sketch — stay in chrome by the atlas's own rule: the offer describes verbs that consume a
|
|
||||||
*selection*, and these act on the document or the camera. Esc covers Clear Selection, `N` covers
|
|
||||||
Normal to Sketch, and the ribbon covers Finish.
|
|
||||||
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 8.4 KiB |