Merge remote-tracking branch 'origin/main' into merge-test/main-into-constraints
# Conflicts: # src/app/command_driver.rs # src/command.rs # src/ui/ribbon/mod.rs # src/ui/window/options.rs
No files matched your search
@@ -895,7 +895,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=dfd0de176a878c20a1aeb8d23b0b8265197b5f5c#dfd0de176a878c20a1aeb8d23b0b8265197b5f5c"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=0507b720fef47bd7da0d4f777f97f1743a4f4038#0507b720fef47bd7da0d4f777f97f1743a4f4038"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ rfd = "0.17"
|
|||||||
clap = { version = "4", features = ["derive", "string"] }
|
clap = { version = "4", features = ["derive", "string"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "dfd0de176a878c20a1aeb8d23b0b8265197b5f5c", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "0507b720fef47bd7da0d4f777f97f1743a4f4038", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Boundary" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 3h18v18H3z" stroke="#B4B6B9" stroke-dasharray="2 2"/><path d="M6 7h8v4h4v7H6z" stroke="#6DB7ED" stroke-width="1.7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 291 B |
@@ -1,4 +1,60 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Copy">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<rect x="4" y="5" width="11" height="12" rx="1" stroke="#B4B6B9" stroke-width="1.625" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
<svg
|
||||||
<rect x="9" y="8" width="11" height="12" rx="1" stroke="#6DB7ED" stroke-width="1.625" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
viewBox="0 0 24 24"
|
||||||
|
role="img"
|
||||||
|
aria-label="Copy"
|
||||||
|
version="1.1"
|
||||||
|
id="svg2"
|
||||||
|
sodipodi:docname="copy.svg"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs2" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview2"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:zoom="18.417169"
|
||||||
|
inkscape:cx="1.4660234"
|
||||||
|
inkscape:cy="10.289312"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="939"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg2" />
|
||||||
|
<rect
|
||||||
|
x="1"
|
||||||
|
y="1"
|
||||||
|
width="13.733437"
|
||||||
|
height="15.733437"
|
||||||
|
rx="1.2484941"
|
||||||
|
stroke="#b4b6b9"
|
||||||
|
stroke-width="1.625"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="none"
|
||||||
|
id="rect1"
|
||||||
|
style="stroke-width:2;stroke-dasharray:none" />
|
||||||
|
<rect
|
||||||
|
x="9.2077332"
|
||||||
|
y="8.2077332"
|
||||||
|
width="13.792267"
|
||||||
|
height="14.792267"
|
||||||
|
rx="1.2538424"
|
||||||
|
stroke="#6db7ed"
|
||||||
|
stroke-width="1.625"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="none"
|
||||||
|
id="rect2"
|
||||||
|
style="stroke-width:2;stroke-dasharray:none" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 395 B After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Divide" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 20L21 4" stroke="#B4B6B9" stroke-width="1.6"/><path d="M6 14l4 4M10 10l4 4M14 6l4 4" stroke="#6DB7ED" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 293 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Donut" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="8" stroke="#B4B6B9" stroke-width="5"/><circle cx="12" cy="12" r="5.5" stroke="#6DB7ED" stroke-width="1"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 292 B |
@@ -1,4 +1,58 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Erase">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<polygon points="5,16 13,6 19,12 11,21" stroke="#B4B6B9" stroke-width="1.625" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
<svg
|
||||||
<line x1="4" y1="21" x2="20" y2="21" stroke="#6DB7ED" stroke-width="1.625" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
viewBox="0 0 24 24"
|
||||||
|
role="img"
|
||||||
|
aria-label="Erase"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="erase.svg"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:zoom="28.051892"
|
||||||
|
inkscape:cx="6.0958454"
|
||||||
|
inkscape:cy="10.926179"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="939"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<line
|
||||||
|
x1="3.8424129"
|
||||||
|
y1="19.83783"
|
||||||
|
x2="20.157587"
|
||||||
|
y2="4.1621704"
|
||||||
|
stroke="#6db7ed"
|
||||||
|
stroke-width="3.01569"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="none"
|
||||||
|
id="line1"
|
||||||
|
style="fill:none;stroke:#ff0000;stroke-width:3;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<line
|
||||||
|
x1="3.8149595"
|
||||||
|
y1="4.1357937"
|
||||||
|
x2="20.185041"
|
||||||
|
y2="19.864206"
|
||||||
|
stroke="#6db7ed"
|
||||||
|
stroke-width="3.03332"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
fill="none"
|
||||||
|
id="line1-5"
|
||||||
|
style="stroke:#ff0000;stroke-width:3;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 377 B After Width: | Height: | Size: 1.6 KiB |
@@ -6,7 +6,7 @@
|
|||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg4"
|
id="svg4"
|
||||||
sodipodi:docname="extend.svg"
|
sodipodi:docname="extend.svg"
|
||||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#505050"
|
inkscape:deskcolor="#505050"
|
||||||
showgrid="true"
|
showgrid="true"
|
||||||
inkscape:zoom="11.265625"
|
inkscape:zoom="7.258968"
|
||||||
inkscape:cx="31.955617"
|
inkscape:cx="30.58286"
|
||||||
inkscape:cy="29.869626"
|
inkscape:cy="34.92232"
|
||||||
inkscape:window-width="1920"
|
inkscape:window-width="1920"
|
||||||
inkscape:window-height="939"
|
inkscape:window-height="939"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="0"
|
||||||
@@ -49,27 +49,27 @@
|
|||||||
</sodipodi:namedview>
|
</sodipodi:namedview>
|
||||||
<!-- Boundary line (horizontal) at top -->
|
<!-- Boundary line (horizontal) at top -->
|
||||||
<line
|
<line
|
||||||
x1="3"
|
x1="1.3321468"
|
||||||
y1="2.625"
|
y1="2.625"
|
||||||
x2="21"
|
x2="22.72109"
|
||||||
y2="2.625"
|
y2="2.625"
|
||||||
stroke="#B4B6B9"
|
stroke="#b4b6b9"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
id="line1"
|
id="line1"
|
||||||
style="stroke-width:2.25;stroke-dasharray:none" />
|
style="stroke-width:2.45268;stroke-dasharray:none" />
|
||||||
<!-- Left line: already reaches boundary -->
|
<!-- Left line: already reaches boundary -->
|
||||||
<!-- Right line: short (doesn't reach) -->
|
<!-- Right line: short (doesn't reach) -->
|
||||||
<line
|
<line
|
||||||
x1="12"
|
x1="12"
|
||||||
y1="14.0625"
|
y1="13.349622"
|
||||||
x2="12"
|
x2="12"
|
||||||
y2="21.5625"
|
y2="21.5625"
|
||||||
stroke="#B4B6B9"
|
stroke="#b4b6b9"
|
||||||
stroke-width="1.67705"
|
stroke-width="1.67705"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
id="line3"
|
id="line3"
|
||||||
style="stroke-width:2.25;stroke-dasharray:none" />
|
style="stroke-width:2.3545;stroke-dasharray:none" />
|
||||||
<!-- Extended portion (from short line up to boundary, blue) -->
|
<!-- Extended portion (from short line up to boundary, blue) -->
|
||||||
<line
|
<line
|
||||||
x1="12"
|
x1="12"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -1,18 +1,92 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<defs>
|
<svg
|
||||||
<clipPath id="sq">
|
viewBox="0 0 24 24"
|
||||||
<rect x="4" y="4" width="16" height="16"/>
|
width="64"
|
||||||
|
height="64"
|
||||||
|
version="1.1"
|
||||||
|
id="svg6"
|
||||||
|
sodipodi:docname="hatch_lines.svg"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview6"
|
||||||
|
pagecolor="#505050"
|
||||||
|
bordercolor="#eeeeee"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#505050"
|
||||||
|
inkscape:zoom="6.3152247"
|
||||||
|
inkscape:cx="34.044711"
|
||||||
|
inkscape:cy="32.698757"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="939"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg6" />
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<clipPath
|
||||||
|
id="sq">
|
||||||
|
<rect
|
||||||
|
x="4"
|
||||||
|
y="4"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
id="rect1" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
<!-- ANSI31: 45° diagonal hatch lines clipped to square -->
|
<!-- ANSI31: 45° diagonal hatch lines clipped to square -->
|
||||||
<g clip-path="url(#sq)">
|
<g
|
||||||
<line x1="-2" y1="22" x2="22" y2="-2" stroke="#6DB7ED" stroke-width="1.2"/>
|
clip-path="url(#sq)"
|
||||||
<line x1="2" y1="26" x2="26" y2="2" stroke="#6DB7ED" stroke-width="1.2"/>
|
id="g6"
|
||||||
<line x1="-6" y1="18" x2="18" y2="-6" stroke="#6DB7ED" stroke-width="1.2"/>
|
transform="matrix(1.4946751,0,0,1.5,-5.8935028,-5.999999)">
|
||||||
<line x1="-10" y1="14" x2="14" y2="-10" stroke="#6DB7ED" stroke-width="1.2"/>
|
<line
|
||||||
<line x1="6" y1="30" x2="30" y2="6" stroke="#6DB7ED" stroke-width="1.2"/>
|
x1="-2"
|
||||||
<line x1="10" y1="34" x2="34" y2="10" stroke="#6DB7ED" stroke-width="1.2"/>
|
y1="22"
|
||||||
|
x2="22"
|
||||||
|
y2="-2"
|
||||||
|
stroke="#6db7ed"
|
||||||
|
stroke-width="1.2"
|
||||||
|
id="line1" />
|
||||||
|
<line
|
||||||
|
x1="2"
|
||||||
|
y1="26"
|
||||||
|
x2="26"
|
||||||
|
y2="2"
|
||||||
|
stroke="#6db7ed"
|
||||||
|
stroke-width="1.2"
|
||||||
|
id="line2" />
|
||||||
|
<line
|
||||||
|
x1="-6"
|
||||||
|
y1="18"
|
||||||
|
x2="18"
|
||||||
|
y2="-6"
|
||||||
|
stroke="#6db7ed"
|
||||||
|
stroke-width="1.2"
|
||||||
|
id="line3" />
|
||||||
|
<line
|
||||||
|
x1="6"
|
||||||
|
y1="30"
|
||||||
|
x2="30"
|
||||||
|
y2="6"
|
||||||
|
stroke="#6db7ed"
|
||||||
|
stroke-width="1.2"
|
||||||
|
id="line5" />
|
||||||
</g>
|
</g>
|
||||||
<!-- Square boundary -->
|
<!-- Square boundary -->
|
||||||
<rect x="4" y="4" width="16" height="16" stroke="#B4B6B9" stroke-width="1.5" fill="none"/>
|
<rect
|
||||||
|
x="0.87927544"
|
||||||
|
y="0.87927294"
|
||||||
|
width="22.241451"
|
||||||
|
height="22.241451"
|
||||||
|
stroke="#b4b6b9"
|
||||||
|
stroke-width="1.75855"
|
||||||
|
fill="none"
|
||||||
|
id="rect6" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 897 B After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Helix" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5 4C5 0 20 2 19 6C18 10 4 7 4 12C4 17 20 12 20 17C20 22 5 24 5 19C5 15 19 18 19 12C19 8 5 12 5 7" stroke="#B4B6B9" stroke-width="1.6"/><circle cx="5" cy="4" r="1.3" fill="#6DB7ED" stroke="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 365 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Measure" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 20L21 4M4 7h11M4 4v6M15 4v6" stroke="#B4B6B9" stroke-width="1.4"/><path d="M6 14l4 4M12 9l4 4" stroke="#6DB7ED" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 304 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Multiple Points" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 4h3v3H3zM16 2h3v3h-3zM10 11h3v3h-3zM3 19h3v3H3zM18 17h3v3h-3z" fill="#6DB7ED" stroke="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Ray" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 20L21 3" stroke="#B4B6B9" stroke-width="1.6"/><path d="M16 3h5v5" stroke="#6DB7ED" stroke-width="1.6"/><path d="M2 18h4v4H2z" fill="#6DB7ED" stroke="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 326 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Region" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 8L11 3L21 8V21H3Z" fill="#B4B6B9" stroke="none"/><circle cx="12" cy="13" r="4" fill="#6DB7ED" stroke="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 282 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Spline CV" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 18L6 4L18 4L21 18" stroke="#B4B6B9" stroke-dasharray="2 2"/><path d="M3 18C6 0 18 0 21 18" stroke="#B4B6B9" stroke-width="1.6"/><path d="M2 17h2v2H2zM5 3h2v2H5zM17 3h2v2h-2zM20 17h2v2h-2z" fill="#6DB7ED" stroke="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 395 B |
@@ -6,7 +6,7 @@
|
|||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg5"
|
id="svg5"
|
||||||
sodipodi:docname="stretch.svg"
|
sodipodi:docname="stretch.svg"
|
||||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -22,9 +22,9 @@
|
|||||||
inkscape:pageopacity="0"
|
inkscape:pageopacity="0"
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#505050"
|
inkscape:deskcolor="#505050"
|
||||||
inkscape:zoom="12.762909"
|
inkscape:zoom="11.968488"
|
||||||
inkscape:cx="33.926435"
|
inkscape:cx="25.984903"
|
||||||
inkscape:cy="28.010856"
|
inkscape:cy="32.794451"
|
||||||
inkscape:window-width="1920"
|
inkscape:window-width="1920"
|
||||||
inkscape:window-height="939"
|
inkscape:window-height="939"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="0"
|
||||||
@@ -48,12 +48,12 @@
|
|||||||
visible="true" />
|
visible="true" />
|
||||||
</sodipodi:namedview>
|
</sodipodi:namedview>
|
||||||
<rect
|
<rect
|
||||||
style="fill:none;stroke:#B4B6B9;stroke-width:2.25;stroke-dasharray:none"
|
style="fill:none;stroke:#b4b6b9;stroke-width:2.70159;stroke-dasharray:none"
|
||||||
id="rect5"
|
id="rect5"
|
||||||
width="12.75"
|
width="12.298418"
|
||||||
height="14.25"
|
height="21.298416"
|
||||||
x="1.125"
|
x="1.3507915"
|
||||||
y="4.875"
|
y="1.3507915"
|
||||||
ry="0" />
|
ry="0" />
|
||||||
<!-- Original right edge (ghost, dashed) -->
|
<!-- Original right edge (ghost, dashed) -->
|
||||||
<!-- Left/top/bottom of rect (static part, solid gray) -->
|
<!-- Left/top/bottom of rect (static part, solid gray) -->
|
||||||
@@ -61,8 +61,8 @@
|
|||||||
<!-- Top line extension (stretched, blue) -->
|
<!-- Top line extension (stretched, blue) -->
|
||||||
<!-- Stretch arrow -->
|
<!-- Stretch arrow -->
|
||||||
<path
|
<path
|
||||||
style="fill:none;stroke:#6DB7ED;stroke-width:2.25;stroke-dasharray:none;stroke-opacity:1"
|
style="fill:none;stroke:#6db7ed;stroke-width:2.7102;stroke-dasharray:none;stroke-opacity:1"
|
||||||
d="m 15,4.875 h 7.875 v 14.25 H 15"
|
d="m 14.999617,1.3549399 h 7.647048 V 22.646516 h -7.647048"
|
||||||
id="path8"
|
id="path8"
|
||||||
sodipodi:nodetypes="cccc" />
|
sodipodi:nodetypes="cccc" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.0 KiB |
@@ -6,7 +6,7 @@
|
|||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg6"
|
id="svg6"
|
||||||
sodipodi:docname="trim.svg"
|
sodipodi:docname="trim.svg"
|
||||||
inkscape:version="1.4.3 (0d15f75042, 2025-12-25)"
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -22,9 +22,9 @@
|
|||||||
inkscape:pageopacity="0"
|
inkscape:pageopacity="0"
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#505050"
|
inkscape:deskcolor="#505050"
|
||||||
inkscape:zoom="11.265625"
|
inkscape:zoom="6.1823588"
|
||||||
inkscape:cx="31.955617"
|
inkscape:cx="16.498557"
|
||||||
inkscape:cy="32"
|
inkscape:cy="24.181709"
|
||||||
inkscape:window-width="1920"
|
inkscape:window-width="1920"
|
||||||
inkscape:window-height="939"
|
inkscape:window-height="939"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="0"
|
||||||
@@ -32,32 +32,32 @@
|
|||||||
inkscape:window-maximized="1"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="svg6" />
|
inkscape:current-layer="svg6" />
|
||||||
<path
|
<path
|
||||||
style="fill:#ff0000;fill-opacity:1;stroke:#ff0000;stroke-width:2.25;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stroke-linecap:round"
|
style="fill:#ff0000;fill-opacity:1;stroke:#ff0000;stroke-width:2.62057;stroke-linecap:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||||
d="M 12,12 V 2.0305132"
|
d="M 12.000002,12.696386 V 1.3102848"
|
||||||
id="path6" />
|
id="path6" />
|
||||||
<!-- Cutting boundary (horizontal line) -->
|
<!-- Cutting boundary (horizontal line) -->
|
||||||
<line
|
<line
|
||||||
x1="3"
|
x1="1.3102847"
|
||||||
y1="11.75"
|
y1="12.410862"
|
||||||
x2="21"
|
x2="22.689716"
|
||||||
y2="11.75"
|
y2="12.410862"
|
||||||
stroke="#B4B6B9"
|
stroke="#b4b6b9"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
id="line1"
|
id="line1"
|
||||||
style="stroke-width:2.25;stroke-dasharray:none" />
|
style="stroke-width:2.62057;stroke-dasharray:none" />
|
||||||
<!-- Left vertical line (full, not trimmed) -->
|
<!-- Left vertical line (full, not trimmed) -->
|
||||||
<!-- Right vertical line: lower portion kept -->
|
<!-- Right vertical line: lower portion kept -->
|
||||||
<line
|
<line
|
||||||
x1="12"
|
x1="12.000001"
|
||||||
y1="11.75"
|
y1="12.410862"
|
||||||
x2="12"
|
x2="12.000001"
|
||||||
y2="20.75"
|
y2="22.689716"
|
||||||
stroke="#B4B6B9"
|
stroke="#b4b6b9"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
id="line3"
|
id="line3"
|
||||||
style="stroke-width:2.25;stroke-dasharray:none" />
|
style="stroke-width:2.62057;stroke-dasharray:none" />
|
||||||
<!-- Right vertical line: upper portion being trimmed (dashed = removed) -->
|
<!-- Right vertical line: upper portion being trimmed (dashed = removed) -->
|
||||||
<!-- Trim cut indicator (X mark on trimmed segment) -->
|
<!-- Trim cut indicator (X mark on trimmed segment) -->
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Construction Line" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M2 22L22 2" stroke="#B4B6B9" stroke-width="1.6"/><path d="M2 17v5h5M17 2h5v5" stroke="#6DB7ED" stroke-width="1.6"/><path d="M10 10h4v4h-4z" fill="#6DB7ED" stroke="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 351 B |
@@ -4,6 +4,12 @@
|
|||||||
`Transport::apply` carries one operation or a read-only query batch to the
|
`Transport::apply` carries one operation or a read-only query batch to the
|
||||||
executor. The native adapter binds each request to its existing `HostSession`.
|
executor. The native adapter binds each request to its existing `HostSession`.
|
||||||
|
|
||||||
|
For out-of-process / worker-thread plugins, `doc_api_for_host(host)` constructs a
|
||||||
|
`DocApi` backed by `OcsPluginApiIpc`, which sends `PluginRequest::DocApiRequest`
|
||||||
|
through the host's `PluginRequestSender` and deserializes
|
||||||
|
`PluginResponse::DocApiResponse`. This keeps plugin code free of manual envelope
|
||||||
|
serialization while reusing the same executor that the in-process transport uses.
|
||||||
|
|
||||||
The executor validates inputs and dispatches through `DocApiBackend`. Backends
|
The executor validates inputs and dispatches through `DocApiBackend`. Backends
|
||||||
must prepare fallible geometry and serialization before changing the document.
|
must prepare fallible geometry and serialization before changing the document.
|
||||||
Bulk writes prepare all entities, begin one undo record, apply the prepared
|
Bulk writes prepare all entities, begin one undo record, apply the prepared
|
||||||
@@ -22,14 +28,18 @@ do not reorder them. The consistency tests verify their bincode discriminants.
|
|||||||
generates the API reference and binding schema. The layout snapshot is curated
|
generates the API reference and binding schema. The layout snapshot is curated
|
||||||
vocabulary, not a reflected binary codec schema.
|
vocabulary, not a reflected binary codec schema.
|
||||||
|
|
||||||
|
Layer CRUD is a document-table family, not an entity family. The API carries
|
||||||
|
layer properties in the plain-data `LayerInfo` DTO and the host translates them
|
||||||
|
to/from `acadrust::tables::Layer`. Layer names are case-insensitive. The host
|
||||||
|
rejects duplicate names, deletion of layer "0" or the current layer, and removal
|
||||||
|
of a layer that still has entities assigned.
|
||||||
|
|
||||||
|
XDATA and XRECORD are serialization roundtrip families: the API carries their
|
||||||
|
payloads as plain-data DTOs and the host stores/retrieves them without semantic
|
||||||
|
interpretation. XDATA records are keyed by registered application name on any
|
||||||
|
entity; XRECORD objects are standalone named dictionaries of group-code/value
|
||||||
|
entries. Both preserve exact bytes/values across save and reload.
|
||||||
|
|
||||||
To extend the API, update the DTO, backend, executor, facade and spec together,
|
To extend the API, update the DTO, backend, executor, facade and spec together,
|
||||||
add a native behavior test, and rebuild the generated reference/schema. New
|
add a native behavior test, and rebuild the generated reference/schema. New
|
||||||
backend methods must reject unsupported behavior explicitly.
|
backend methods must reject unsupported behavior explicitly.
|
||||||
|
|
||||||
Extension workflow:
|
|
||||||
|
|
||||||
1. Add the operation/query DTO and its name mapping, then update `spec/entities.toml`.
|
|
||||||
2. Implement validation, backend preparation, executor dispatch and facade methods.
|
|
||||||
3. Preserve each profile's plane and use cadkernel for geometry.
|
|
||||||
4. Test native behavior, failed-operation atomicity and the binding transport payload.
|
|
||||||
5. Run `cargo test -p ocs_doc_api --all-features` and rebuild the generated snapshots.
|
|
||||||
@@ -15,7 +15,7 @@ thiserror = "1"
|
|||||||
|
|
||||||
# Host adapters convert ObjectId to acadrust handles; kernel handles stay local.
|
# Host adapters convert ObjectId to acadrust handles; kernel handles stay local.
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"], optional = true }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "f23ffc873228e5f41b876485c741e4a8ac20a67d", features = ["serde"], optional = true }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "dfd0de176a878c20a1aeb8d23b0b8265197b5f5c", features = ["acis", "offset"], optional = true }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "0507b720fef47bd7da0d4f777f97f1743a4f4038", features = ["acis", "offset"], optional = true }
|
||||||
ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] }
|
ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
@@ -34,6 +34,8 @@ default = []
|
|||||||
host = ["acadrust", "cadkernel"]
|
host = ["acadrust", "cadkernel"]
|
||||||
# ipc: pull ocs_plugin_api for the IPC transport adapter (plugin side).
|
# ipc: pull ocs_plugin_api for the IPC transport adapter (plugin side).
|
||||||
ipc = ["acadrust", "ocs_plugin_api"]
|
ipc = ["acadrust", "ocs_plugin_api"]
|
||||||
|
# doc_api_host: convenience helper that builds a DocApi from a HostApi (IPC-only).
|
||||||
|
doc_api_host = ["ipc"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "ocs_doc_api"
|
name = "ocs_doc_api"
|
||||||
|
|||||||
@@ -30,11 +30,109 @@ Transports bind to one document tab. Typed handles from another transport and
|
|||||||
requests for another tab are rejected. Raw `ObjectId` values are relative to the
|
requests for another tab are rejected. Raw `ObjectId` values are relative to the
|
||||||
receiving document. Errors are serialized as `ApiError` variants over IPC.
|
receiving document. Errors are serialized as `ApiError` variants over IPC.
|
||||||
|
|
||||||
|
Layer CRUD is exposed through `Document::layers()`, `create_layer(info)`,
|
||||||
|
`update_layer(name, info)`, `delete_layer(name)` and through the `layer()` /
|
||||||
|
`set_layer(...)` methods on every entity handle (`Line`, `Circle`, `Solid`,
|
||||||
|
`Entity`, etc.). Layer properties are carried as the plain-data `LayerInfo` DTO
|
||||||
|
with `LayerFlags`, `Color` and `LineWeight`. The host enforces CAD-style rules:
|
||||||
|
layer "0" and the current layer cannot be deleted, duplicate names are rejected,
|
||||||
|
and a layer still referenced by entities cannot be removed.
|
||||||
|
|
||||||
|
XDATA and XRECORD payloads are carried as plain-data, serde-compatible DTOs
|
||||||
|
(`XDataRecord`, `XRecordSpec`, and their value/entry types) and roundtripped
|
||||||
|
verbatim by the host. Every `Entity` handle can read/write one XDATA record per
|
||||||
|
registered application name via `xdata(application_name)` and `set_xdata(...)`,
|
||||||
|
and standalone XRECORD objects can be created with `doc.entities().create_xrecord(...)`
|
||||||
|
and updated through the `XRecord` handle.
|
||||||
|
|
||||||
| Feature | Use |
|
| Feature | Use |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Default | DTOs, facade, transport trait and binding schema |
|
| Default | DTOs, facade, transport trait and binding schema |
|
||||||
| `host` | Kernel and entity adapters, executor, `DocApiBackend`, `InProcess` |
|
| `host` | Kernel and entity adapters, executor, `DocApiBackend`, `InProcess` |
|
||||||
| `ipc` | `OcsPluginApiIpc` adapter for a plugin connected to the host |
|
| `ipc` | `OcsPluginApiIpc` adapter for a plugin connected to the host |
|
||||||
|
| `doc_api_host` | Convenience `doc_api_for_host(&dyn HostApi) -> Option<DocApi>` for out-of-process / worker-thread plugins |
|
||||||
|
|
||||||
|
### Connecting from a plugin
|
||||||
|
|
||||||
|
For out-of-process or worker-thread plugins that receive `&dyn HostApi` (or any
|
||||||
|
`ocs_plugin_api::host::HostApi` implementor), enable the `doc_api_host` feature
|
||||||
|
and use the helper instead of manually serializing `DocApiEnvelope`s:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ocs_doc_api::doc_api_for_host;
|
||||||
|
use ocs_plugin_api::host::HostApi;
|
||||||
|
|
||||||
|
fn on_dispatch(host: &mut dyn HostApi) -> Option<()> {
|
||||||
|
let doc_api = doc_api_for_host(host)?;
|
||||||
|
let doc = doc_api.document(host.tab_id());
|
||||||
|
let cube = doc.solids().create_cuboid([0.0; 3], [10.0; 3]).ok()?;
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`doc_api_for_host` returns `None` for hosts that do not expose a
|
||||||
|
`PluginRequestSender` (for example the in-process `HostSession`). In-process code
|
||||||
|
should use `DocApi::in_process(backend, tab_id)` with a concrete
|
||||||
|
`DocApiBackend`.
|
||||||
|
|
||||||
|
### Plugin example: create, read, update and delete a line
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ocs_doc_api::{doc_api_for_host, ApiResult, Color, LayerInfo, LineWeight};
|
||||||
|
use ocs_plugin_api::host::HostApi;
|
||||||
|
|
||||||
|
fn draw_and_modify_line(host: &mut dyn HostApi) -> ApiResult<()> {
|
||||||
|
// Build a DocApi from the host's plugin request channel.
|
||||||
|
let api = doc_api_for_host(host).ok_or_else(|| {
|
||||||
|
ocs_doc_api::ApiError::Transport("host does not expose a DocApi channel".into())
|
||||||
|
})?;
|
||||||
|
let doc = api.document(api.active_tab());
|
||||||
|
|
||||||
|
// Ensure a target layer exists.
|
||||||
|
let mut info = LayerInfo::new("MyLayer");
|
||||||
|
info.color = Color::Index(1);
|
||||||
|
info.line_weight = LineWeight::Value(25);
|
||||||
|
let _ = doc.create_layer(&info); // ok if it already exists
|
||||||
|
|
||||||
|
// Create a line on that layer.
|
||||||
|
let line = doc.curves().create_line([0.0, 0.0, 0.0], [10.0, 0.0, 0.0])?;
|
||||||
|
line.set_layer("MyLayer")?;
|
||||||
|
|
||||||
|
// Read it back.
|
||||||
|
assert_eq!(line.layer()?, "MYLAYER");
|
||||||
|
|
||||||
|
// Move the endpoint in place (same ObjectId).
|
||||||
|
line.transform(ocs_doc_api::PlacementSpec::at([5.0, 0.0, 0.0]))?;
|
||||||
|
|
||||||
|
// Delete it; the document is left unchanged if this failed.
|
||||||
|
line.delete()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin example: create a box and read its volume
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ocs_doc_api::{doc_api_for_host, ApiResult};
|
||||||
|
use ocs_plugin_api::host::HostApi;
|
||||||
|
|
||||||
|
fn create_box_and_measure(host: &mut dyn HostApi) -> ApiResult<f64> {
|
||||||
|
let api = doc_api_for_host(host).ok_or_else(|| {
|
||||||
|
ocs_doc_api::ApiError::Transport("host does not expose a DocApi channel".into())
|
||||||
|
})?;
|
||||||
|
let doc = api.document(api.active_tab());
|
||||||
|
|
||||||
|
let box_ = doc.solids().create_cuboid([0.0; 3], [10.0; 3])?;
|
||||||
|
let volume = box_.volume()?;
|
||||||
|
|
||||||
|
// Boolean example: intersect with a second box.
|
||||||
|
let other = doc.solids().create_cuboid([5.0; 3], [10.0; 3])?;
|
||||||
|
let intersection = box_.intersect(&other)?;
|
||||||
|
let _intersection_volume = intersection.volume()?;
|
||||||
|
|
||||||
|
Ok(volume)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
See the [API reference](src/gen/api_reference.md) for constructors and methods,
|
See the [API reference](src/gen/api_reference.md) for constructors and methods,
|
||||||
[architecture](ARCHITECTURE.md) for backend rules, and
|
[architecture](ARCHITECTURE.md) for backend rules, and
|
||||||
|
|||||||
@@ -21,8 +21,39 @@ The bridge converts these objects to the Rust DTOs and bincode 1.3 wire format,
|
|||||||
sends `DocApiRequest { tab_id, bytes }`, and decodes `Result<Receipt, ApiError>`.
|
sends `DocApiRequest { tab_id, bytes }`, and decodes `Result<Receipt, ApiError>`.
|
||||||
Host plugin API version 6 and document envelope version 1 are required.
|
Host plugin API version 6 and document envelope version 1 are required.
|
||||||
|
|
||||||
|
For Rust plugins compiled against `ocs_doc_api` with the `doc_api_host` feature,
|
||||||
|
use the `doc_api_for_host` helper to build a typed `DocApi` from any
|
||||||
|
`ocs_plugin_api::host::HostApi` that exposes a `PluginRequestSender`
|
||||||
|
(out-of-process / worker-thread hosts):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ocs_doc_api::doc_api_for_host;
|
||||||
|
use ocs_plugin_api::host::HostApi;
|
||||||
|
|
||||||
|
fn on_dispatch(host: &mut dyn HostApi) -> Option<()> {
|
||||||
|
let doc_api = doc_api_for_host(host)?;
|
||||||
|
let doc = doc_api.document(host.tab_id());
|
||||||
|
let cube = doc.solids().create_cuboid([0.0; 3], [10.0; 3]).ok()?;
|
||||||
|
// ...
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In-process plugins should use `DocApi::in_process(backend, tab_id)` with a
|
||||||
|
concrete `DocApiBackend`; `doc_api_for_host` returns `None` for hosts that do not
|
||||||
|
expose a `PluginRequestSender`.
|
||||||
|
|
||||||
Decode receipt outcomes as `{"NewId": 42}` or `{"NewIds": [42, 43]}` and query
|
Decode receipt outcomes as `{"NewId": 42}` or `{"NewIds": [42, 43]}` and query
|
||||||
results as externally tagged dictionaries such as `{"Volume": 12.5}`. The facade
|
results as externally tagged dictionaries such as `{"Volume": 12.5}`. The facade
|
||||||
unwraps these into typed handles or values. The bridge must raise an exception
|
unwraps these into typed handles or values. Layer results use `{"Layers": [...]}`
|
||||||
for an error response. Transport binding determines the tab; requesting another
|
and `{"EntityLayer": "WALLS"}`. Layer properties (`LayerInfo`, `LayerFlags`,
|
||||||
tab or combining typed handles from different documents raises an error.
|
`Color`, `LineWeight`) appear as plain dictionaries in operation payloads, e.g.
|
||||||
|
`{"CreateLayer": {"name": "Walls", "flags": {"frozen": false, "locked": false,
|
||||||
|
"frozen_in_new_viewport": false, "off": false}, "color": {"Index": 7},
|
||||||
|
"line_type": "Continuous", "line_weight": {"Default": null},
|
||||||
|
"plot_style": "", "is_plottable": true}}`.
|
||||||
|
XDATA and XRECORD payloads appear as nested externally tagged dictionaries
|
||||||
|
(e.g. `{"XData": {"application_name": ..., "values": [...]}}` or
|
||||||
|
`{"XRecord": {"name": ..., "entries": [...]}}`). The bridge must raise an
|
||||||
|
exception for an error response. Transport binding determines the tab; requesting
|
||||||
|
another tab or combining typed handles from different documents raises an error.
|
||||||
@@ -36,6 +36,12 @@ class Entity:
|
|||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
self._doc._apply_op({"Delete": {"id": self.id}})
|
self._doc._apply_op({"Delete": {"id": self.id}})
|
||||||
|
|
||||||
|
def layer(self):
|
||||||
|
return self._doc._apply_query({"GetEntityLayer": {"id": self.id}})
|
||||||
|
|
||||||
|
def set_layer(self, layer):
|
||||||
|
self._doc._apply_op({"SetEntityLayer": {"id": self.id, "layer": layer}})
|
||||||
'''
|
'''
|
||||||
|
|
||||||
DOCUMENT = '''
|
DOCUMENT = '''
|
||||||
@@ -79,6 +85,18 @@ class Document:
|
|||||||
def curves(self): return Curves(self)
|
def curves(self): return Curves(self)
|
||||||
def entities(self): return Entities(self)
|
def entities(self): return Entities(self)
|
||||||
|
|
||||||
|
def layers(self):
|
||||||
|
return self._apply_query({"ListLayers": {}})
|
||||||
|
|
||||||
|
def create_layer(self, info):
|
||||||
|
self._apply_op({"CreateLayer": info})
|
||||||
|
|
||||||
|
def update_layer(self, name, info):
|
||||||
|
self._apply_op({"UpdateLayer": {"name": name, "info": info}})
|
||||||
|
|
||||||
|
def delete_layer(self, name):
|
||||||
|
self._apply_op({"DeleteLayer": {"name": name}})
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DocApi:
|
class DocApi:
|
||||||
_transport: Any
|
_transport: Any
|
||||||
|
|||||||
@@ -168,9 +168,32 @@ Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `ArcCurve`,
|
|||||||
- **`transform(placement)`** — in-place rigid similarity (same `ObjectId`). **v1:
|
- **`transform(placement)`** — in-place rigid similarity (same `ObjectId`). **v1:
|
||||||
solids only.**
|
solids only.**
|
||||||
- **`delete()`** — remove the entity (one undo step).
|
- **`delete()`** — remove the entity (one undo step).
|
||||||
|
- **`layer()` -> `String`** — the name of the layer the entity is on.
|
||||||
|
- **`set_layer(layer)`** — move the entity to an existing layer (one undo step;
|
||||||
|
blocked if the entity or target layer is locked).
|
||||||
|
|
||||||
`Entity` additionally has `view()` (id + kind + bounds) and `as_solid()` (typed
|
`Entity` additionally has `view()` (id + kind + bounds), `as_solid()` (typed
|
||||||
downcast when `kind == "Solid3D"`).
|
downcast when `kind == "Solid3D"`), and XDATA access through
|
||||||
|
`xdata(application_name)` / `set_xdata(application_name, record)`.
|
||||||
|
|
||||||
|
## Layer table
|
||||||
|
|
||||||
|
Layers are a named document table, not an entity family. `Document::layers()`
|
||||||
|
lists all layers as `Vec<LayerInfo>`. Table-level ops are `CreateLayer(info)`,
|
||||||
|
`UpdateLayer { name, info }` and `DeleteLayer { name }`. `Entity::layer()`
|
||||||
|
returns the layer name and `Entity::set_layer(layer)` moves the entity to an
|
||||||
|
existing layer. The host rejects duplicate names, deletion of layer "0" or the
|
||||||
|
current layer, and removing a layer that still has entities assigned.
|
||||||
|
|
||||||
|
## Transports
|
||||||
|
|
||||||
|
- **`InProcess`** (feature `host`) — drives a `DocApiBackend` directly in the host.
|
||||||
|
- **`OcsPluginApiIpc`** (feature `ipc`) — serializes envelopes over the
|
||||||
|
`ocs_plugin_api` `PluginRequestSender`/`PluginRequest` channel. The host routes
|
||||||
|
`DocApiRequest { tab_id, bytes }` to the same executor.
|
||||||
|
- **`doc_api_for_host(host)`** (feature `doc_api_host`) — convenience helper that
|
||||||
|
builds a `DocApi` from any `ocs_plugin_api::host::HostApi` exposing a
|
||||||
|
`PluginRequestSender` (out-of-process / worker-thread plugins).
|
||||||
|
|
||||||
## Collections & cross-cutting
|
## Collections & cross-cutting
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use ocs_doc_api::backend::{DocApiBackend, KernelBody};
|
use ocs_doc_api::backend::{DocApiBackend, KernelBody};
|
||||||
use ocs_doc_api::{
|
use ocs_doc_api::{
|
||||||
Aabb, ApiError, ApiResult, Curve2Spec, EntityView, GeometryErrorKind, GeometryRevision,
|
normalize_layer_name, Aabb, ApiError, ApiResult, Curve2Spec, EntityView, GeometryErrorKind,
|
||||||
ObjectId, PlacementSpec,
|
GeometryRevision, LayerInfo, ObjectId, PlacementSpec,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct MockBackend {
|
pub struct MockBackend {
|
||||||
next_id: u64,
|
next_id: u64,
|
||||||
revision: u64,
|
revision: u64,
|
||||||
@@ -31,6 +32,38 @@ pub struct MockBackend {
|
|||||||
attributes_store: HashMap<ObjectId, Vec<(String, String)>>,
|
attributes_store: HashMap<ObjectId, Vec<(String, String)>>,
|
||||||
/// Stored viewport views (target, height).
|
/// Stored viewport views (target, height).
|
||||||
viewport_views: HashMap<ObjectId, ([f64; 3], f64)>,
|
viewport_views: HashMap<ObjectId, ([f64; 3], f64)>,
|
||||||
|
/// Stored XDATA records keyed by (entity id, application name).
|
||||||
|
xdata_store: HashMap<(ObjectId, String), ocs_doc_api::XDataRecord>,
|
||||||
|
/// Stored XRECORD payloads keyed by entity id.
|
||||||
|
xrecord_store: HashMap<ObjectId, ocs_doc_api::XRecordSpec>,
|
||||||
|
/// Stored layer table.
|
||||||
|
layers: HashMap<String, LayerInfo>,
|
||||||
|
/// Layer name assigned to each entity.
|
||||||
|
entity_layers: HashMap<ObjectId, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MockBackend {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut backend = Self {
|
||||||
|
next_id: 0,
|
||||||
|
revision: 0,
|
||||||
|
undo_steps: 0,
|
||||||
|
bodies: HashMap::new(),
|
||||||
|
kinds: HashMap::new(),
|
||||||
|
curves: HashMap::new(),
|
||||||
|
text_values: HashMap::new(),
|
||||||
|
hatch_boundaries: HashMap::new(),
|
||||||
|
dimension_measurements: HashMap::new(),
|
||||||
|
attributes_store: HashMap::new(),
|
||||||
|
viewport_views: HashMap::new(),
|
||||||
|
xdata_store: HashMap::new(),
|
||||||
|
xrecord_store: HashMap::new(),
|
||||||
|
layers: HashMap::new(),
|
||||||
|
entity_layers: HashMap::new(),
|
||||||
|
};
|
||||||
|
backend.layers.insert("0".to_string(), LayerInfo::new("0"));
|
||||||
|
backend
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockBackend {
|
impl MockBackend {
|
||||||
@@ -52,7 +85,7 @@ impl MockBackend {
|
|||||||
/// Coarse bounds for a stored 2D curve spec (mirrors the host's entity_bounds).
|
/// Coarse bounds for a stored 2D curve spec (mirrors the host's entity_bounds).
|
||||||
fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
||||||
match spec {
|
match spec {
|
||||||
Curve2Spec::Line { start, end } => Aabb {
|
Curve2Spec::Line { start, end, .. } => Aabb {
|
||||||
min: [
|
min: [
|
||||||
start[0].min(end[0]),
|
start[0].min(end[0]),
|
||||||
start[1].min(end[1]),
|
start[1].min(end[1]),
|
||||||
@@ -64,11 +97,11 @@ fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
|||||||
start[2].max(end[2]),
|
start[2].max(end[2]),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
Curve2Spec::Circle { centre, radius } => Aabb {
|
Curve2Spec::Circle { centre, radius, .. } => Aabb {
|
||||||
min: [centre[0] - radius, centre[1] - radius, centre[2]],
|
min: [centre[0] - radius, centre[1] - radius, centre[2]],
|
||||||
max: [centre[0] + radius, centre[1] + radius, centre[2]],
|
max: [centre[0] + radius, centre[1] + radius, centre[2]],
|
||||||
},
|
},
|
||||||
Curve2Spec::Point { position } => Aabb {
|
Curve2Spec::Point { position, .. } => Aabb {
|
||||||
min: *position,
|
min: *position,
|
||||||
max: *position,
|
max: *position,
|
||||||
},
|
},
|
||||||
@@ -133,6 +166,15 @@ impl DocApiBackend for MockBackend {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn create_many(&mut self, specs: &[ocs_doc_api::EntitySpec]) -> ApiResult<Vec<ObjectId>> {
|
fn create_many(&mut self, specs: &[ocs_doc_api::EntitySpec]) -> ApiResult<Vec<ObjectId>> {
|
||||||
|
// Validate per-spec layers up front so the op is atomic.
|
||||||
|
for spec in specs {
|
||||||
|
if let Some(name) = spec.layer() {
|
||||||
|
self.lookup_layer(name).map_err(|e| ApiError::validation(
|
||||||
|
"CreateMany",
|
||||||
|
e.to_string(),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut next = self.clone();
|
let mut next = self.clone();
|
||||||
let ids = specs
|
let ids = specs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -173,10 +215,29 @@ impl DocApiBackend for MockBackend {
|
|||||||
Curve2Spec::Ray { .. } => "Ray",
|
Curve2Spec::Ray { .. } => "Ray",
|
||||||
Curve2Spec::XLine { .. } => "XLine",
|
Curve2Spec::XLine { .. } => "XLine",
|
||||||
};
|
};
|
||||||
|
let layer = spec
|
||||||
|
.layer()
|
||||||
|
.map(|name| {
|
||||||
|
self.lookup_layer(name).map(|info| info.normalized_name())
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
let id = self.alloc(kind);
|
let id = self.alloc(kind);
|
||||||
self.curves.insert(id, spec.clone());
|
self.curves.insert(id, spec.clone());
|
||||||
|
self.entity_layers
|
||||||
|
.insert(id, layer.unwrap_or_else(|| "0".to_string()));
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lookup_layer(&self, name: &str) -> ApiResult<LayerInfo> {
|
||||||
|
let norm = normalize_layer_name(name);
|
||||||
|
if norm.is_empty() {
|
||||||
|
return Err(ApiError::validation("LookupLayer", "empty layer name"));
|
||||||
|
}
|
||||||
|
self.layers
|
||||||
|
.get(&norm)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| ApiError::validation("LookupLayer", format!("layer '{name}' does not exist")))
|
||||||
|
}
|
||||||
fn add_insert(&mut self, spec: &ocs_doc_api::ops::InsertSpec) -> ApiResult<ObjectId> {
|
fn add_insert(&mut self, spec: &ocs_doc_api::ops::InsertSpec) -> ApiResult<ObjectId> {
|
||||||
if spec.block_name.is_empty() {
|
if spec.block_name.is_empty() {
|
||||||
return Err(ApiError::validation("CreateInsert", "empty block name"));
|
return Err(ApiError::validation("CreateInsert", "empty block name"));
|
||||||
@@ -355,6 +416,144 @@ impl DocApiBackend for MockBackend {
|
|||||||
.copied()
|
.copied()
|
||||||
.ok_or(ApiError::UnknownId(id))
|
.ok_or(ApiError::UnknownId(id))
|
||||||
}
|
}
|
||||||
|
fn set_xdata(
|
||||||
|
&mut self,
|
||||||
|
id: ObjectId,
|
||||||
|
application_name: &str,
|
||||||
|
record: Option<&ocs_doc_api::XDataRecord>,
|
||||||
|
) -> ApiResult<()> {
|
||||||
|
if !self.entity_exists(id) {
|
||||||
|
return Err(ApiError::UnknownId(id));
|
||||||
|
}
|
||||||
|
let key = (id, application_name.to_string());
|
||||||
|
match record {
|
||||||
|
Some(r) => {
|
||||||
|
self.xdata_store.insert(key, r.clone());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.xdata_store.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn xdata(
|
||||||
|
&self,
|
||||||
|
id: ObjectId,
|
||||||
|
application_name: &str,
|
||||||
|
) -> ApiResult<Option<ocs_doc_api::XDataRecord>> {
|
||||||
|
if !self.entity_exists(id) {
|
||||||
|
return Err(ApiError::UnknownId(id));
|
||||||
|
}
|
||||||
|
Ok(self
|
||||||
|
.xdata_store
|
||||||
|
.get(&(id, application_name.to_string()))
|
||||||
|
.cloned())
|
||||||
|
}
|
||||||
|
fn add_xrecord(&mut self, spec: &ocs_doc_api::XRecordSpec) -> ApiResult<ObjectId> {
|
||||||
|
let id = self.alloc("XRecord");
|
||||||
|
self.xrecord_store.insert(id, spec.clone());
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
fn set_xrecord(&mut self, id: ObjectId, spec: &ocs_doc_api::XRecordSpec) -> ApiResult<()> {
|
||||||
|
if !self.object_exists(id) {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"SetXRecord",
|
||||||
|
format!("unknown ObjectId {id:?}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.xrecord_store.insert(id, spec.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn xrecord(&self, id: ObjectId) -> ApiResult<Option<ocs_doc_api::XRecordSpec>> {
|
||||||
|
if !self.object_exists(id) {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"GetXRecord",
|
||||||
|
format!("unknown ObjectId {id:?}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(self.xrecord_store.get(&id).cloned())
|
||||||
|
}
|
||||||
|
fn create_layer(&mut self, info: &LayerInfo) -> ApiResult<()> {
|
||||||
|
let name = normalize_layer_name(&info.name);
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(ApiError::validation("CreateLayer", "empty layer name"));
|
||||||
|
}
|
||||||
|
if self.layers.contains_key(&name) {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"CreateLayer",
|
||||||
|
format!("layer '{name}' already exists"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut stored = info.clone();
|
||||||
|
stored.name = name.clone();
|
||||||
|
self.layers.insert(name, stored);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn update_layer(&mut self, name: &str, info: &LayerInfo) -> ApiResult<()> {
|
||||||
|
let name_upper = normalize_layer_name(name);
|
||||||
|
if !self.layers.contains_key(&name_upper) {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"UpdateLayer",
|
||||||
|
format!("layer '{name}' does not exist"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut stored = info.clone();
|
||||||
|
stored.name = name_upper.clone();
|
||||||
|
self.layers.insert(name_upper, stored);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn delete_layer(&mut self, name: &str) -> ApiResult<()> {
|
||||||
|
let name_upper = normalize_layer_name(name);
|
||||||
|
if name_upper == "0" {
|
||||||
|
return Err(ApiError::validation("DeleteLayer", "cannot delete layer 0"));
|
||||||
|
}
|
||||||
|
if self.entity_layers.values().any(|l| normalize_layer_name(l) == name_upper) {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"DeleteLayer",
|
||||||
|
format!("layer '{name_upper}' still in use"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.layers.remove(&name_upper).is_none() {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"DeleteLayer",
|
||||||
|
format!("layer '{name}' does not exist"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn set_entity_layer(&mut self, id: ObjectId, layer: &str) -> ApiResult<()> {
|
||||||
|
if !self.entity_exists(id) {
|
||||||
|
return Err(ApiError::UnknownId(id));
|
||||||
|
}
|
||||||
|
let layer_upper = normalize_layer_name(layer);
|
||||||
|
let target = self
|
||||||
|
.layers
|
||||||
|
.get(&layer_upper)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::validation(
|
||||||
|
"SetEntityLayer",
|
||||||
|
format!("layer '{layer_upper}' does not exist"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if target.flags.locked {
|
||||||
|
return Err(ApiError::Unsupported(format!(
|
||||||
|
"entity {id:?} cannot be moved to locked layer '{layer_upper}'"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
self.entity_layers.insert(id, layer_upper);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn layers(&self) -> ApiResult<Vec<LayerInfo>> {
|
||||||
|
let mut v: Vec<_> = self.layers.values().cloned().collect();
|
||||||
|
v.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
fn entity_layer(&self, id: ObjectId) -> ApiResult<String> {
|
||||||
|
self.entity_layers
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or(ApiError::UnknownId(id))
|
||||||
|
}
|
||||||
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||||
if self.entity_exists(id) {
|
if self.entity_exists(id) {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -364,17 +563,85 @@ impl DocApiBackend for MockBackend {
|
|||||||
}
|
}
|
||||||
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool> {
|
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool> {
|
||||||
self.bodies.remove(&id);
|
self.bodies.remove(&id);
|
||||||
|
self.xdata_store.retain(|(k, _), _| *k != id);
|
||||||
|
self.xrecord_store.remove(&id);
|
||||||
|
self.entity_layers.remove(&id);
|
||||||
Ok(self.kinds.remove(&id).is_some())
|
Ok(self.kinds.remove(&id).is_some())
|
||||||
}
|
}
|
||||||
|
fn enumerate_entities(
|
||||||
|
&self,
|
||||||
|
kind: Option<&str>,
|
||||||
|
layer: Option<&str>,
|
||||||
|
include_bounds: bool,
|
||||||
|
) -> ApiResult<Vec<EntityView>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for (id, kind_name) in &self.kinds {
|
||||||
|
if let Some(k) = kind {
|
||||||
|
if !k.eq_ignore_ascii_case(kind_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let entity_layer = self
|
||||||
|
.entity_layers
|
||||||
|
.get(id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "0".to_string());
|
||||||
|
if let Some(l) = layer {
|
||||||
|
if !l.eq_ignore_ascii_case(&entity_layer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bounds = if include_bounds {
|
||||||
|
self.curves.get(id).map(curve_bounds)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
out.push(EntityView {
|
||||||
|
id: *id,
|
||||||
|
kind: kind_name.clone(),
|
||||||
|
layer: entity_layer,
|
||||||
|
bounds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
fn point_position(&self, id: ObjectId) -> ApiResult<[f64; 3]> {
|
||||||
|
let spec = self.curves.get(&id).ok_or(ApiError::UnknownId(id))?;
|
||||||
|
match spec {
|
||||||
|
Curve2Spec::Point { position, .. } => Ok(*position),
|
||||||
|
_ => Err(ApiError::Unsupported(
|
||||||
|
"GetPointPosition is only for Point entities".into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn line_geometry(&self, id: ObjectId) -> ApiResult<([f64; 3], [f64; 3])> {
|
||||||
|
let spec = self.curves.get(&id).ok_or(ApiError::UnknownId(id))?;
|
||||||
|
match spec {
|
||||||
|
Curve2Spec::Line { start, end, .. } => Ok((*start, *end)),
|
||||||
|
_ => Err(ApiError::Unsupported(
|
||||||
|
"GetLineGeometry is only for Line entities".into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView> {
|
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView> {
|
||||||
let kind = self.kinds.get(&id).ok_or(ApiError::UnknownId(id))?.clone();
|
let kind = self.kinds.get(&id).ok_or(ApiError::UnknownId(id))?.clone();
|
||||||
|
let layer = self
|
||||||
|
.entity_layers
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "0".to_string());
|
||||||
let bounds = self.bodies.get(&id).and_then(|b| {
|
let bounds = self.bodies.get(&id).and_then(|b| {
|
||||||
cadkernel::brep::body_bounds(b).map(|bb| Aabb {
|
cadkernel::brep::body_bounds(b).map(|bb| Aabb {
|
||||||
min: bb.min,
|
min: bb.min,
|
||||||
max: bb.max,
|
max: bb.max,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Ok(EntityView { id, kind, bounds })
|
Ok(EntityView {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
layer,
|
||||||
|
bounds,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
fn transform_entity(&mut self, id: ObjectId, placement: &PlacementSpec) -> ApiResult<()> {
|
fn transform_entity(&mut self, id: ObjectId, placement: &PlacementSpec) -> ApiResult<()> {
|
||||||
if !self.entity_exists(id) {
|
if !self.entity_exists(id) {
|
||||||
@@ -423,6 +690,9 @@ impl DocApiBackend for MockBackend {
|
|||||||
fn entity_exists(&self, id: ObjectId) -> bool {
|
fn entity_exists(&self, id: ObjectId) -> bool {
|
||||||
self.kinds.contains_key(&id)
|
self.kinds.contains_key(&id)
|
||||||
}
|
}
|
||||||
|
fn object_exists(&self, id: ObjectId) -> bool {
|
||||||
|
self.xrecord_store.contains_key(&id)
|
||||||
|
}
|
||||||
fn revision(&self) -> GeometryRevision {
|
fn revision(&self) -> GeometryRevision {
|
||||||
GeometryRevision(self.revision)
|
GeometryRevision(self.revision)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,10 +54,7 @@ fn main() -> ocs_doc_api::ApiResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A totally unknown id also surfaces a structured error.
|
// A totally unknown id also surfaces a structured error.
|
||||||
assert!(matches!(
|
assert!(doc.entities().get(ObjectId::from_u64(0xDEAD)).is_err());
|
||||||
doc.entities().get(ObjectId::from_u64(0xDEAD)),
|
|
||||||
Err(_)
|
|
||||||
));
|
|
||||||
println!("failure semantics verified");
|
println!("failure semantics verified");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -457,6 +457,35 @@ name = "create_table"
|
|||||||
args = [{ name = "insertion_point", ty = "[f64; 3]" }, { name = "data", ty = "&[Vec<String>]" }]
|
args = [{ name = "insertion_point", ty = "[f64; 3]" }, { name = "data", ty = "&[Vec<String>]" }]
|
||||||
op = "CreateTable"
|
op = "CreateTable"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# XDATA / XRECORD FAMILY — application-specific extended data roundtrip
|
||||||
|
# ============================================================================
|
||||||
|
[[family]]
|
||||||
|
name = "XRecord"
|
||||||
|
acadrust_variant = "XRecord"
|
||||||
|
handle = "XRecord"
|
||||||
|
collection = "entities"
|
||||||
|
description = "Extended record (XRecord): arbitrary DXF group-code/value pairs. Roundtripped verbatim as an XRecordSpec payload."
|
||||||
|
|
||||||
|
[[family.constructor]]
|
||||||
|
name = "create_xrecord"
|
||||||
|
args = [{ name = "spec", ty = "XRecordSpec" }]
|
||||||
|
op = "CreateXRecord"
|
||||||
|
returns = "XRecord"
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "payload"
|
||||||
|
kind = "query"
|
||||||
|
query = "GetXRecord"
|
||||||
|
returns = "XRecordSpec"
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "set_payload"
|
||||||
|
kind = "op"
|
||||||
|
op = "SetXRecord"
|
||||||
|
args = [{ name = "spec", ty = "XRecordSpec" }]
|
||||||
|
returns = "()"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Cross-cutting ops (generic handles + bulk) — documented in build.rs
|
# Cross-cutting ops (generic handles + bulk) — documented in build.rs
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -469,6 +498,42 @@ op = "CreateTable"
|
|||||||
# - Read-only batching: query_batch(|q| ...) — one round-trip, no revision bump.
|
# - Read-only batching: query_batch(|q| ...) — one round-trip, no revision bump.
|
||||||
# - OpGroup: best-effort client-side failure cleanup (NOT a transaction).
|
# - OpGroup: best-effort client-side failure cleanup (NOT a transaction).
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LAYER family (named drawing layers)
|
||||||
|
# ============================================================================
|
||||||
|
[[family]]
|
||||||
|
name = "Layer"
|
||||||
|
handle = "Layer"
|
||||||
|
collection = "layers"
|
||||||
|
description = "Named drawing layer (color, linetype, lineweight, visibility). Not an entity handle; accessed through Document::layers()."
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "list"
|
||||||
|
kind = "query"
|
||||||
|
query = "ListLayers"
|
||||||
|
returns = "Vec<LayerInfo>"
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "create"
|
||||||
|
kind = "op"
|
||||||
|
op = "CreateLayer"
|
||||||
|
args = [{ name = "info", ty = "LayerInfo" }]
|
||||||
|
returns = "()"
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "update"
|
||||||
|
kind = "op"
|
||||||
|
op = "UpdateLayer"
|
||||||
|
args = [{ name = "name", ty = "String" }, { name = "info", ty = "LayerInfo" }]
|
||||||
|
returns = "()"
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "delete"
|
||||||
|
kind = "op"
|
||||||
|
op = "DeleteLayer"
|
||||||
|
args = [{ name = "name", ty = "String" }]
|
||||||
|
returns = "()"
|
||||||
|
|
||||||
# ---- Kernel/op mapping (reference for the executor + docs) ----
|
# ---- Kernel/op mapping (reference for the executor + docs) ----
|
||||||
[[kernel_map]]
|
[[kernel_map]]
|
||||||
op = "CreateSolid.Cuboid"
|
op = "CreateSolid.Cuboid"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use crate::error::{ApiError, ApiResult};
|
use crate::error::{ApiError, ApiResult};
|
||||||
use crate::id::ObjectId;
|
use crate::id::ObjectId;
|
||||||
use crate::ops::Curve2Spec;
|
use crate::ops::{Curve2Spec, LayerInfo};
|
||||||
use crate::query::{Aabb, EntityView};
|
use crate::query::{Aabb, EntityView};
|
||||||
use crate::revision::GeometryRevision;
|
use crate::revision::GeometryRevision;
|
||||||
|
|
||||||
@@ -152,6 +152,85 @@ pub trait DocApiBackend {
|
|||||||
sections: &[(cadkernel::space::Plane, Vec<cadkernel::geom2d::Curve>)],
|
sections: &[(cadkernel::space::Plane, Vec<cadkernel::geom2d::Curve>)],
|
||||||
) -> ApiResult<ObjectId>;
|
) -> ApiResult<ObjectId>;
|
||||||
|
|
||||||
|
/// Attach or replace an XDATA record for `application_name` on `id`.
|
||||||
|
/// `None` removes any existing record for that application.
|
||||||
|
fn set_xdata(
|
||||||
|
&mut self,
|
||||||
|
id: ObjectId,
|
||||||
|
application_name: &str,
|
||||||
|
record: Option<&crate::ops::XDataRecord>,
|
||||||
|
) -> ApiResult<()>;
|
||||||
|
|
||||||
|
/// Read the XDATA record for `application_name` on `id` (`None` if absent).
|
||||||
|
fn xdata(
|
||||||
|
&self,
|
||||||
|
id: ObjectId,
|
||||||
|
application_name: &str,
|
||||||
|
) -> ApiResult<Option<crate::ops::XDataRecord>>;
|
||||||
|
|
||||||
|
/// Create a standalone `XRECORD` object from a spec; returns the fresh `ObjectId`.
|
||||||
|
fn add_xrecord(&mut self, spec: &crate::ops::XRecordSpec) -> ApiResult<ObjectId>;
|
||||||
|
|
||||||
|
/// Replace an existing `XRECORD` object's payload in place.
|
||||||
|
fn set_xrecord(&mut self, id: ObjectId, spec: &crate::ops::XRecordSpec) -> ApiResult<()>;
|
||||||
|
|
||||||
|
/// Read an existing `XRECORD` object's payload (`None` if `id` is not an XRecord).
|
||||||
|
fn xrecord(&self, id: ObjectId) -> ApiResult<Option<crate::ops::XRecordSpec>>;
|
||||||
|
|
||||||
|
// ── layer CRUD ─────────────────────────────────────────────────────────
|
||||||
|
/// Create a new layer. Fails with `Validation` if the name already exists or is reserved.
|
||||||
|
fn create_layer(&mut self, info: &crate::ops::LayerInfo) -> ApiResult<()>;
|
||||||
|
|
||||||
|
/// Update an existing layer's properties by name. Fails with `Validation` if absent.
|
||||||
|
fn update_layer(&mut self, name: &str, info: &crate::ops::LayerInfo) -> ApiResult<()>;
|
||||||
|
|
||||||
|
/// Delete a layer by name. Fails with `Validation` if the layer is "0",
|
||||||
|
/// the current layer, does not exist, or has entities.
|
||||||
|
fn delete_layer(&mut self, name: &str) -> ApiResult<()>;
|
||||||
|
|
||||||
|
/// Look up a layer by raw name, normalizing it. Default implementation scans
|
||||||
|
/// `layers()`; backends with O(1) lookup should override.
|
||||||
|
fn lookup_layer(&self, name: &str) -> ApiResult<LayerInfo> {
|
||||||
|
let norm = crate::layer::normalize_layer_name(name);
|
||||||
|
if norm.is_empty() {
|
||||||
|
return Err(ApiError::validation("LookupLayer", "empty layer name"));
|
||||||
|
}
|
||||||
|
self.layers()?
|
||||||
|
.into_iter()
|
||||||
|
.find(|l| l.normalized_name() == norm)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::validation(
|
||||||
|
"LookupLayer",
|
||||||
|
format!("layer '{name}' does not exist"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move an entity to a different layer. The target layer must exist.
|
||||||
|
fn set_entity_layer(&mut self, id: ObjectId, layer: &str) -> ApiResult<()>;
|
||||||
|
|
||||||
|
/// List all layers in the document.
|
||||||
|
fn layers(&self) -> ApiResult<Vec<crate::ops::LayerInfo>>;
|
||||||
|
|
||||||
|
/// The layer name of an entity.
|
||||||
|
fn entity_layer(&self, id: ObjectId) -> ApiResult<String>;
|
||||||
|
|
||||||
|
/// Enumerate first-class entities, optionally filtered by kind and/or layer.
|
||||||
|
/// `include_bounds` controls whether `EntityView::bounds` is populated; skip it
|
||||||
|
/// for lightweight list views.
|
||||||
|
fn enumerate_entities(
|
||||||
|
&self,
|
||||||
|
kind: Option<&str>,
|
||||||
|
layer: Option<&str>,
|
||||||
|
include_bounds: bool,
|
||||||
|
) -> ApiResult<Vec<EntityView>>;
|
||||||
|
|
||||||
|
/// The WCS location of a Point entity.
|
||||||
|
fn point_position(&self, id: ObjectId) -> ApiResult<[f64; 3]>;
|
||||||
|
|
||||||
|
/// The start and end WCS points of a Line entity.
|
||||||
|
fn line_geometry(&self, id: ObjectId) -> ApiResult<([f64; 3], [f64; 3])>;
|
||||||
|
|
||||||
/// Can `id` be modified in place right now (exists, is the expected family,
|
/// Can `id` be modified in place right now (exists, is the expected family,
|
||||||
/// not on a locked layer)? Read-only pre-check used before mutations.
|
/// not on a locked layer)? Read-only pre-check used before mutations.
|
||||||
/// Default: existence + not-locked (backends narrow the family check).
|
/// Default: existence + not-locked (backends narrow the family check).
|
||||||
@@ -178,6 +257,10 @@ pub trait DocApiBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether an object (named-object / XRecord / dictionary) exists. Distinct
|
||||||
|
/// from `entity_exists`, which is for first-class entities only.
|
||||||
|
fn object_exists(&self, id: ObjectId) -> bool;
|
||||||
|
|
||||||
/// A generic, untyped view of any entity (id + kind + coarse bounds).
|
/// A generic, untyped view of any entity (id + kind + coarse bounds).
|
||||||
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView>;
|
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView>;
|
||||||
|
|
||||||
|
|||||||
@@ -113,19 +113,19 @@ pub fn bulge_arc_segment(
|
|||||||
pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
||||||
crate::validation::curve(spec)?;
|
crate::validation::curve(spec)?;
|
||||||
let entity = match spec {
|
let entity = match spec {
|
||||||
Curve2Spec::Line { start, end } => {
|
Curve2Spec::Line { start, end, .. } => {
|
||||||
let mut e = Line::new();
|
let mut e = Line::new();
|
||||||
e.start = v3(*start);
|
e.start = v3(*start);
|
||||||
e.end = v3(*end);
|
e.end = v3(*end);
|
||||||
EntityType::Line(e)
|
EntityType::Line(e)
|
||||||
}
|
}
|
||||||
Curve2Spec::Circle { centre, radius } => {
|
Curve2Spec::Circle { centre, radius, .. } => {
|
||||||
let mut e = Circle::new();
|
let mut e = Circle::new();
|
||||||
e.center = v3(*centre);
|
e.center = v3(*centre);
|
||||||
e.radius = *radius;
|
e.radius = *radius;
|
||||||
EntityType::Circle(e)
|
EntityType::Circle(e)
|
||||||
}
|
}
|
||||||
Curve2Spec::Polyline { points, closed } => {
|
Curve2Spec::Polyline { points, closed, .. } => {
|
||||||
let mut e = LwPolyline::new();
|
let mut e = LwPolyline::new();
|
||||||
e.vertices = points
|
e.vertices = points
|
||||||
.iter()
|
.iter()
|
||||||
@@ -147,7 +147,7 @@ pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
|||||||
e.is_closed = *closed;
|
e.is_closed = *closed;
|
||||||
EntityType::LwPolyline(e)
|
EntityType::LwPolyline(e)
|
||||||
}
|
}
|
||||||
Curve2Spec::Point { position } => {
|
Curve2Spec::Point { position, .. } => {
|
||||||
let mut e = Point::new();
|
let mut e = Point::new();
|
||||||
e.location = v3(*position);
|
e.location = v3(*position);
|
||||||
EntityType::Point(e)
|
EntityType::Point(e)
|
||||||
@@ -157,6 +157,7 @@ pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
|||||||
radius,
|
radius,
|
||||||
start_angle,
|
start_angle,
|
||||||
end_angle,
|
end_angle,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
let mut e = Arc::new();
|
let mut e = Arc::new();
|
||||||
e.center = v3(*centre);
|
e.center = v3(*centre);
|
||||||
@@ -171,6 +172,7 @@ pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
|||||||
ratio,
|
ratio,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
// Validate the minor/major ratio so the (major-axis) bounds never
|
// Validate the minor/major ratio so the (major-axis) bounds never
|
||||||
// under-bound: ratio must be in (0, 1].
|
// under-bound: ratio must be in (0, 1].
|
||||||
@@ -193,6 +195,7 @@ pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
|||||||
control_points,
|
control_points,
|
||||||
knots,
|
knots,
|
||||||
weights,
|
weights,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
let mut e = Spline::new();
|
let mut e = Spline::new();
|
||||||
e.degree = *degree;
|
e.degree = *degree;
|
||||||
@@ -201,10 +204,10 @@ pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
|||||||
e.weights = weights.clone();
|
e.weights = weights.clone();
|
||||||
EntityType::Spline(e)
|
EntityType::Spline(e)
|
||||||
}
|
}
|
||||||
Curve2Spec::Ray { origin, direction } => {
|
Curve2Spec::Ray { origin, direction, .. } => {
|
||||||
EntityType::Ray(Ray::new(v3(*origin), v3(*direction)))
|
EntityType::Ray(Ray::new(v3(*origin), v3(*direction)))
|
||||||
}
|
}
|
||||||
Curve2Spec::XLine { origin, direction } => {
|
Curve2Spec::XLine { origin, direction, .. } => {
|
||||||
EntityType::XLine(XLine::new(v3(*origin), v3(*direction)))
|
EntityType::XLine(XLine::new(v3(*origin), v3(*direction)))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,14 +25,35 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
|
|||||||
fn apply_op_inner<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt> {
|
fn apply_op_inner<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt> {
|
||||||
let name = op.op_name();
|
let name = op.op_name();
|
||||||
crate::validation::operation(&op)?;
|
crate::validation::operation(&op)?;
|
||||||
if let Operation::TransformMany { ids, .. } | Operation::DeleteMany(ids) = &op {
|
match &op {
|
||||||
let unique: std::collections::HashSet<_> = ids.iter().collect();
|
Operation::TransformMany { ids, .. } | Operation::DeleteMany(ids) => {
|
||||||
if ids.is_empty() || unique.len() != ids.len() {
|
let unique: std::collections::HashSet<_> = ids.iter().collect();
|
||||||
return Err(ApiError::validation(
|
if ids.is_empty() || unique.len() != ids.len() {
|
||||||
name,
|
return Err(ApiError::validation(
|
||||||
"batch ids must be nonempty and unique",
|
name,
|
||||||
));
|
"batch ids must be nonempty and unique",
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Operation::SetXDataMany(records) => {
|
||||||
|
if records.is_empty() {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
name,
|
||||||
|
"SetXDataMany records must be nonempty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if records.len() > BULK_ITEM_CAP {
|
||||||
|
return Err(over_cap(name, records.len()));
|
||||||
|
}
|
||||||
|
let unique: std::collections::HashSet<_> = records.iter().map(|(id, _, _)| id).collect();
|
||||||
|
if unique.len() != records.len() {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
name,
|
||||||
|
"SetXDataMany records must have unique entity ids",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
if let Operation::Transform { placement, .. } | Operation::TransformMany { placement, .. } = &op
|
if let Operation::Transform { placement, .. } | Operation::TransformMany { placement, .. } = &op
|
||||||
{
|
{
|
||||||
@@ -353,6 +374,73 @@ fn apply_op_inner<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Recei
|
|||||||
b.finalize_op();
|
b.finalize_op();
|
||||||
OpOutcome::NewId(id)
|
OpOutcome::NewId(id)
|
||||||
}
|
}
|
||||||
|
Operation::CreateLayer(info) => {
|
||||||
|
b.push_undo(name);
|
||||||
|
b.create_layer(info)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(ObjectId::NULL)
|
||||||
|
}
|
||||||
|
Operation::UpdateLayer { name, info } => {
|
||||||
|
b.push_undo(name);
|
||||||
|
b.update_layer(name, info)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(ObjectId::NULL)
|
||||||
|
}
|
||||||
|
Operation::DeleteLayer { name } => {
|
||||||
|
b.push_undo(name);
|
||||||
|
b.delete_layer(name)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(ObjectId::NULL)
|
||||||
|
}
|
||||||
|
Operation::SetEntityLayer { id, layer } => {
|
||||||
|
require_exists(b, *id, name)?;
|
||||||
|
b.push_undo(name);
|
||||||
|
b.set_entity_layer(*id, layer)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(*id)
|
||||||
|
}
|
||||||
|
Operation::SetXData {
|
||||||
|
id,
|
||||||
|
application_name,
|
||||||
|
record,
|
||||||
|
} => {
|
||||||
|
require_exists(b, *id, name)?;
|
||||||
|
b.push_undo(name);
|
||||||
|
b.set_xdata(*id, application_name, record.as_ref())?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(*id)
|
||||||
|
}
|
||||||
|
Operation::SetXDataMany(records) => {
|
||||||
|
// A later locked entity must not leave earlier records changed.
|
||||||
|
for (i, (id, _, _)) in records.iter().enumerate() {
|
||||||
|
b.can_modify(*id).map_err(|e| match e {
|
||||||
|
ApiError::UnknownId(_) => stale_index(name, i, *id),
|
||||||
|
ApiError::Unsupported(reason) => {
|
||||||
|
ApiError::validation(name, format!("index {i}: {reason}"))
|
||||||
|
}
|
||||||
|
other => other,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
b.push_undo(name);
|
||||||
|
for (id, application_name, record) in records {
|
||||||
|
b.set_xdata(*id, application_name, record.as_ref())?;
|
||||||
|
}
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(ObjectId::NULL)
|
||||||
|
}
|
||||||
|
Operation::CreateXRecord(spec) => {
|
||||||
|
b.push_undo(name);
|
||||||
|
let id = b.add_xrecord(spec)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::NewId(id)
|
||||||
|
}
|
||||||
|
Operation::SetXRecord { id, spec } => {
|
||||||
|
require_object_exists(b, *id, name)?;
|
||||||
|
b.push_undo(name);
|
||||||
|
b.set_xrecord(*id, spec)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(*id)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Ok(Receipt {
|
Ok(Receipt {
|
||||||
outcome: Some(outcome),
|
outcome: Some(outcome),
|
||||||
@@ -417,6 +505,38 @@ pub fn apply_queries<B: DocApiBackend>(b: &mut B, queries: Vec<Query>) -> ApiRes
|
|||||||
let (target, height) = b.viewport_view(*id).map_err(|e| label(qname, e))?;
|
let (target, height) = b.viewport_view(*id).map_err(|e| label(qname, e))?;
|
||||||
QueryResult::ViewportView { target, height }
|
QueryResult::ViewportView { target, height }
|
||||||
}
|
}
|
||||||
|
Query::GetXData {
|
||||||
|
id,
|
||||||
|
application_name,
|
||||||
|
} => QueryResult::XData(
|
||||||
|
b.xdata(*id, application_name)
|
||||||
|
.map_err(|e| label(qname, e))?
|
||||||
|
.clone(),
|
||||||
|
),
|
||||||
|
Query::GetXRecord { id } => {
|
||||||
|
QueryResult::XRecord(b.xrecord(*id).map_err(|e| label(qname, e))?.ok_or_else(
|
||||||
|
|| ApiError::validation(qname, format!("ObjectId {id:?} is not an XRecord")),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
Query::ListLayers => QueryResult::Layers(b.layers().map_err(|e| label(qname, e))?),
|
||||||
|
Query::GetEntityLayer { id } => {
|
||||||
|
QueryResult::EntityLayer(b.entity_layer(*id).map_err(|e| label(qname, e))?)
|
||||||
|
}
|
||||||
|
Query::EnumerateEntities {
|
||||||
|
kind,
|
||||||
|
layer,
|
||||||
|
include_bounds,
|
||||||
|
} => QueryResult::Entities(
|
||||||
|
b.enumerate_entities(kind.as_deref(), layer.as_deref(), *include_bounds)
|
||||||
|
.map_err(|e| label(qname, e))?,
|
||||||
|
),
|
||||||
|
Query::GetPointPosition { id } => QueryResult::PointPosition(
|
||||||
|
b.point_position(*id).map_err(|e| label(qname, e))?,
|
||||||
|
),
|
||||||
|
Query::GetLineGeometry { id } => {
|
||||||
|
let (start, end) = b.line_geometry(*id).map_err(|e| label(qname, e))?;
|
||||||
|
QueryResult::LineGeometry((start, end))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
results.push(r);
|
results.push(r);
|
||||||
}
|
}
|
||||||
@@ -459,6 +579,15 @@ fn require_exists<B: DocApiBackend>(b: &B, id: ObjectId, op: &'static str) -> Ap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate that a named object exists before performing an operation.
|
||||||
|
fn require_object_exists<B: DocApiBackend>(b: &B, id: ObjectId, op: &'static str) -> ApiResult<()> {
|
||||||
|
if b.object_exists(id) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ApiError::validation(op, format!("unknown ObjectId {id:?}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn over_cap(op: &'static str, n: usize) -> ApiError {
|
fn over_cap(op: &'static str, n: usize) -> ApiError {
|
||||||
ApiError::validation(op, format!("bulk op over cap: {n} > {BULK_ITEM_CAP}"))
|
ApiError::validation(op, format!("bulk op over cap: {n} > {BULK_ITEM_CAP}"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::envelope::{DocApiEnvelope, Receipt};
|
|||||||
use crate::error::{ApiError, ApiResult};
|
use crate::error::{ApiError, ApiResult};
|
||||||
use crate::gen::{Operation, Query};
|
use crate::gen::{Operation, Query};
|
||||||
use crate::id::ObjectId;
|
use crate::id::ObjectId;
|
||||||
use crate::ops::{BoolOp, Curve2Spec, PlacementSpec, SolidPrimitive};
|
use crate::ops::{BoolOp, Curve2Spec, PlacementSpec, SolidPrimitive, XDataRecord, XRecordSpec};
|
||||||
use crate::query::{Aabb, EntityView, QueryResult};
|
use crate::query::{Aabb, EntityView, QueryResult};
|
||||||
use crate::revision::GeometryRevision;
|
use crate::revision::GeometryRevision;
|
||||||
use crate::transport::Transport;
|
use crate::transport::Transport;
|
||||||
@@ -170,6 +170,74 @@ impl Document {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List all layers in the document.
|
||||||
|
pub fn layers(&self) -> ApiResult<Vec<crate::ops::LayerInfo>> {
|
||||||
|
match self.session.one_query(Query::ListLayers)? {
|
||||||
|
QueryResult::Layers(v) => Ok(v),
|
||||||
|
_ => Err(ApiError::Transport("unexpected layers result".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new layer. One undo step; fails if the normalized name already
|
||||||
|
/// exists or is empty.
|
||||||
|
pub fn create_layer(&self, info: &crate::ops::LayerInfo) -> ApiResult<()> {
|
||||||
|
if !crate::layer::is_valid_layer_name(&info.name) {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"CreateLayer",
|
||||||
|
"empty layer name",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.session
|
||||||
|
.apply_op(Operation::CreateLayer(info.clone()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update an existing layer's properties by name. One undo step; `info.name`
|
||||||
|
/// becomes the new display name (case-insensitive key unchanged).
|
||||||
|
pub fn update_layer(&self, name: &str, info: &crate::ops::LayerInfo) -> ApiResult<()> {
|
||||||
|
if !crate::layer::is_valid_layer_name(&info.name) {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"UpdateLayer",
|
||||||
|
"empty layer name",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.session.apply_op(Operation::UpdateLayer {
|
||||||
|
name: name.to_string(),
|
||||||
|
info: info.clone(),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a layer by name. One undo step; fails for layer "0", the current
|
||||||
|
/// layer, or any layer still referenced by entities.
|
||||||
|
pub fn delete_layer(&self, name: &str) -> ApiResult<()> {
|
||||||
|
self.session.apply_op(Operation::DeleteLayer {
|
||||||
|
name: name.to_string(),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set XDATA on many entities in a single atomic operation.
|
||||||
|
///
|
||||||
|
/// Each tuple is `(entity_id, application_name, record)`. Pass `None` for
|
||||||
|
/// `record` to delete the application's data. Fails up-front if any id is
|
||||||
|
/// invalid; the whole batch is one undo step.
|
||||||
|
pub fn set_xdata_many<I, S>(&self, items: I) -> ApiResult<()>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = (ObjectId, S, Option<XDataRecord>)>,
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
let updates: Vec<_> = items
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, app, record)| (id, app.into(), record))
|
||||||
|
.collect();
|
||||||
|
if updates.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.session.apply_op(Operation::SetXDataMany(updates))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Batch of read-only queries in ONE round-trip (safe: no mutation/undo).
|
/// Batch of read-only queries in ONE round-trip (safe: no mutation/undo).
|
||||||
/// The closure records queries on a [`QueryBatch`]; the results are returned
|
/// The closure records queries on a [`QueryBatch`]; the results are returned
|
||||||
/// in the same order as a [`QueryResults`] view the caller destructures.
|
/// in the same order as a [`QueryResults`] view the caller destructures.
|
||||||
@@ -318,6 +386,12 @@ macro_rules! handle {
|
|||||||
self.id
|
self.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! entity_handle {
|
||||||
|
($name:ident) => {
|
||||||
|
handle!($name);
|
||||||
impl $name {
|
impl $name {
|
||||||
pub fn bounds(&self) -> ApiResult<Aabb> {
|
pub fn bounds(&self) -> ApiResult<Aabb> {
|
||||||
match self.session.one_query(Query::GetBounds { id: self.id })? {
|
match self.session.one_query(Query::GetBounds { id: self.id })? {
|
||||||
@@ -336,24 +410,62 @@ macro_rules! handle {
|
|||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
/// Return the entity's current layer name (normalized uppercase).
|
||||||
|
pub fn layer(&self) -> ApiResult<String> {
|
||||||
|
match self
|
||||||
|
.session
|
||||||
|
.one_query(Query::GetEntityLayer { id: self.id })?
|
||||||
|
{
|
||||||
|
QueryResult::EntityLayer(name) => Ok(name),
|
||||||
|
_ => Err(ApiError::Transport("unexpected entity layer result".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Move the entity to a different layer. One undo step; fails if the
|
||||||
|
/// target layer does not exist or is locked.
|
||||||
|
pub fn set_layer(&self, layer: &str) -> ApiResult<()> {
|
||||||
|
self.session.apply_op(Operation::SetEntityLayer {
|
||||||
|
id: self.id,
|
||||||
|
layer: layer.to_string(),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
handle!(Entity);
|
entity_handle!(Entity);
|
||||||
handle!(Solid);
|
entity_handle!(Solid);
|
||||||
handle!(Line);
|
entity_handle!(Line);
|
||||||
handle!(Circle);
|
entity_handle!(Circle);
|
||||||
handle!(Polyline);
|
entity_handle!(Polyline);
|
||||||
handle!(Point);
|
entity_handle!(Point);
|
||||||
handle!(ArcCurve);
|
entity_handle!(ArcCurve);
|
||||||
handle!(Ellipse);
|
entity_handle!(Ellipse);
|
||||||
handle!(Spline);
|
entity_handle!(Spline);
|
||||||
handle!(Ray);
|
entity_handle!(Ray);
|
||||||
handle!(XLine);
|
entity_handle!(XLine);
|
||||||
handle!(Text);
|
entity_handle!(Text);
|
||||||
handle!(MText);
|
entity_handle!(MText);
|
||||||
handle!(Dimension);
|
entity_handle!(Dimension);
|
||||||
|
handle!(XRecord);
|
||||||
|
|
||||||
|
impl XRecord {
|
||||||
|
/// Read the XRECORD payload.
|
||||||
|
pub fn payload(&self) -> ApiResult<XRecordSpec> {
|
||||||
|
match self.session.one_query(Query::GetXRecord { id: self.id })? {
|
||||||
|
QueryResult::XRecord(spec) => Ok(spec),
|
||||||
|
_ => Err(ApiError::Transport("unexpected xrecord result".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Replace the XRECORD payload in place (one undo step).
|
||||||
|
pub fn set_payload(&self, spec: &XRecordSpec) -> ApiResult<()> {
|
||||||
|
self.session.apply_op(Operation::SetXRecord {
|
||||||
|
id: self.id,
|
||||||
|
spec: spec.clone(),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Dimension {
|
impl Dimension {
|
||||||
/// The measured value of this dimension (distance for linear/radius, degrees
|
/// The measured value of this dimension (distance for linear/radius, degrees
|
||||||
@@ -416,6 +528,26 @@ impl Entity {
|
|||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
/// The XDATA record for `application_name` on this entity (`None` if absent).
|
||||||
|
pub fn xdata(&self, application_name: &str) -> ApiResult<Option<XDataRecord>> {
|
||||||
|
match self.session.one_query(Query::GetXData {
|
||||||
|
id: self.id,
|
||||||
|
application_name: application_name.to_string(),
|
||||||
|
})? {
|
||||||
|
QueryResult::XData(v) => Ok(v),
|
||||||
|
_ => Err(ApiError::Transport("unexpected xdata result".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Attach or replace an XDATA record for `application_name` on this entity.
|
||||||
|
/// Pass `None` to remove the record (one undo step).
|
||||||
|
pub fn set_xdata(&self, application_name: &str, record: Option<XDataRecord>) -> ApiResult<()> {
|
||||||
|
self.session.apply_op(Operation::SetXData {
|
||||||
|
id: self.id,
|
||||||
|
application_name: application_name.to_string(),
|
||||||
|
record,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
pub fn as_solid(&self) -> Option<Solid> {
|
pub fn as_solid(&self) -> Option<Solid> {
|
||||||
// Typed downcast is validated by the view's kind.
|
// Typed downcast is validated by the view's kind.
|
||||||
matches!(self.view().ok()?.kind.as_str(), "Solid3D")
|
matches!(self.view().ok()?.kind.as_str(), "Solid3D")
|
||||||
@@ -621,22 +753,80 @@ impl CurveCollection {
|
|||||||
Ok((self.session.clone(), id))
|
Ok((self.session.clone(), id))
|
||||||
}
|
}
|
||||||
pub fn create_line(&self, start: [f64; 3], end: [f64; 3]) -> ApiResult<Line> {
|
pub fn create_line(&self, start: [f64; 3], end: [f64; 3]) -> ApiResult<Line> {
|
||||||
let (s, id) = self.create_curve(Curve2Spec::Line { start, end })?;
|
let (s, id) = self.create_curve(Curve2Spec::Line {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
layer: None,
|
||||||
|
})?;
|
||||||
|
Ok(Line::new(s, id))
|
||||||
|
}
|
||||||
|
pub fn create_line_on_layer(
|
||||||
|
&self,
|
||||||
|
start: [f64; 3],
|
||||||
|
end: [f64; 3],
|
||||||
|
layer: &str,
|
||||||
|
) -> ApiResult<Line> {
|
||||||
|
let (s, id) = self.create_curve(Curve2Spec::Line {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
layer: Some(layer.to_string()),
|
||||||
|
})?;
|
||||||
Ok(Line::new(s, id))
|
Ok(Line::new(s, id))
|
||||||
}
|
}
|
||||||
pub fn create_circle(&self, centre: [f64; 3], radius: f64) -> ApiResult<Circle> {
|
pub fn create_circle(&self, centre: [f64; 3], radius: f64) -> ApiResult<Circle> {
|
||||||
let (s, id) = self.create_curve(Curve2Spec::Circle { centre, radius })?;
|
let (s, id) = self.create_curve(Curve2Spec::Circle {
|
||||||
|
centre,
|
||||||
|
radius,
|
||||||
|
layer: None,
|
||||||
|
})?;
|
||||||
|
Ok(Circle::new(s, id))
|
||||||
|
}
|
||||||
|
pub fn create_circle_on_layer(
|
||||||
|
&self,
|
||||||
|
centre: [f64; 3],
|
||||||
|
radius: f64,
|
||||||
|
layer: &str,
|
||||||
|
) -> ApiResult<Circle> {
|
||||||
|
let (s, id) = self.create_curve(Curve2Spec::Circle {
|
||||||
|
centre,
|
||||||
|
radius,
|
||||||
|
layer: Some(layer.to_string()),
|
||||||
|
})?;
|
||||||
Ok(Circle::new(s, id))
|
Ok(Circle::new(s, id))
|
||||||
}
|
}
|
||||||
pub fn create_polyline(&self, points: &[[f64; 3]], closed: bool) -> ApiResult<Polyline> {
|
pub fn create_polyline(&self, points: &[[f64; 3]], closed: bool) -> ApiResult<Polyline> {
|
||||||
let (s, id) = self.create_curve(Curve2Spec::Polyline {
|
let (s, id) = self.create_curve(Curve2Spec::Polyline {
|
||||||
points: points.to_vec(),
|
points: points.to_vec(),
|
||||||
closed,
|
closed,
|
||||||
|
layer: None,
|
||||||
|
})?;
|
||||||
|
Ok(Polyline::new(s, id))
|
||||||
|
}
|
||||||
|
pub fn create_polyline_on_layer(
|
||||||
|
&self,
|
||||||
|
points: &[[f64; 3]],
|
||||||
|
closed: bool,
|
||||||
|
layer: &str,
|
||||||
|
) -> ApiResult<Polyline> {
|
||||||
|
let (s, id) = self.create_curve(Curve2Spec::Polyline {
|
||||||
|
points: points.to_vec(),
|
||||||
|
closed,
|
||||||
|
layer: Some(layer.to_string()),
|
||||||
})?;
|
})?;
|
||||||
Ok(Polyline::new(s, id))
|
Ok(Polyline::new(s, id))
|
||||||
}
|
}
|
||||||
pub fn create_point(&self, position: [f64; 3]) -> ApiResult<Point> {
|
pub fn create_point(&self, position: [f64; 3]) -> ApiResult<Point> {
|
||||||
let (s, id) = self.create_curve(Curve2Spec::Point { position })?;
|
let (s, id) = self.create_curve(Curve2Spec::Point {
|
||||||
|
position,
|
||||||
|
layer: None,
|
||||||
|
})?;
|
||||||
|
Ok(Point::new(s, id))
|
||||||
|
}
|
||||||
|
pub fn create_point_on_layer(&self, position: [f64; 3], layer: &str) -> ApiResult<Point> {
|
||||||
|
let (s, id) = self.create_curve(Curve2Spec::Point {
|
||||||
|
position,
|
||||||
|
layer: Some(layer.to_string()),
|
||||||
|
})?;
|
||||||
Ok(Point::new(s, id))
|
Ok(Point::new(s, id))
|
||||||
}
|
}
|
||||||
pub fn create_arc(
|
pub fn create_arc(
|
||||||
@@ -651,6 +841,24 @@ impl CurveCollection {
|
|||||||
radius,
|
radius,
|
||||||
start_angle,
|
start_angle,
|
||||||
end_angle,
|
end_angle,
|
||||||
|
layer: None,
|
||||||
|
})?;
|
||||||
|
Ok(ArcCurve::new(s, id))
|
||||||
|
}
|
||||||
|
pub fn create_arc_on_layer(
|
||||||
|
&self,
|
||||||
|
centre: [f64; 3],
|
||||||
|
radius: f64,
|
||||||
|
start_angle: f64,
|
||||||
|
end_angle: f64,
|
||||||
|
layer: &str,
|
||||||
|
) -> ApiResult<ArcCurve> {
|
||||||
|
let (s, id) = self.create_curve(Curve2Spec::Arc {
|
||||||
|
centre,
|
||||||
|
radius,
|
||||||
|
start_angle,
|
||||||
|
end_angle,
|
||||||
|
layer: Some(layer.to_string()),
|
||||||
})?;
|
})?;
|
||||||
Ok(ArcCurve::new(s, id))
|
Ok(ArcCurve::new(s, id))
|
||||||
}
|
}
|
||||||
@@ -668,6 +876,26 @@ impl CurveCollection {
|
|||||||
ratio,
|
ratio,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
layer: None,
|
||||||
|
})?;
|
||||||
|
Ok(Ellipse::new(s, id))
|
||||||
|
}
|
||||||
|
pub fn create_ellipse_on_layer(
|
||||||
|
&self,
|
||||||
|
centre: [f64; 3],
|
||||||
|
major_axis: [f64; 3],
|
||||||
|
ratio: f64,
|
||||||
|
start: f64,
|
||||||
|
end: f64,
|
||||||
|
layer: &str,
|
||||||
|
) -> ApiResult<Ellipse> {
|
||||||
|
let (s, id) = self.create_curve(Curve2Spec::Ellipse {
|
||||||
|
centre,
|
||||||
|
major_axis,
|
||||||
|
ratio,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
layer: Some(layer.to_string()),
|
||||||
})?;
|
})?;
|
||||||
Ok(Ellipse::new(s, id))
|
Ok(Ellipse::new(s, id))
|
||||||
}
|
}
|
||||||
@@ -683,15 +911,67 @@ impl CurveCollection {
|
|||||||
control_points: control_points.to_vec(),
|
control_points: control_points.to_vec(),
|
||||||
knots: knots.to_vec(),
|
knots: knots.to_vec(),
|
||||||
weights: weights.to_vec(),
|
weights: weights.to_vec(),
|
||||||
|
layer: None,
|
||||||
|
})?;
|
||||||
|
Ok(Spline::new(s, id))
|
||||||
|
}
|
||||||
|
pub fn create_spline_on_layer(
|
||||||
|
&self,
|
||||||
|
degree: i32,
|
||||||
|
control_points: &[[f64; 3]],
|
||||||
|
knots: &[f64],
|
||||||
|
weights: &[f64],
|
||||||
|
layer: &str,
|
||||||
|
) -> ApiResult<Spline> {
|
||||||
|
let (s, id) = self.create_curve(Curve2Spec::Spline {
|
||||||
|
degree,
|
||||||
|
control_points: control_points.to_vec(),
|
||||||
|
knots: knots.to_vec(),
|
||||||
|
weights: weights.to_vec(),
|
||||||
|
layer: Some(layer.to_string()),
|
||||||
})?;
|
})?;
|
||||||
Ok(Spline::new(s, id))
|
Ok(Spline::new(s, id))
|
||||||
}
|
}
|
||||||
pub fn create_ray(&self, origin: [f64; 3], direction: [f64; 3]) -> ApiResult<Ray> {
|
pub fn create_ray(&self, origin: [f64; 3], direction: [f64; 3]) -> ApiResult<Ray> {
|
||||||
let (s, id) = self.create_curve(Curve2Spec::Ray { origin, direction })?;
|
let (s, id) = self.create_curve(Curve2Spec::Ray {
|
||||||
|
origin,
|
||||||
|
direction,
|
||||||
|
layer: None,
|
||||||
|
})?;
|
||||||
|
Ok(Ray::new(s, id))
|
||||||
|
}
|
||||||
|
pub fn create_ray_on_layer(
|
||||||
|
&self,
|
||||||
|
origin: [f64; 3],
|
||||||
|
direction: [f64; 3],
|
||||||
|
layer: &str,
|
||||||
|
) -> ApiResult<Ray> {
|
||||||
|
let (s, id) = self.create_curve(Curve2Spec::Ray {
|
||||||
|
origin,
|
||||||
|
direction,
|
||||||
|
layer: Some(layer.to_string()),
|
||||||
|
})?;
|
||||||
Ok(Ray::new(s, id))
|
Ok(Ray::new(s, id))
|
||||||
}
|
}
|
||||||
pub fn create_xline(&self, origin: [f64; 3], direction: [f64; 3]) -> ApiResult<XLine> {
|
pub fn create_xline(&self, origin: [f64; 3], direction: [f64; 3]) -> ApiResult<XLine> {
|
||||||
let (s, id) = self.create_curve(Curve2Spec::XLine { origin, direction })?;
|
let (s, id) = self.create_curve(Curve2Spec::XLine {
|
||||||
|
origin,
|
||||||
|
direction,
|
||||||
|
layer: None,
|
||||||
|
})?;
|
||||||
|
Ok(XLine::new(s, id))
|
||||||
|
}
|
||||||
|
pub fn create_xline_on_layer(
|
||||||
|
&self,
|
||||||
|
origin: [f64; 3],
|
||||||
|
direction: [f64; 3],
|
||||||
|
layer: &str,
|
||||||
|
) -> ApiResult<XLine> {
|
||||||
|
let (s, id) = self.create_curve(Curve2Spec::XLine {
|
||||||
|
origin,
|
||||||
|
direction,
|
||||||
|
layer: Some(layer.to_string()),
|
||||||
|
})?;
|
||||||
Ok(XLine::new(s, id))
|
Ok(XLine::new(s, id))
|
||||||
}
|
}
|
||||||
/// A RASTER_IMAGE placed at `insertion_point` (host auto-registers the image definition).
|
/// A RASTER_IMAGE placed at `insertion_point` (host auto-registers the image definition).
|
||||||
@@ -874,10 +1154,42 @@ impl CurveCollection {
|
|||||||
Ok(MText::new(self.session.clone(), id))
|
Ok(MText::new(self.session.clone(), id))
|
||||||
}
|
}
|
||||||
/// Bulk-create many points in ONE op: all-or-nothing, one undo step.
|
/// Bulk-create many points in ONE op: all-or-nothing, one undo step.
|
||||||
|
/// Places points on the current layer (`layer: None`).
|
||||||
pub fn create_points(&self, positions: &[[f64; 3]]) -> ApiResult<Vec<Point>> {
|
pub fn create_points(&self, positions: &[[f64; 3]]) -> ApiResult<Vec<Point>> {
|
||||||
let specs: Vec<crate::ops::EntitySpec> = positions
|
let specs: Vec<crate::ops::EntitySpec> = positions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&p| crate::ops::EntitySpec::Curve(Curve2Spec::Point { position: p }))
|
.map(|&p| {
|
||||||
|
crate::ops::EntitySpec::Curve(crate::ops::Curve2Spec::Point {
|
||||||
|
position: p,
|
||||||
|
layer: None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let receipt = self.session.apply_op(Operation::CreateMany(specs))?;
|
||||||
|
let outcome = receipt
|
||||||
|
.outcome
|
||||||
|
.ok_or_else(|| ApiError::Transport("create_many returned no outcome".into()))?;
|
||||||
|
Ok(outcome
|
||||||
|
.new_ids()
|
||||||
|
.iter()
|
||||||
|
.map(|&id| Point::new(self.session.clone(), id))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bulk-create many points on a specific layer in ONE op.
|
||||||
|
pub fn create_points_on_layer(
|
||||||
|
&self,
|
||||||
|
positions: &[[f64; 3]],
|
||||||
|
layer: &str,
|
||||||
|
) -> ApiResult<Vec<Point>> {
|
||||||
|
let specs: Vec<crate::ops::EntitySpec> = positions
|
||||||
|
.iter()
|
||||||
|
.map(|&p| {
|
||||||
|
crate::ops::EntitySpec::Curve(crate::ops::Curve2Spec::Point {
|
||||||
|
position: p,
|
||||||
|
layer: Some(layer.to_string()),
|
||||||
|
})
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let receipt = self.session.apply_op(Operation::CreateMany(specs))?;
|
let receipt = self.session.apply_op(Operation::CreateMany(specs))?;
|
||||||
let outcome = receipt
|
let outcome = receipt
|
||||||
@@ -1001,6 +1313,41 @@ impl EntityCollection {
|
|||||||
self.session.apply_op(Operation::Delete { id })?;
|
self.session.apply_op(Operation::Delete { id })?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a standalone `XRECORD` object.
|
||||||
|
pub fn create_xrecord(&self, spec: &XRecordSpec) -> ApiResult<XRecord> {
|
||||||
|
let receipt = self
|
||||||
|
.session
|
||||||
|
.apply_op(Operation::CreateXRecord(spec.clone()))?;
|
||||||
|
let id = receipt
|
||||||
|
.outcome
|
||||||
|
.and_then(|o| o.new_id())
|
||||||
|
.ok_or_else(|| ApiError::Transport("create_xrecord returned no id".into()))?;
|
||||||
|
Ok(XRecord::new(self.session.clone(), id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lookup an `XRECORD` object by id and return a typed accessor handle.
|
||||||
|
pub fn get_xrecord(&self, id: ObjectId) -> ApiResult<XRecord> {
|
||||||
|
// Validate existence + that the payload is readable as an XRecord.
|
||||||
|
match self.session.one_query(Query::GetXRecord { id })? {
|
||||||
|
QueryResult::XRecord(_) => Ok(XRecord::new(self.session.clone(), id)),
|
||||||
|
_ => Err(ApiError::Transport("unexpected xrecord result".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bulk-create many entities in ONE op (all-or-nothing, one undo step).
|
||||||
|
pub fn create_many(&self, specs: &[crate::ops::EntitySpec]) -> ApiResult<Vec<Entity>> {
|
||||||
|
let receipt = self.session.apply_op(Operation::CreateMany(specs.to_vec()))?;
|
||||||
|
let outcome = receipt
|
||||||
|
.outcome
|
||||||
|
.ok_or_else(|| ApiError::Transport("create_many returned no outcome".into()))?;
|
||||||
|
Ok(outcome
|
||||||
|
.new_ids()
|
||||||
|
.iter()
|
||||||
|
.map(|&id| Entity::new(self.session.clone(), id))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
/// Bulk transform (one op, all-or-nothing).
|
/// Bulk transform (one op, all-or-nothing).
|
||||||
pub fn transform_many(&self, ids: &[ObjectId], placement: PlacementSpec) -> ApiResult<()> {
|
pub fn transform_many(&self, ids: &[ObjectId], placement: PlacementSpec) -> ApiResult<()> {
|
||||||
self.session.apply_op(Operation::TransformMany {
|
self.session.apply_op(Operation::TransformMany {
|
||||||
|
|||||||
@@ -192,6 +192,30 @@ Table: create_table(insertion_point, data[row][col]) builds rows x columns of te
|
|||||||
|
|
||||||
- **create_table**(insertion_point: [f64; 3], data: &[Vec<String>]) -> `Entity` — `CreateTable`
|
- **create_table**(insertion_point: [f64; 3], data: &[Vec<String>]) -> `Entity` — `CreateTable`
|
||||||
|
|
||||||
|
## `XRecord` (acadrust `XRecord`, collection `entities`)
|
||||||
|
|
||||||
|
Extended record (XRecord): arbitrary DXF group-code/value pairs. Roundtripped verbatim as an XRecordSpec payload.
|
||||||
|
|
||||||
|
### Constructors (`doc.entities()`)
|
||||||
|
|
||||||
|
- **create_xrecord**(spec: XRecordSpec) -> `XRecord` — `CreateXRecord`
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- **payload**() -> `XRecordSpec` — query `GetXRecord`
|
||||||
|
- **set_payload**(spec: XRecordSpec) -> `()` — op `SetXRecord`
|
||||||
|
|
||||||
|
## `Layer` (acadrust `-`, collection `layers`)
|
||||||
|
|
||||||
|
Named drawing layer (color, linetype, lineweight, visibility). Not an entity handle; accessed through Document::layers().
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- **list**() -> `Vec<LayerInfo>` — query `ListLayers`
|
||||||
|
- **create**(info: LayerInfo) -> `()` — op `CreateLayer`
|
||||||
|
- **update**(name: String, info: LayerInfo) -> `()` — op `UpdateLayer`
|
||||||
|
- **delete**(name: String) -> `()` — op `DeleteLayer`
|
||||||
|
|
||||||
## Generic methods (every handle)
|
## Generic methods (every handle)
|
||||||
|
|
||||||
Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `ArcCurve`,
|
Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `ArcCurve`,
|
||||||
@@ -204,9 +228,32 @@ Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `ArcCurve`,
|
|||||||
- **`transform(placement)`** — in-place rigid similarity (same `ObjectId`). **v1:
|
- **`transform(placement)`** — in-place rigid similarity (same `ObjectId`). **v1:
|
||||||
solids only.**
|
solids only.**
|
||||||
- **`delete()`** — remove the entity (one undo step).
|
- **`delete()`** — remove the entity (one undo step).
|
||||||
|
- **`layer()` -> `String`** — the name of the layer the entity is on.
|
||||||
|
- **`set_layer(layer)`** — move the entity to an existing layer (one undo step;
|
||||||
|
blocked if the entity or target layer is locked).
|
||||||
|
|
||||||
`Entity` additionally has `view()` (id + kind + bounds) and `as_solid()` (typed
|
`Entity` additionally has `view()` (id + kind + bounds), `as_solid()` (typed
|
||||||
downcast when `kind == "Solid3D"`).
|
downcast when `kind == "Solid3D"`), and XDATA access through
|
||||||
|
`xdata(application_name)` / `set_xdata(application_name, record)`.
|
||||||
|
|
||||||
|
## Layer table
|
||||||
|
|
||||||
|
Layers are a named document table, not an entity family. `Document::layers()`
|
||||||
|
lists all layers as `Vec<LayerInfo>`. Table-level ops are `CreateLayer(info)`,
|
||||||
|
`UpdateLayer { name, info }` and `DeleteLayer { name }`. `Entity::layer()`
|
||||||
|
returns the layer name and `Entity::set_layer(layer)` moves the entity to an
|
||||||
|
existing layer. The host rejects duplicate names, deletion of layer "0" or the
|
||||||
|
current layer, and removing a layer that still has entities assigned.
|
||||||
|
|
||||||
|
## Transports
|
||||||
|
|
||||||
|
- **`InProcess`** (feature `host`) — drives a `DocApiBackend` directly in the host.
|
||||||
|
- **`OcsPluginApiIpc`** (feature `ipc`) — serializes envelopes over the
|
||||||
|
`ocs_plugin_api` `PluginRequestSender`/`PluginRequest` channel. The host routes
|
||||||
|
`DocApiRequest { tab_id, bytes }` to the same executor.
|
||||||
|
- **`doc_api_for_host(host)`** (feature `doc_api_host`) — convenience helper that
|
||||||
|
builds a `DocApi` from any `ocs_plugin_api::host::HostApi` exposing a
|
||||||
|
`PluginRequestSender` (out-of-process / worker-thread plugins).
|
||||||
|
|
||||||
## Collections & cross-cutting
|
## Collections & cross-cutting
|
||||||
|
|
||||||
@@ -284,6 +331,14 @@ Every `Operation` is ONE atomic write op (one undo step); every `Query` is read-
|
|||||||
- `CreateAttributeDefinition(AttributeDefinitionSpec)`
|
- `CreateAttributeDefinition(AttributeDefinitionSpec)`
|
||||||
- `CreateTable(TableSpec)`
|
- `CreateTable(TableSpec)`
|
||||||
- `CreateDimensionAngular2Ln(DimensionAngularSpec)`
|
- `CreateDimensionAngular2Ln(DimensionAngularSpec)`
|
||||||
|
- `SetXData { id: ObjectId, application_name: String, record: Option<XDataRecord> }`
|
||||||
|
- `CreateXRecord(XRecordSpec)`
|
||||||
|
- `SetXRecord { id: ObjectId, spec: XRecordSpec }`
|
||||||
|
- `CreateLayer(LayerInfo)`
|
||||||
|
- `UpdateLayer { name: String, info: LayerInfo }`
|
||||||
|
- `DeleteLayer { name: String }`
|
||||||
|
- `SetEntityLayer { id: ObjectId, layer: String }`
|
||||||
|
- `SetXDataMany(Vec<(ObjectId, String, Option<XDataRecord)>)`
|
||||||
|
|
||||||
### `Query`
|
### `Query`
|
||||||
|
|
||||||
@@ -299,6 +354,10 @@ Every `Operation` is ONE atomic write op (one undo step); every `Query` is read-
|
|||||||
- `GetAttributes { id: ObjectId }`
|
- `GetAttributes { id: ObjectId }`
|
||||||
- `GetBlockEntities { block_name: String }`
|
- `GetBlockEntities { block_name: String }`
|
||||||
- `GetViewportView { id: ObjectId }`
|
- `GetViewportView { id: ObjectId }`
|
||||||
|
- `GetXData { id: ObjectId, application_name: String }`
|
||||||
|
- `GetXRecord { id: ObjectId }`
|
||||||
|
- `ListLayers`
|
||||||
|
- `GetEntityLayer { id: ObjectId }`
|
||||||
|
|
||||||
## Errors (`ApiError`)
|
## Errors (`ApiError`)
|
||||||
|
|
||||||
|
|||||||
@@ -1124,6 +1124,128 @@
|
|||||||
"handle": "Entity",
|
"handle": "Entity",
|
||||||
"methods": [],
|
"methods": [],
|
||||||
"name": "Table"
|
"name": "Table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"acadrust_variant": "XRecord",
|
||||||
|
"collection": "entities",
|
||||||
|
"constructors": [
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "spec",
|
||||||
|
"ty": "XRecordSpec"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"name": "create_xrecord",
|
||||||
|
"op": "CreateXRecord",
|
||||||
|
"returns": "XRecord"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": "XRecord",
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "query",
|
||||||
|
"name": "payload",
|
||||||
|
"op": null,
|
||||||
|
"query": "GetXRecord",
|
||||||
|
"read_only": true,
|
||||||
|
"returns": "XRecordSpec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "spec",
|
||||||
|
"ty": "XRecordSpec"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "op",
|
||||||
|
"name": "set_payload",
|
||||||
|
"op": "SetXRecord",
|
||||||
|
"query": null,
|
||||||
|
"read_only": false,
|
||||||
|
"returns": "()"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "XRecord"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"acadrust_variant": null,
|
||||||
|
"collection": "layers",
|
||||||
|
"constructors": [],
|
||||||
|
"handle": "Layer",
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "query",
|
||||||
|
"name": "list",
|
||||||
|
"op": null,
|
||||||
|
"query": "ListLayers",
|
||||||
|
"read_only": true,
|
||||||
|
"returns": "Vec<LayerInfo>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "info",
|
||||||
|
"ty": "LayerInfo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "op",
|
||||||
|
"name": "create",
|
||||||
|
"op": "CreateLayer",
|
||||||
|
"query": null,
|
||||||
|
"read_only": false,
|
||||||
|
"returns": "()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ty": "String"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "info",
|
||||||
|
"ty": "LayerInfo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "op",
|
||||||
|
"name": "update",
|
||||||
|
"op": "UpdateLayer",
|
||||||
|
"query": null,
|
||||||
|
"read_only": false,
|
||||||
|
"returns": "()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ty": "String"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "op",
|
||||||
|
"name": "delete",
|
||||||
|
"op": "DeleteLayer",
|
||||||
|
"query": null,
|
||||||
|
"read_only": false,
|
||||||
|
"returns": "()"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Layer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"layouts": {
|
"layouts": {
|
||||||
@@ -1164,7 +1286,15 @@
|
|||||||
"CreateDimensionAngular(DimensionAngularSpec)",
|
"CreateDimensionAngular(DimensionAngularSpec)",
|
||||||
"CreateAttributeDefinition(AttributeDefinitionSpec)",
|
"CreateAttributeDefinition(AttributeDefinitionSpec)",
|
||||||
"CreateTable(TableSpec)",
|
"CreateTable(TableSpec)",
|
||||||
"CreateDimensionAngular2Ln(DimensionAngularSpec)"
|
"CreateDimensionAngular2Ln(DimensionAngularSpec)",
|
||||||
|
"SetXData { id: ObjectId, application_name: String, record: Option<XDataRecord> }",
|
||||||
|
"CreateXRecord(XRecordSpec)",
|
||||||
|
"SetXRecord { id: ObjectId, spec: XRecordSpec }",
|
||||||
|
"CreateLayer(LayerInfo)",
|
||||||
|
"UpdateLayer { name: String, info: LayerInfo }",
|
||||||
|
"DeleteLayer { name: String }",
|
||||||
|
"SetEntityLayer { id: ObjectId, layer: String }",
|
||||||
|
"SetXDataMany(Vec<(ObjectId, String, Option<XDataRecord)>)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Query": {
|
"Query": {
|
||||||
@@ -1180,7 +1310,11 @@
|
|||||||
"GetDimensionMeasurement { id: ObjectId }",
|
"GetDimensionMeasurement { id: ObjectId }",
|
||||||
"GetAttributes { id: ObjectId }",
|
"GetAttributes { id: ObjectId }",
|
||||||
"GetBlockEntities { block_name: String }",
|
"GetBlockEntities { block_name: String }",
|
||||||
"GetViewportView { id: ObjectId }"
|
"GetViewportView { id: ObjectId }",
|
||||||
|
"GetXData { id: ObjectId, application_name: String }",
|
||||||
|
"GetXRecord { id: ObjectId }",
|
||||||
|
"ListLayers",
|
||||||
|
"GetEntityLayer { id: ObjectId }"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1205,7 +1339,9 @@
|
|||||||
"XLine",
|
"XLine",
|
||||||
"Text",
|
"Text",
|
||||||
"MText",
|
"MText",
|
||||||
"Dimension"
|
"Dimension",
|
||||||
|
"XRecord",
|
||||||
|
"Layer"
|
||||||
],
|
],
|
||||||
"root": "DocApi"
|
"root": "DocApi"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,15 @@
|
|||||||
"CreateDimensionAngular(DimensionAngularSpec)",
|
"CreateDimensionAngular(DimensionAngularSpec)",
|
||||||
"CreateAttributeDefinition(AttributeDefinitionSpec)",
|
"CreateAttributeDefinition(AttributeDefinitionSpec)",
|
||||||
"CreateTable(TableSpec)",
|
"CreateTable(TableSpec)",
|
||||||
"CreateDimensionAngular2Ln(DimensionAngularSpec)"
|
"CreateDimensionAngular2Ln(DimensionAngularSpec)",
|
||||||
|
"SetXData { id: ObjectId, application_name: String, record: Option<XDataRecord> }",
|
||||||
|
"CreateXRecord(XRecordSpec)",
|
||||||
|
"SetXRecord { id: ObjectId, spec: XRecordSpec }",
|
||||||
|
"CreateLayer(LayerInfo)",
|
||||||
|
"UpdateLayer { name: String, info: LayerInfo }",
|
||||||
|
"DeleteLayer { name: String }",
|
||||||
|
"SetEntityLayer { id: ObjectId, layer: String }",
|
||||||
|
"SetXDataMany(Vec<(ObjectId, String, Option<XDataRecord)>)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Query": {
|
"Query": {
|
||||||
@@ -45,7 +53,11 @@
|
|||||||
"GetDimensionMeasurement { id: ObjectId }",
|
"GetDimensionMeasurement { id: ObjectId }",
|
||||||
"GetAttributes { id: ObjectId }",
|
"GetAttributes { id: ObjectId }",
|
||||||
"GetBlockEntities { block_name: String }",
|
"GetBlockEntities { block_name: String }",
|
||||||
"GetViewportView { id: ObjectId }"
|
"GetViewportView { id: ObjectId }",
|
||||||
|
"GetXData { id: ObjectId, application_name: String }",
|
||||||
|
"GetXRecord { id: ObjectId }",
|
||||||
|
"ListLayers",
|
||||||
|
"GetEntityLayer { id: ObjectId }"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ use crate::id::ObjectId;
|
|||||||
use crate::ops::{
|
use crate::ops::{
|
||||||
AttributeDefinitionSpec, BoolOp, Curve2Spec, DimensionAngularSpec, DimensionRadialSpec,
|
AttributeDefinitionSpec, BoolOp, Curve2Spec, DimensionAngularSpec, DimensionRadialSpec,
|
||||||
DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, RasterImageSpec,
|
DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, RasterImageSpec,
|
||||||
SolidPrimitive, TableSpec, TextSpec, ViewportSpec,
|
SolidPrimitive, TableSpec, TextSpec, ViewportSpec, XDataRecord, XRecordSpec,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A typed write operation. Each variant is ONE atomic host call = one
|
/// A typed write operation. Each variant is ONE atomic host call = one
|
||||||
@@ -99,4 +99,29 @@ pub enum Operation {
|
|||||||
CreateTable(TableSpec),
|
CreateTable(TableSpec),
|
||||||
// dimension sub-types (2-line angular)
|
// dimension sub-types (2-line angular)
|
||||||
CreateDimensionAngular2Ln(DimensionAngularSpec),
|
CreateDimensionAngular2Ln(DimensionAngularSpec),
|
||||||
|
// xdata / xrecord roundtrip
|
||||||
|
SetXData {
|
||||||
|
id: ObjectId,
|
||||||
|
application_name: String,
|
||||||
|
record: Option<XDataRecord>,
|
||||||
|
},
|
||||||
|
SetXDataMany(Vec<(ObjectId, String, Option<XDataRecord>)>),
|
||||||
|
CreateXRecord(XRecordSpec),
|
||||||
|
SetXRecord {
|
||||||
|
id: ObjectId,
|
||||||
|
spec: XRecordSpec,
|
||||||
|
},
|
||||||
|
// layer CRUD
|
||||||
|
CreateLayer(crate::ops::LayerInfo),
|
||||||
|
UpdateLayer {
|
||||||
|
name: String,
|
||||||
|
info: crate::ops::LayerInfo,
|
||||||
|
},
|
||||||
|
DeleteLayer {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
SetEntityLayer {
|
||||||
|
id: ObjectId,
|
||||||
|
layer: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -55,4 +55,35 @@ pub enum Query {
|
|||||||
GetViewportView {
|
GetViewportView {
|
||||||
id: ObjectId,
|
id: ObjectId,
|
||||||
},
|
},
|
||||||
|
/// The XDATA record for `application_name` attached to an entity (`None` if absent).
|
||||||
|
GetXData {
|
||||||
|
id: ObjectId,
|
||||||
|
application_name: String,
|
||||||
|
},
|
||||||
|
/// The XRECORD object payload by id.
|
||||||
|
GetXRecord {
|
||||||
|
id: ObjectId,
|
||||||
|
},
|
||||||
|
/// List all layers in the document.
|
||||||
|
ListLayers,
|
||||||
|
/// The layer name of an entity.
|
||||||
|
GetEntityLayer {
|
||||||
|
id: ObjectId,
|
||||||
|
},
|
||||||
|
/// Enumerate first-class entities, optionally filtered by kind and/or layer.
|
||||||
|
/// `include_bounds` populates `EntityView::bounds`; leave it false for a
|
||||||
|
/// lightweight list view.
|
||||||
|
EnumerateEntities {
|
||||||
|
kind: Option<String>,
|
||||||
|
layer: Option<String>,
|
||||||
|
include_bounds: bool,
|
||||||
|
},
|
||||||
|
/// The WCS location of a Point entity.
|
||||||
|
GetPointPosition {
|
||||||
|
id: ObjectId,
|
||||||
|
},
|
||||||
|
/// The start and end WCS points of a Line entity.
|
||||||
|
GetLineGeometry {
|
||||||
|
id: ObjectId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
//! Shared layer-name normalization rules.
|
||||||
|
//!
|
||||||
|
//! Layer names in OpenCADStudio/acadrust are case-insensitive, leading/trailing
|
||||||
|
//! whitespace is ignored, and empty names are invalid. The host (`normalize_name`
|
||||||
|
//! in `acadrust::tables`) implements the canonical rule set; this module re-exports
|
||||||
|
//! a compatible, dependency-free function for the wire side and for backends that
|
||||||
|
//! do not link against `acadrust` directly.
|
||||||
|
|
||||||
|
/// Normalize a raw layer name into its case-insensitive lookup key.
|
||||||
|
///
|
||||||
|
/// This mirrors `acadrust::tables::normalize_name` used by the host:
|
||||||
|
/// - trims leading and trailing ASCII whitespace
|
||||||
|
/// - converts the result to uppercase
|
||||||
|
/// - returns `""` if the name is empty after trimming
|
||||||
|
pub fn normalize_layer_name(name: &str) -> String {
|
||||||
|
name.trim().to_ascii_uppercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A normalized layer name is non-empty and matches the host lookup key.
|
||||||
|
pub fn is_valid_layer_name(name: &str) -> bool {
|
||||||
|
!normalize_layer_name(name).is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalization_trims_and_uppercases() {
|
||||||
|
assert_eq!(normalize_layer_name(" Walls "), "WALLS");
|
||||||
|
assert_eq!(normalize_layer_name("walls"), "WALLS");
|
||||||
|
assert_eq!(normalize_layer_name("WALLS"), "WALLS");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_and_whitespace_names_are_invalid() {
|
||||||
|
assert_eq!(normalize_layer_name(""), "");
|
||||||
|
assert_eq!(normalize_layer_name(" "), "");
|
||||||
|
assert!(!is_valid_layer_name(""));
|
||||||
|
assert!(!is_valid_layer_name(" "));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ mod validation;
|
|||||||
|
|
||||||
pub mod envelope;
|
pub mod envelope;
|
||||||
pub mod facade;
|
pub mod facade;
|
||||||
|
pub mod layer;
|
||||||
pub mod ops;
|
pub mod ops;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
@@ -41,9 +42,14 @@ pub use envelope::{DocApiEnvelope, EnvelopeBody, OpOutcome, Receipt};
|
|||||||
pub use facade::{
|
pub use facade::{
|
||||||
ArcCurve, Circle, CurveCollection, Dimension, DocApi, Document, Ellipse, Entity,
|
ArcCurve, Circle, CurveCollection, Dimension, DocApi, Document, Ellipse, Entity,
|
||||||
EntityCollection, HasId, Line, MText, OpGroup, Point, Polyline, QueryBatch, QueryResults, Ray,
|
EntityCollection, HasId, Line, MText, OpGroup, Point, Polyline, QueryBatch, QueryResults, Ray,
|
||||||
Solid, SolidCollection, Spline, Text, XLine,
|
Solid, SolidCollection, Spline, Text, XLine, XRecord,
|
||||||
|
};
|
||||||
|
pub use layer::{is_valid_layer_name, normalize_layer_name};
|
||||||
|
pub use ops::{
|
||||||
|
BoolOp, Color, Curve2Spec, EntitySpec, LayerFlags, LayerInfo, LineWeight, Operation,
|
||||||
|
PlacementSpec, SolidPrimitive, XDataRecord, XDataValue, XRecordCloningFlags, XRecordEntry,
|
||||||
|
XRecordSpec, XRecordValue,
|
||||||
};
|
};
|
||||||
pub use ops::{BoolOp, Curve2Spec, EntitySpec, Operation, PlacementSpec, SolidPrimitive};
|
|
||||||
pub use query::{Aabb, EntityView, Query, QueryResult};
|
pub use query::{Aabb, EntityView, Query, QueryResult};
|
||||||
|
|
||||||
#[cfg(feature = "host")]
|
#[cfg(feature = "host")]
|
||||||
@@ -51,6 +57,9 @@ pub use convert::{
|
|||||||
bulge_arc_segment, curve_spec_to_entity, entity_bounds, entity_kind_name,
|
bulge_arc_segment, curve_spec_to_entity, entity_bounds, entity_kind_name,
|
||||||
entity_to_profile_curves, transform_entity_geometry,
|
entity_to_profile_curves, transform_entity_geometry,
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "doc_api_host")]
|
||||||
|
pub use transport::ipc::doc_api_for_host;
|
||||||
|
|
||||||
pub use transport::Transport;
|
pub use transport::Transport;
|
||||||
|
|
||||||
/// The crate's own wire-envelope protocol version (`DocApiEnvelope::version`).
|
/// The crate's own wire-envelope protocol version (`DocApiEnvelope::version`).
|
||||||
|
|||||||
@@ -8,6 +8,80 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
pub use crate::gen::Operation;
|
pub use crate::gen::Operation;
|
||||||
|
|
||||||
|
/// An XDATA record payload: one registered application name plus its typed
|
||||||
|
/// value list. This is a plain-data, serde-compatible mirror of
|
||||||
|
/// `acadrust::xdata::ExtendedDataRecord` so the wire DTO stays independent of
|
||||||
|
/// the host dependency.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct XDataRecord {
|
||||||
|
pub application_name: String,
|
||||||
|
pub values: Vec<XDataValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single XDATA value. Mirrors `acadrust::xdata::XDataValue`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum XDataValue {
|
||||||
|
String(String),
|
||||||
|
ControlString(String),
|
||||||
|
LayerName(String),
|
||||||
|
BinaryData(Vec<u8>),
|
||||||
|
Handle(u64),
|
||||||
|
Point3D([f64; 3]),
|
||||||
|
Position3D([f64; 3]),
|
||||||
|
Displacement3D([f64; 3]),
|
||||||
|
Direction3D([f64; 3]),
|
||||||
|
Real(f64),
|
||||||
|
Distance(f64),
|
||||||
|
ScaleFactor(f64),
|
||||||
|
Integer16(i16),
|
||||||
|
Integer32(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construction spec for an `XRECORD` object. Mirrors the acadrust
|
||||||
|
/// `XRecord` payload (name + cloning flags + group-code entries) in a
|
||||||
|
/// serde-compatible, host-independent form.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct XRecordSpec {
|
||||||
|
pub name: String,
|
||||||
|
pub cloning_flags: XRecordCloningFlags,
|
||||||
|
pub entries: Vec<XRecordEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// XRecord cloning behavior flags. Mirror of
|
||||||
|
/// `acadrust::objects::DictionaryCloningFlags`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
pub enum XRecordCloningFlags {
|
||||||
|
#[default]
|
||||||
|
NotApplicable,
|
||||||
|
KeepExisting,
|
||||||
|
UseClone,
|
||||||
|
XrefName,
|
||||||
|
Name,
|
||||||
|
UnmangleName,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One XRECORD group-code entry.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct XRecordEntry {
|
||||||
|
pub code: i32,
|
||||||
|
pub value: XRecordValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single XRECORD value. Mirrors `acadrust::objects::XRecordValue`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum XRecordValue {
|
||||||
|
String(String),
|
||||||
|
Double(f64),
|
||||||
|
Int16(i16),
|
||||||
|
Int32(i32),
|
||||||
|
Int64(i64),
|
||||||
|
Byte(u8),
|
||||||
|
Bool(bool),
|
||||||
|
Handle(u64),
|
||||||
|
Point3D([f64; 3]),
|
||||||
|
Chunk(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
/// Boolean operation kind for [`Operation::SolidBoolean`].
|
/// Boolean operation kind for [`Operation::SolidBoolean`].
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum BoolOp {
|
pub enum BoolOp {
|
||||||
@@ -16,6 +90,72 @@ pub enum BoolOp {
|
|||||||
Difference,
|
Difference,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A plain-data color value. Mirrors `acadrust::types::Color` for wire
|
||||||
|
/// independence from the host dependency.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Color {
|
||||||
|
ByLayer,
|
||||||
|
None,
|
||||||
|
ByBlock,
|
||||||
|
Index(u8),
|
||||||
|
Rgb { r: u8, g: u8, b: u8 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
pub const WHITE: Color = Color::Index(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A plain-data line weight. Mirrors `acadrust::types::LineWeight`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
pub enum LineWeight {
|
||||||
|
#[default]
|
||||||
|
ByLayer,
|
||||||
|
ByBlock,
|
||||||
|
Default,
|
||||||
|
/// Specific line weight in 1/100 mm.
|
||||||
|
Value(i16),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layer visibility/lock flags.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
pub struct LayerFlags {
|
||||||
|
pub frozen: bool,
|
||||||
|
pub locked: bool,
|
||||||
|
pub frozen_in_new_viewport: bool,
|
||||||
|
pub off: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A plain-data layer table entry. Mirrors `acadrust::tables::Layer`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct LayerInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub flags: LayerFlags,
|
||||||
|
pub color: Color,
|
||||||
|
pub line_type: String,
|
||||||
|
pub line_weight: LineWeight,
|
||||||
|
pub plot_style: String,
|
||||||
|
pub is_plottable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayerInfo {
|
||||||
|
pub fn new(name: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
flags: LayerFlags::default(),
|
||||||
|
color: Color::WHITE,
|
||||||
|
line_type: "Continuous".to_string(),
|
||||||
|
line_weight: LineWeight::Default,
|
||||||
|
plot_style: String::new(),
|
||||||
|
is_plottable: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The normalized (trimmed + uppercased) lookup key for this layer name.
|
||||||
|
pub fn normalized_name(&self) -> String {
|
||||||
|
crate::layer::normalize_layer_name(&self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Construction spec for a B-rep solid primitive (plain-data mirror of the
|
/// Construction spec for a B-rep solid primitive (plain-data mirror of the
|
||||||
/// non-serde `cadkernel::brep::make::*` arguments).
|
/// non-serde `cadkernel::brep::make::*` arguments).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
@@ -55,17 +195,25 @@ pub enum Curve2Spec {
|
|||||||
Line {
|
Line {
|
||||||
start: [f64; 3],
|
start: [f64; 3],
|
||||||
end: [f64; 3],
|
end: [f64; 3],
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
Circle {
|
Circle {
|
||||||
centre: [f64; 3],
|
centre: [f64; 3],
|
||||||
radius: f64,
|
radius: f64,
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
Polyline {
|
Polyline {
|
||||||
points: Vec<[f64; 3]>,
|
points: Vec<[f64; 3]>,
|
||||||
closed: bool,
|
closed: bool,
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
Point {
|
Point {
|
||||||
position: [f64; 3],
|
position: [f64; 3],
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
/// Circular arc, counter-clockwise from `start_angle` to `end_angle` (radians).
|
/// Circular arc, counter-clockwise from `start_angle` to `end_angle` (radians).
|
||||||
Arc {
|
Arc {
|
||||||
@@ -73,6 +221,8 @@ pub enum Curve2Spec {
|
|||||||
radius: f64,
|
radius: f64,
|
||||||
start_angle: f64,
|
start_angle: f64,
|
||||||
end_angle: f64,
|
end_angle: f64,
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
/// Ellipse: `major_axis` is the major-axis endpoint relative to `centre`;
|
/// Ellipse: `major_axis` is the major-axis endpoint relative to `centre`;
|
||||||
/// `ratio` = minor/major. `start`/`end` parameters 0..2π (full ellipse = 0..2π).
|
/// `ratio` = minor/major. `start`/`end` parameters 0..2π (full ellipse = 0..2π).
|
||||||
@@ -82,6 +232,8 @@ pub enum Curve2Spec {
|
|||||||
ratio: f64,
|
ratio: f64,
|
||||||
start: f64,
|
start: f64,
|
||||||
end: f64,
|
end: f64,
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
/// NURBS spline (control points + knots + weights; `degree` typically 3).
|
/// NURBS spline (control points + knots + weights; `degree` typically 3).
|
||||||
Spline {
|
Spline {
|
||||||
@@ -89,19 +241,42 @@ pub enum Curve2Spec {
|
|||||||
control_points: Vec<[f64; 3]>,
|
control_points: Vec<[f64; 3]>,
|
||||||
knots: Vec<f64>,
|
knots: Vec<f64>,
|
||||||
weights: Vec<f64>,
|
weights: Vec<f64>,
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
/// A ray from `origin` along `direction` (bounded at origin only).
|
/// A ray from `origin` along `direction` (bounded at origin only).
|
||||||
Ray {
|
Ray {
|
||||||
origin: [f64; 3],
|
origin: [f64; 3],
|
||||||
direction: [f64; 3],
|
direction: [f64; 3],
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
/// An infinite construction line through `origin` along `direction`.
|
/// An infinite construction line through `origin` along `direction`.
|
||||||
XLine {
|
XLine {
|
||||||
origin: [f64; 3],
|
origin: [f64; 3],
|
||||||
direction: [f64; 3],
|
direction: [f64; 3],
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Curve2Spec {
|
||||||
|
/// The target layer name, if any.
|
||||||
|
pub fn layer(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
Curve2Spec::Line { layer, .. }
|
||||||
|
| Curve2Spec::Circle { layer, .. }
|
||||||
|
| Curve2Spec::Polyline { layer, .. }
|
||||||
|
| Curve2Spec::Point { layer, .. }
|
||||||
|
| Curve2Spec::Arc { layer, .. }
|
||||||
|
| Curve2Spec::Ellipse { layer, .. }
|
||||||
|
| Curve2Spec::Spline { layer, .. }
|
||||||
|
| Curve2Spec::Ray { layer, .. }
|
||||||
|
| Curve2Spec::XLine { layer, .. } => layer.as_deref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A generic entity-construction payload used by the bulk op
|
/// A generic entity-construction payload used by the bulk op
|
||||||
/// [`crate::ops::Operation::CreateMany`]. Mirrors the per-family construction
|
/// [`crate::ops::Operation::CreateMany`]. Mirrors the per-family construction
|
||||||
/// specs so one bulk op can carry many homogeneous (or mixed) creations.
|
/// specs so one bulk op can carry many homogeneous (or mixed) creations.
|
||||||
@@ -111,6 +286,16 @@ pub enum EntitySpec {
|
|||||||
Solid(SolidPrimitive),
|
Solid(SolidPrimitive),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EntitySpec {
|
||||||
|
/// The target layer name for this spec, if explicitly set.
|
||||||
|
pub fn layer(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
EntitySpec::Curve(c) => c.layer(),
|
||||||
|
EntitySpec::Solid(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Construction spec for a block reference (`INSERT`): place the block
|
/// Construction spec for a block reference (`INSERT`): place the block
|
||||||
/// `block_name` (must exist as a BlockRecord) at `insert_point` with uniform
|
/// `block_name` (must exist as a BlockRecord) at `insert_point` with uniform
|
||||||
/// `scale` and `rotation` (radians, about Z).
|
/// `scale` and `rotation` (radians, about Z).
|
||||||
@@ -291,6 +476,14 @@ impl crate::gen::Operation {
|
|||||||
CreateAttributeDefinition(_) => "CreateAttributeDefinition",
|
CreateAttributeDefinition(_) => "CreateAttributeDefinition",
|
||||||
CreateTable(_) => "CreateTable",
|
CreateTable(_) => "CreateTable",
|
||||||
CreateDimensionAngular2Ln(_) => "CreateDimensionAngular2Ln",
|
CreateDimensionAngular2Ln(_) => "CreateDimensionAngular2Ln",
|
||||||
|
SetXData { .. } => "SetXData",
|
||||||
|
SetXDataMany(_) => "SetXDataMany",
|
||||||
|
CreateXRecord(_) => "CreateXRecord",
|
||||||
|
SetXRecord { .. } => "SetXRecord",
|
||||||
|
CreateLayer(_) => "CreateLayer",
|
||||||
|
UpdateLayer { .. } => "UpdateLayer",
|
||||||
|
DeleteLayer { .. } => "DeleteLayer",
|
||||||
|
SetEntityLayer { .. } => "SetEntityLayer",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,8 @@ pub struct EntityView {
|
|||||||
/// The acadrust `EntityType` variant name (e.g. "Solid3D", "Line").
|
/// The acadrust `EntityType` variant name (e.g. "Solid3D", "Line").
|
||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub bounds: Option<Aabb>,
|
pub bounds: Option<Aabb>,
|
||||||
|
/// The normalized (uppercase) layer name the entity is on.
|
||||||
|
pub layer: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The result of a single [`Query`] (one per query in a `Queries` batch).
|
/// The result of a single [`Query`] (one per query in a `Queries` batch).
|
||||||
@@ -68,6 +70,20 @@ pub enum QueryResult {
|
|||||||
target: [f64; 3],
|
target: [f64; 3],
|
||||||
height: f64,
|
height: f64,
|
||||||
},
|
},
|
||||||
|
/// An XDATA record for a single application (`None` if no such record).
|
||||||
|
XData(Option<crate::ops::XDataRecord>),
|
||||||
|
/// An XRECORD object payload.
|
||||||
|
XRecord(crate::ops::XRecordSpec),
|
||||||
|
/// All layers in the document.
|
||||||
|
Layers(Vec<crate::ops::LayerInfo>),
|
||||||
|
/// The layer name of an entity.
|
||||||
|
EntityLayer(String),
|
||||||
|
/// A filtered list of entity views.
|
||||||
|
Entities(Vec<EntityView>),
|
||||||
|
/// The WCS location of a Point entity.
|
||||||
|
PointPosition([f64; 3]),
|
||||||
|
/// The start and end WCS points of a Line entity.
|
||||||
|
LineGeometry(([f64; 3], [f64; 3])),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience: the query name for diagnostics.
|
/// Convenience: the query name for diagnostics.
|
||||||
@@ -87,6 +103,13 @@ impl crate::gen::Query {
|
|||||||
GetAttributes { .. } => "GetAttributes",
|
GetAttributes { .. } => "GetAttributes",
|
||||||
GetBlockEntities { .. } => "GetBlockEntities",
|
GetBlockEntities { .. } => "GetBlockEntities",
|
||||||
GetViewportView { .. } => "GetViewportView",
|
GetViewportView { .. } => "GetViewportView",
|
||||||
|
GetXData { .. } => "GetXData",
|
||||||
|
GetXRecord { .. } => "GetXRecord",
|
||||||
|
ListLayers => "ListLayers",
|
||||||
|
GetEntityLayer { .. } => "GetEntityLayer",
|
||||||
|
EnumerateEntities { .. } => "EnumerateEntities",
|
||||||
|
GetPointPosition { .. } => "GetPointPosition",
|
||||||
|
GetLineGeometry { .. } => "GetLineGeometry",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,11 +5,12 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ocs_plugin_api::host::{PluginRequestError, PluginRequestSender};
|
use ocs_plugin_api::host::{HostApi, PluginRequestError, PluginRequestSender};
|
||||||
use ocs_plugin_api::ipc::protocol::{PluginRequest, PluginResponse};
|
use ocs_plugin_api::ipc::protocol::{PluginRequest, PluginResponse};
|
||||||
|
|
||||||
use crate::envelope::{DocApiEnvelope, Receipt};
|
use crate::envelope::{DocApiEnvelope, Receipt};
|
||||||
use crate::error::{ApiError, ApiResult};
|
use crate::error::{ApiError, ApiResult};
|
||||||
|
use crate::facade::DocApi;
|
||||||
use crate::transport::Transport;
|
use crate::transport::Transport;
|
||||||
|
|
||||||
/// Out-of-process IPC transport over the `ocs_plugin_api` channel. `Send + Sync`;
|
/// Out-of-process IPC transport over the `ocs_plugin_api` channel. `Send + Sync`;
|
||||||
@@ -29,6 +30,21 @@ impl OcsPluginApiIpc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a typed [`DocApi`] from any host that can provide a
|
||||||
|
/// [`PluginRequestSender`] (out-of-process / V4 worker plugins).
|
||||||
|
///
|
||||||
|
/// Returns `None` for hosts that do not expose a request sender (e.g. in-process
|
||||||
|
/// hosts). In-process code should use [`crate::DocApi::in_process`] with a
|
||||||
|
/// concrete [`crate::backend::DocApiBackend`].
|
||||||
|
pub fn doc_api_for_host(host: &dyn HostApi) -> Option<DocApi> {
|
||||||
|
let sender = host.plugin_request_sender()?;
|
||||||
|
let tab_id = host.tab_id();
|
||||||
|
Some(DocApi::connect(
|
||||||
|
Arc::new(OcsPluginApiIpc::new(sender.into(), tab_id)),
|
||||||
|
tab_id,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
impl Transport for OcsPluginApiIpc {
|
impl Transport for OcsPluginApiIpc {
|
||||||
fn apply(&self, req: DocApiEnvelope) -> ApiResult<Receipt> {
|
fn apply(&self, req: DocApiEnvelope) -> ApiResult<Receipt> {
|
||||||
let bytes = bincode::serialize(&req)
|
let bytes = bincode::serialize(&req)
|
||||||
@@ -56,3 +72,128 @@ impl Transport for OcsPluginApiIpc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::facade::HasId;
|
||||||
|
use crate::ObjectId;
|
||||||
|
use ocs_plugin_api::host::{
|
||||||
|
CadDocument, EntityType, ExtendedDataRecord, Handle, HostApi,
|
||||||
|
};
|
||||||
|
use ocs_plugin_api::ipc::protocol::{PluginRequest, PluginResponse};
|
||||||
|
use std::any::Any;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct MockSender {
|
||||||
|
captured: Arc<Mutex<Vec<(u64, PluginRequest)>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginRequestSender for MockSender {
|
||||||
|
fn request(&self, req: PluginRequest) -> Result<PluginResponse, PluginRequestError> {
|
||||||
|
let tab_id = match &req {
|
||||||
|
PluginRequest::DocApiRequest { tab_id, .. } => *tab_id,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
self.captured.lock().unwrap().push((tab_id, req));
|
||||||
|
Ok(PluginResponse::DocApiResponse {
|
||||||
|
bytes: bincode::serialize(&crate::ApiResult::Ok(Receipt {
|
||||||
|
outcome: Some(crate::envelope::OpOutcome::NewId(ObjectId::from_u64(7))),
|
||||||
|
query_results: vec![],
|
||||||
|
new_revision: crate::revision::GeometryRevision(1),
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MockHost {
|
||||||
|
sender: Option<MockSender>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostApi for MockHost {
|
||||||
|
fn tab_index(&self) -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
fn document(&self) -> &CadDocument {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn document_mut(&mut self) -> &mut CadDocument {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn add_entity(&mut self, _entity: EntityType) -> Handle {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn bump_geometry(&mut self) {}
|
||||||
|
fn push_undo(&mut self, _label: &str) {}
|
||||||
|
fn set_dirty(&mut self) {}
|
||||||
|
fn push_info(&mut self, _msg: &str) {}
|
||||||
|
fn push_output(&mut self, _msg: &str) {}
|
||||||
|
fn push_error(&mut self, _msg: &str) {}
|
||||||
|
fn start_interactive(&mut self, _command: Box<dyn ocs_plugin_api::host::InteractiveCommand>) {}
|
||||||
|
fn read_record(&self, _handle: Handle, _app_name: &str) -> Option<&ExtendedDataRecord> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn write_record(&mut self, _handle: Handle, _record: ExtendedDataRecord) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn remove_record(&mut self, _handle: Handle, _app_name: &str) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn plugin_state_any(&self, _plugin_id: &str) -> Option<&(dyn Any + Send + Sync)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn plugin_state_any_mut(&mut self, _plugin_id: &str) -> Option<&mut (dyn Any + Send + Sync)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn ensure_plugin_state_any(
|
||||||
|
&mut self,
|
||||||
|
_plugin_id: &'static str,
|
||||||
|
_init: &mut dyn FnMut() -> Box<dyn Any + Send + Sync>,
|
||||||
|
) -> &mut (dyn Any + Send + Sync) {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn document_reader(&self) -> Box<dyn ocs_plugin_api::host::DocumentReader + '_> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn plugin_request_sender(&self) -> Option<Box<dyn PluginRequestSender>> {
|
||||||
|
self.sender.as_ref().map(|s| Box::new(s.clone()) as Box<dyn PluginRequestSender>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doc_api_for_host_builds_doc_api_and_routes_doc_api_request() {
|
||||||
|
let captured = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let host = MockHost {
|
||||||
|
sender: Some(MockSender {
|
||||||
|
captured: Arc::clone(&captured),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let doc_api = doc_api_for_host(&host).expect("host exposes a sender");
|
||||||
|
let doc = doc_api.document(host.tab_id());
|
||||||
|
let id = doc
|
||||||
|
.solids()
|
||||||
|
.create_cuboid([0.0; 3], [1.0; 3])
|
||||||
|
.expect("mock returns a NewId receipt");
|
||||||
|
assert_eq!(id.id(), ObjectId::from_u64(7));
|
||||||
|
|
||||||
|
let calls = captured.lock().unwrap();
|
||||||
|
assert_eq!(calls.len(), 1);
|
||||||
|
let (tab_id, ref req) = &calls[0];
|
||||||
|
assert_eq!(*tab_id, host.tab_id());
|
||||||
|
match req {
|
||||||
|
PluginRequest::DocApiRequest { tab_id, bytes } => {
|
||||||
|
assert_eq!(*tab_id, host.tab_id());
|
||||||
|
assert!(!bytes.is_empty());
|
||||||
|
}
|
||||||
|
other => panic!("expected DocApiRequest, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doc_api_for_host_returns_none_without_sender() {
|
||||||
|
let host = MockHost { sender: None };
|
||||||
|
assert!(doc_api_for_host(&host).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//! Reject invalid numeric inputs before the backend starts an undo operation.
|
//! Reject invalid numeric inputs before the backend starts an undo operation.
|
||||||
use crate::{ops::*, ApiError, ApiResult};
|
use crate::{id::ObjectId, ops::*, ApiError, ApiResult};
|
||||||
|
|
||||||
fn finite(values: &[f64]) -> bool {
|
fn finite(values: &[f64]) -> bool {
|
||||||
values.iter().all(|v| v.is_finite())
|
values.iter().all(|v| v.is_finite())
|
||||||
@@ -21,19 +21,102 @@ fn check(valid: bool, op: &'static str) -> ApiResult<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_payload(valid: bool, op: &'static str, reason: &'static str) -> ApiResult<()> {
|
||||||
|
if valid {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ApiError::validation(op, reason))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_application_name(name: &str) -> bool {
|
||||||
|
!name.trim().is_empty() && !name.contains('\0')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_xdata_record(app: &str, record: Option<&XDataRecord>) -> bool {
|
||||||
|
valid_application_name(app)
|
||||||
|
&& record.is_none_or(|record| {
|
||||||
|
record.application_name == app
|
||||||
|
&& record.values.iter().all(|value| match value {
|
||||||
|
XDataValue::String(value)
|
||||||
|
| XDataValue::ControlString(value)
|
||||||
|
| XDataValue::LayerName(value) => !value.contains('\0'),
|
||||||
|
XDataValue::Point3D(value)
|
||||||
|
| XDataValue::Position3D(value)
|
||||||
|
| XDataValue::Displacement3D(value)
|
||||||
|
| XDataValue::Direction3D(value) => finite(value),
|
||||||
|
XDataValue::Real(value)
|
||||||
|
| XDataValue::Distance(value)
|
||||||
|
| XDataValue::ScaleFactor(value) => value.is_finite(),
|
||||||
|
XDataValue::BinaryData(_)
|
||||||
|
| XDataValue::Handle(_)
|
||||||
|
| XDataValue::Integer16(_)
|
||||||
|
| XDataValue::Integer32(_) => true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_xrecord(spec: &XRecordSpec) -> bool {
|
||||||
|
!spec.name.trim().is_empty()
|
||||||
|
&& !spec.name.contains('\0')
|
||||||
|
&& spec.entries.len() <= BULK_ITEM_CAP
|
||||||
|
&& spec.entries.iter().all(|entry| {
|
||||||
|
let code = entry.code;
|
||||||
|
match &entry.value {
|
||||||
|
XRecordValue::String(value) => {
|
||||||
|
matches!(
|
||||||
|
code,
|
||||||
|
0..=4
|
||||||
|
| 6..=9
|
||||||
|
| 100..=102
|
||||||
|
| 300..=309
|
||||||
|
| 410..=419
|
||||||
|
| 430..=439
|
||||||
|
| 470..=479
|
||||||
|
| 999
|
||||||
|
| 1000..=1003
|
||||||
|
) && !value.contains('\0')
|
||||||
|
&& value.encode_utf16().count() <= u16::MAX as usize
|
||||||
|
}
|
||||||
|
XRecordValue::Point3D(value) => {
|
||||||
|
matches!(code, 10..=37 | 110..=139 | 210..=269 | 1010..=1039 | 1043..=1069)
|
||||||
|
&& finite(value)
|
||||||
|
}
|
||||||
|
XRecordValue::Double(value) => {
|
||||||
|
matches!(code, 38..=59 | 140..=149 | 460..=469 | 1040..=1042)
|
||||||
|
&& value.is_finite()
|
||||||
|
}
|
||||||
|
XRecordValue::Int16(_) => {
|
||||||
|
matches!(code, 60..=79 | 170..=179 | 270..=279 | 370..=389 | 400..=409 | 1070)
|
||||||
|
}
|
||||||
|
XRecordValue::Int32(_) => matches!(code, 80..=99 | 420..=429 | 440..=459 | 1071),
|
||||||
|
XRecordValue::Int64(_) => matches!(code, 150..=169),
|
||||||
|
XRecordValue::Byte(_) => matches!(code, 280..=289),
|
||||||
|
XRecordValue::Bool(_) => matches!(code, 290..=299),
|
||||||
|
XRecordValue::Handle(_) => {
|
||||||
|
code < 0 || matches!(code, 5 | 105 | 320..=369 | 390..=399 | 480..=481 | 1005)
|
||||||
|
}
|
||||||
|
XRecordValue::Chunk(value) => {
|
||||||
|
matches!(code, 310..=319 | 1004) && value.len() <= u8::MAX as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn curve(spec: &Curve2Spec) -> ApiResult<()> {
|
pub(crate) fn curve(spec: &Curve2Spec) -> ApiResult<()> {
|
||||||
use Curve2Spec::*;
|
use Curve2Spec::*;
|
||||||
let valid = match spec {
|
let valid = match spec {
|
||||||
Line { start, end } => finite(start) && finite(end) && start != end,
|
Line { start, end, .. } => finite(start) && finite(end) && start != end,
|
||||||
Circle { centre, radius } => finite(centre) && positive(*radius),
|
Circle { centre, radius, .. } => finite(centre) && positive(*radius),
|
||||||
Point { position } => finite(position),
|
Point { position, .. } => finite(position),
|
||||||
Arc {
|
Arc {
|
||||||
centre,
|
centre,
|
||||||
radius,
|
radius,
|
||||||
start_angle,
|
start_angle,
|
||||||
end_angle,
|
end_angle,
|
||||||
|
..
|
||||||
} => finite(centre) && positive(*radius) && finite(&[*start_angle, *end_angle]),
|
} => finite(centre) && positive(*radius) && finite(&[*start_angle, *end_angle]),
|
||||||
Polyline { points, closed } => {
|
Polyline { points, closed, .. } => {
|
||||||
points.len() >= if *closed { 3 } else { 2 }
|
points.len() >= if *closed { 3 } else { 2 }
|
||||||
&& points.len() <= BULK_ITEM_CAP
|
&& points.len() <= BULK_ITEM_CAP
|
||||||
&& points.iter().all(|p| finite(p) && p[2] == points[0][2])
|
&& points.iter().all(|p| finite(p) && p[2] == points[0][2])
|
||||||
@@ -44,6 +127,7 @@ pub(crate) fn curve(spec: &Curve2Spec) -> ApiResult<()> {
|
|||||||
ratio,
|
ratio,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
finite(centre)
|
finite(centre)
|
||||||
&& direction(major_axis)
|
&& direction(major_axis)
|
||||||
@@ -56,6 +140,7 @@ pub(crate) fn curve(spec: &Curve2Spec) -> ApiResult<()> {
|
|||||||
control_points,
|
control_points,
|
||||||
knots,
|
knots,
|
||||||
weights,
|
weights,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
*degree > 0
|
*degree > 0
|
||||||
&& control_points.len() <= BULK_ITEM_CAP
|
&& control_points.len() <= BULK_ITEM_CAP
|
||||||
@@ -76,10 +161,12 @@ pub(crate) fn curve(spec: &Curve2Spec) -> ApiResult<()> {
|
|||||||
Ray {
|
Ray {
|
||||||
origin,
|
origin,
|
||||||
direction: d,
|
direction: d,
|
||||||
|
..
|
||||||
}
|
}
|
||||||
| XLine {
|
| XLine {
|
||||||
origin,
|
origin,
|
||||||
direction: d,
|
direction: d,
|
||||||
|
..
|
||||||
} => finite(origin) && direction(d),
|
} => finite(origin) && direction(d),
|
||||||
};
|
};
|
||||||
check(valid, "CreateCurve")
|
check(valid, "CreateCurve")
|
||||||
@@ -111,6 +198,36 @@ pub(crate) fn operation(op: &Operation) -> ApiResult<()> {
|
|||||||
}
|
}
|
||||||
CreateText(s) => finite(&s.insertion_point) && positive(s.height) && s.rotation.is_finite(),
|
CreateText(s) => finite(&s.insertion_point) && positive(s.height) && s.rotation.is_finite(),
|
||||||
CreateMText(s) => finite(&s.insertion_point) && positive(s.height),
|
CreateMText(s) => finite(&s.insertion_point) && positive(s.height),
|
||||||
|
SetXData {
|
||||||
|
id,
|
||||||
|
application_name,
|
||||||
|
record,
|
||||||
|
} => {
|
||||||
|
return check_payload(
|
||||||
|
*id != ObjectId::NULL && valid_xdata_record(application_name, record.as_ref()),
|
||||||
|
"SetXData",
|
||||||
|
"invalid XDATA record",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SetXDataMany(records) => {
|
||||||
|
return check_payload(
|
||||||
|
records.iter().all(|(id, app, record)| {
|
||||||
|
*id != ObjectId::NULL && valid_xdata_record(app, record.as_ref())
|
||||||
|
}),
|
||||||
|
"SetXDataMany",
|
||||||
|
"invalid XDATA record",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CreateXRecord(spec) => {
|
||||||
|
return check_payload(valid_xrecord(spec), "CreateXRecord", "invalid XRecord payload")
|
||||||
|
}
|
||||||
|
SetXRecord { id, spec } => {
|
||||||
|
return check_payload(
|
||||||
|
*id != ObjectId::NULL && valid_xrecord(spec),
|
||||||
|
"SetXRecord",
|
||||||
|
"invalid XRecord payload",
|
||||||
|
)
|
||||||
|
}
|
||||||
CreateHatch(s) => {
|
CreateHatch(s) => {
|
||||||
(3..=BULK_ITEM_CAP).contains(&s.boundary.len()) && s.boundary.iter().all(|p| finite(p))
|
(3..=BULK_ITEM_CAP).contains(&s.boundary.len()) && s.boundary.iter().all(|p| finite(p))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,16 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use ocs_doc_api::backend::{DocApiBackend, KernelBody};
|
use ocs_doc_api::backend::{DocApiBackend, KernelBody};
|
||||||
use ocs_doc_api::{
|
use ocs_doc_api::{
|
||||||
Aabb, ApiError, ApiResult, Curve2Spec, DocApi, EntityView, GeometryErrorKind, GeometryRevision,
|
normalize_layer_name, Aabb, ApiError, ApiResult, Color, Curve2Spec, DocApi, EntityView,
|
||||||
HasId, ObjectId, PlacementSpec,
|
GeometryErrorKind, GeometryRevision, HasId, LayerInfo, LineWeight, ObjectId, PlacementSpec,
|
||||||
|
XDataRecord, XDataValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Coarse bounds for a stored 2D curve spec (mirrors the host's entity_bounds and
|
/// Coarse bounds for a stored 2D curve spec (mirrors the host's entity_bounds and
|
||||||
/// the example mock — all three must answer curve bounds identically).
|
/// the example mock — all three must answer curve bounds identically).
|
||||||
fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
||||||
match spec {
|
match spec {
|
||||||
Curve2Spec::Line { start, end } => Aabb {
|
Curve2Spec::Line { start, end, .. } => Aabb {
|
||||||
min: [
|
min: [
|
||||||
start[0].min(end[0]),
|
start[0].min(end[0]),
|
||||||
start[1].min(end[1]),
|
start[1].min(end[1]),
|
||||||
@@ -28,11 +29,12 @@ fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
|||||||
start[2].max(end[2]),
|
start[2].max(end[2]),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
Curve2Spec::Circle { centre, radius } | Curve2Spec::Arc { centre, radius, .. } => Aabb {
|
Curve2Spec::Circle { centre, radius, .. }
|
||||||
|
| Curve2Spec::Arc { centre, radius, .. } => Aabb {
|
||||||
min: [centre[0] - radius, centre[1] - radius, centre[2]],
|
min: [centre[0] - radius, centre[1] - radius, centre[2]],
|
||||||
max: [centre[0] + radius, centre[1] + radius, centre[2]],
|
max: [centre[0] + radius, centre[1] + radius, centre[2]],
|
||||||
},
|
},
|
||||||
Curve2Spec::Point { position } => Aabb {
|
Curve2Spec::Point { position, .. } => Aabb {
|
||||||
min: *position,
|
min: *position,
|
||||||
max: *position,
|
max: *position,
|
||||||
},
|
},
|
||||||
@@ -72,13 +74,17 @@ fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
|||||||
|
|
||||||
/// In-memory mock backend: HashMap<ObjectId, Body> + a tiny entity table.
|
/// In-memory mock backend: HashMap<ObjectId, Body> + a tiny entity table.
|
||||||
/// Tracks undo steps + revision bumps to assert per-op semantics.
|
/// Tracks undo steps + revision bumps to assert per-op semantics.
|
||||||
#[derive(Default, Clone)]
|
#[derive(Clone)]
|
||||||
struct MockBackend {
|
struct MockBackend {
|
||||||
next_id: u64,
|
next_id: u64,
|
||||||
revision: u64,
|
revision: u64,
|
||||||
undo_steps: u32,
|
undo_steps: u32,
|
||||||
bodies: HashMap<ObjectId, KernelBody>,
|
bodies: HashMap<ObjectId, KernelBody>,
|
||||||
kinds: HashMap<ObjectId, String>,
|
kinds: HashMap<ObjectId, String>,
|
||||||
|
/// Layer name assigned to each entity.
|
||||||
|
entity_layers: HashMap<ObjectId, String>,
|
||||||
|
/// Stored layer table.
|
||||||
|
layers: HashMap<String, ocs_doc_api::LayerInfo>,
|
||||||
/// Stored text content for Text/MText annotations.
|
/// Stored text content for Text/MText annotations.
|
||||||
text_values: HashMap<ObjectId, String>,
|
text_values: HashMap<ObjectId, String>,
|
||||||
/// Stored hatch boundary loops.
|
/// Stored hatch boundary loops.
|
||||||
@@ -92,6 +98,36 @@ struct MockBackend {
|
|||||||
curves: HashMap<ObjectId, Curve2Spec>,
|
curves: HashMap<ObjectId, Curve2Spec>,
|
||||||
/// Stored viewport views (target, height).
|
/// Stored viewport views (target, height).
|
||||||
viewport_views: HashMap<ObjectId, ([f64; 3], f64)>,
|
viewport_views: HashMap<ObjectId, ([f64; 3], f64)>,
|
||||||
|
/// Stored XDATA records keyed by (entity id, application name).
|
||||||
|
xdata_store: HashMap<(ObjectId, String), ocs_doc_api::XDataRecord>,
|
||||||
|
/// Stored XRECORD payloads keyed by entity id.
|
||||||
|
xrecord_store: HashMap<ObjectId, ocs_doc_api::XRecordSpec>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MockBackend {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut backend = Self {
|
||||||
|
next_id: 0,
|
||||||
|
revision: 0,
|
||||||
|
undo_steps: 0,
|
||||||
|
bodies: HashMap::new(),
|
||||||
|
kinds: HashMap::new(),
|
||||||
|
entity_layers: HashMap::new(),
|
||||||
|
layers: HashMap::new(),
|
||||||
|
text_values: HashMap::new(),
|
||||||
|
hatch_boundaries: HashMap::new(),
|
||||||
|
dimension_measurements: HashMap::new(),
|
||||||
|
attributes_store: HashMap::new(),
|
||||||
|
curves: HashMap::new(),
|
||||||
|
viewport_views: HashMap::new(),
|
||||||
|
xdata_store: HashMap::new(),
|
||||||
|
xrecord_store: HashMap::new(),
|
||||||
|
};
|
||||||
|
backend
|
||||||
|
.layers
|
||||||
|
.insert("0".to_string(), ocs_doc_api::LayerInfo::new("0"));
|
||||||
|
backend
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockBackend {
|
impl MockBackend {
|
||||||
@@ -104,6 +140,22 @@ impl MockBackend {
|
|||||||
fn body(&self, id: ObjectId) -> ApiResult<&KernelBody> {
|
fn body(&self, id: ObjectId) -> ApiResult<&KernelBody> {
|
||||||
self.bodies.get(&id).ok_or(ApiError::UnknownId(id))
|
self.bodies.get(&id).ok_or(ApiError::UnknownId(id))
|
||||||
}
|
}
|
||||||
|
fn add_curve_only(&mut self, spec: &Curve2Spec) -> ApiResult<ObjectId> {
|
||||||
|
let kind = match spec {
|
||||||
|
Curve2Spec::Line { .. } => "Line",
|
||||||
|
Curve2Spec::Circle { .. } => "Circle",
|
||||||
|
Curve2Spec::Polyline { .. } => "LwPolyline",
|
||||||
|
Curve2Spec::Point { .. } => "Point",
|
||||||
|
Curve2Spec::Arc { .. } => "Arc",
|
||||||
|
Curve2Spec::Ellipse { .. } => "Ellipse",
|
||||||
|
Curve2Spec::Spline { .. } => "Spline",
|
||||||
|
Curve2Spec::Ray { .. } => "Ray",
|
||||||
|
Curve2Spec::XLine { .. } => "XLine",
|
||||||
|
};
|
||||||
|
let id = self.alloc(kind);
|
||||||
|
self.curves.insert(id, spec.clone());
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DocApiBackend for MockBackend {
|
impl DocApiBackend for MockBackend {
|
||||||
@@ -113,6 +165,7 @@ impl DocApiBackend for MockBackend {
|
|||||||
fn store_solid(&mut self, body: &KernelBody) -> ApiResult<ObjectId> {
|
fn store_solid(&mut self, body: &KernelBody) -> ApiResult<ObjectId> {
|
||||||
let id = self.alloc("Solid3D");
|
let id = self.alloc("Solid3D");
|
||||||
self.bodies.insert(id, body.clone());
|
self.bodies.insert(id, body.clone());
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn update_solid(&mut self, id: ObjectId, body: &KernelBody) -> ApiResult<()> {
|
fn update_solid(&mut self, id: ObjectId, body: &KernelBody) -> ApiResult<()> {
|
||||||
@@ -122,6 +175,15 @@ impl DocApiBackend for MockBackend {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn create_many(&mut self, specs: &[ocs_doc_api::EntitySpec]) -> ApiResult<Vec<ObjectId>> {
|
fn create_many(&mut self, specs: &[ocs_doc_api::EntitySpec]) -> ApiResult<Vec<ObjectId>> {
|
||||||
|
// Validate per-spec layers up front so the op is atomic.
|
||||||
|
for spec in specs {
|
||||||
|
if let Some(name) = spec.layer() {
|
||||||
|
self.lookup_layer(name).map_err(|e| ApiError::validation(
|
||||||
|
"CreateMany",
|
||||||
|
e.to_string(),
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut next = self.clone();
|
let mut next = self.clone();
|
||||||
let ids = specs
|
let ids = specs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -151,26 +213,35 @@ impl DocApiBackend for MockBackend {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn add_curve(&mut self, spec: &Curve2Spec) -> ApiResult<ObjectId> {
|
fn add_curve(&mut self, spec: &Curve2Spec) -> ApiResult<ObjectId> {
|
||||||
let kind = match spec {
|
let layer = spec
|
||||||
Curve2Spec::Line { .. } => "Line",
|
.layer()
|
||||||
Curve2Spec::Circle { .. } => "Circle",
|
.map(|name| {
|
||||||
Curve2Spec::Polyline { .. } => "LwPolyline",
|
self.lookup_layer(name).map(|info| info.normalized_name())
|
||||||
Curve2Spec::Point { .. } => "Point",
|
})
|
||||||
Curve2Spec::Arc { .. } => "Arc",
|
.transpose()?;
|
||||||
Curve2Spec::Ellipse { .. } => "Ellipse",
|
let id = self.add_curve_only(spec)?;
|
||||||
Curve2Spec::Spline { .. } => "Spline",
|
self.entity_layers
|
||||||
Curve2Spec::Ray { .. } => "Ray",
|
.insert(id, layer.unwrap_or_else(|| "0".to_string()));
|
||||||
Curve2Spec::XLine { .. } => "XLine",
|
|
||||||
};
|
|
||||||
let id = self.alloc(kind);
|
|
||||||
self.curves.insert(id, spec.clone());
|
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lookup_layer(&self, name: &str) -> ApiResult<ocs_doc_api::LayerInfo> {
|
||||||
|
let norm = normalize_layer_name(name);
|
||||||
|
if norm.is_empty() {
|
||||||
|
return Err(ApiError::validation("LookupLayer", "empty layer name"));
|
||||||
|
}
|
||||||
|
self.layers
|
||||||
|
.get(&norm)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| ApiError::validation("LookupLayer", format!("layer '{name}' does not exist")))
|
||||||
|
}
|
||||||
fn add_insert(&mut self, spec: &ocs_doc_api::ops::InsertSpec) -> ApiResult<ObjectId> {
|
fn add_insert(&mut self, spec: &ocs_doc_api::ops::InsertSpec) -> ApiResult<ObjectId> {
|
||||||
if spec.block_name.is_empty() {
|
if spec.block_name.is_empty() {
|
||||||
return Err(ApiError::validation("CreateInsert", "empty block name"));
|
return Err(ApiError::validation("CreateInsert", "empty block name"));
|
||||||
}
|
}
|
||||||
Ok(self.alloc("Insert"))
|
let id = self.alloc("Insert");
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn add_viewport(&mut self, spec: &ocs_doc_api::ops::ViewportSpec) -> ApiResult<ObjectId> {
|
fn add_viewport(&mut self, spec: &ocs_doc_api::ops::ViewportSpec) -> ApiResult<ObjectId> {
|
||||||
let id = self.alloc("Viewport");
|
let id = self.alloc("Viewport");
|
||||||
@@ -181,11 +252,13 @@ impl DocApiBackend for MockBackend {
|
|||||||
fn add_text(&mut self, spec: &ocs_doc_api::ops::TextSpec) -> ApiResult<ObjectId> {
|
fn add_text(&mut self, spec: &ocs_doc_api::ops::TextSpec) -> ApiResult<ObjectId> {
|
||||||
let id = self.alloc("Text");
|
let id = self.alloc("Text");
|
||||||
self.text_values.insert(id, spec.value.clone());
|
self.text_values.insert(id, spec.value.clone());
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn add_mtext(&mut self, spec: &ocs_doc_api::ops::MTextSpec) -> ApiResult<ObjectId> {
|
fn add_mtext(&mut self, spec: &ocs_doc_api::ops::MTextSpec) -> ApiResult<ObjectId> {
|
||||||
let id = self.alloc("MText");
|
let id = self.alloc("MText");
|
||||||
self.text_values.insert(id, spec.value.clone());
|
self.text_values.insert(id, spec.value.clone());
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn set_text_content(&mut self, id: ObjectId, value: &str) -> ApiResult<()> {
|
fn set_text_content(&mut self, id: ObjectId, value: &str) -> ApiResult<()> {
|
||||||
@@ -206,6 +279,7 @@ impl DocApiBackend for MockBackend {
|
|||||||
let id = self.alloc("Hatch");
|
let id = self.alloc("Hatch");
|
||||||
self.hatch_boundaries
|
self.hatch_boundaries
|
||||||
.insert(id, vec![spec.boundary.clone()]);
|
.insert(id, vec![spec.boundary.clone()]);
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn hatch_boundary(&self, id: ObjectId) -> ApiResult<Vec<Vec<[f64; 2]>>> {
|
fn hatch_boundary(&self, id: ObjectId) -> ApiResult<Vec<Vec<[f64; 2]>>> {
|
||||||
@@ -224,6 +298,7 @@ impl DocApiBackend for MockBackend {
|
|||||||
+ (spec.first_point[2] - spec.second_point[2]).powi(2))
|
+ (spec.first_point[2] - spec.second_point[2]).powi(2))
|
||||||
.sqrt();
|
.sqrt();
|
||||||
self.dimension_measurements.insert(id, d);
|
self.dimension_measurements.insert(id, d);
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn add_dimension_radius(
|
fn add_dimension_radius(
|
||||||
@@ -236,13 +311,16 @@ impl DocApiBackend for MockBackend {
|
|||||||
+ (spec.center[2] - spec.point[2]).powi(2))
|
+ (spec.center[2] - spec.point[2]).powi(2))
|
||||||
.sqrt();
|
.sqrt();
|
||||||
self.dimension_measurements.insert(id, d);
|
self.dimension_measurements.insert(id, d);
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn add_dimension_diameter(
|
fn add_dimension_diameter(
|
||||||
&mut self,
|
&mut self,
|
||||||
spec: &ocs_doc_api::ops::DimensionRadialSpec,
|
spec: &ocs_doc_api::ops::DimensionRadialSpec,
|
||||||
) -> ApiResult<ObjectId> {
|
) -> ApiResult<ObjectId> {
|
||||||
self.add_dimension_radius(spec)
|
let id = self.add_dimension_radius(spec)?;
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn add_dimension_angular(
|
fn add_dimension_angular(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -250,6 +328,7 @@ impl DocApiBackend for MockBackend {
|
|||||||
) -> ApiResult<ObjectId> {
|
) -> ApiResult<ObjectId> {
|
||||||
let id = self.alloc("Dimension");
|
let id = self.alloc("Dimension");
|
||||||
self.dimension_measurements.insert(id, 90.0); // mock: 90 degrees
|
self.dimension_measurements.insert(id, 90.0); // mock: 90 degrees
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64> {
|
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64> {
|
||||||
@@ -286,7 +365,9 @@ impl DocApiBackend for MockBackend {
|
|||||||
"table needs a non-empty rectangular grid",
|
"table needs a non-empty rectangular grid",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(self.alloc("Table"))
|
let id = self.alloc("Table");
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()> {
|
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()> {
|
||||||
if !self.entity_exists(id) {
|
if !self.entity_exists(id) {
|
||||||
@@ -313,7 +394,9 @@ impl DocApiBackend for MockBackend {
|
|||||||
&mut self,
|
&mut self,
|
||||||
_spec: &ocs_doc_api::ops::RasterImageSpec,
|
_spec: &ocs_doc_api::ops::RasterImageSpec,
|
||||||
) -> ApiResult<ObjectId> {
|
) -> ApiResult<ObjectId> {
|
||||||
Ok(self.alloc("RasterImage"))
|
let id = self.alloc("RasterImage");
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn loft(
|
fn loft(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -345,6 +428,203 @@ impl DocApiBackend for MockBackend {
|
|||||||
.copied()
|
.copied()
|
||||||
.ok_or(ApiError::UnknownId(id))
|
.ok_or(ApiError::UnknownId(id))
|
||||||
}
|
}
|
||||||
|
fn set_xdata(
|
||||||
|
&mut self,
|
||||||
|
id: ObjectId,
|
||||||
|
application_name: &str,
|
||||||
|
record: Option<&ocs_doc_api::XDataRecord>,
|
||||||
|
) -> ApiResult<()> {
|
||||||
|
if !self.entity_exists(id) {
|
||||||
|
return Err(ApiError::UnknownId(id));
|
||||||
|
}
|
||||||
|
let key = (id, application_name.to_string());
|
||||||
|
match record {
|
||||||
|
Some(r) => {
|
||||||
|
self.xdata_store.insert(key, r.clone());
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.xdata_store.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn xdata(
|
||||||
|
&self,
|
||||||
|
id: ObjectId,
|
||||||
|
application_name: &str,
|
||||||
|
) -> ApiResult<Option<ocs_doc_api::XDataRecord>> {
|
||||||
|
if !self.entity_exists(id) {
|
||||||
|
return Err(ApiError::UnknownId(id));
|
||||||
|
}
|
||||||
|
Ok(self
|
||||||
|
.xdata_store
|
||||||
|
.get(&(id, application_name.to_string()))
|
||||||
|
.cloned())
|
||||||
|
}
|
||||||
|
fn add_xrecord(&mut self, spec: &ocs_doc_api::XRecordSpec) -> ApiResult<ObjectId> {
|
||||||
|
let id = self.alloc("XRecord");
|
||||||
|
self.xrecord_store.insert(id, spec.clone());
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
fn set_xrecord(&mut self, id: ObjectId, spec: &ocs_doc_api::XRecordSpec) -> ApiResult<()> {
|
||||||
|
if !self.object_exists(id) {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"SetXRecord",
|
||||||
|
format!("unknown ObjectId {id:?}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.xrecord_store.insert(id, spec.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn xrecord(&self, id: ObjectId) -> ApiResult<Option<ocs_doc_api::XRecordSpec>> {
|
||||||
|
if !self.object_exists(id) {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"GetXRecord",
|
||||||
|
format!("unknown ObjectId {id:?}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(self.xrecord_store.get(&id).cloned())
|
||||||
|
}
|
||||||
|
fn create_layer(&mut self, info: &ocs_doc_api::LayerInfo) -> ApiResult<()> {
|
||||||
|
let name = normalize_layer_name(&info.name);
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(ApiError::validation("CreateLayer", "empty layer name"));
|
||||||
|
}
|
||||||
|
if self.layers.contains_key(&name) {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"CreateLayer",
|
||||||
|
format!("layer '{name}' already exists"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut stored = info.clone();
|
||||||
|
stored.name = name.clone();
|
||||||
|
self.layers.insert(name, stored);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn update_layer(&mut self, name: &str, info: &ocs_doc_api::LayerInfo) -> ApiResult<()> {
|
||||||
|
let name_upper = normalize_layer_name(name);
|
||||||
|
if !self.layers.contains_key(&name_upper) {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"UpdateLayer",
|
||||||
|
format!("layer '{name}' does not exist"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut stored = info.clone();
|
||||||
|
stored.name = name_upper.clone();
|
||||||
|
self.layers.insert(name_upper, stored);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn delete_layer(&mut self, name: &str) -> ApiResult<()> {
|
||||||
|
let name_upper = normalize_layer_name(name);
|
||||||
|
if name_upper == "0" {
|
||||||
|
return Err(ApiError::validation("DeleteLayer", "cannot delete layer 0"));
|
||||||
|
}
|
||||||
|
if self
|
||||||
|
.entity_layers
|
||||||
|
.values()
|
||||||
|
.any(|l| normalize_layer_name(l) == name_upper)
|
||||||
|
{
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"DeleteLayer",
|
||||||
|
format!("layer '{name_upper}' still in use"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.layers.remove(&name_upper).is_none() {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
"DeleteLayer",
|
||||||
|
format!("layer '{name}' does not exist"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn set_entity_layer(&mut self, id: ObjectId, layer: &str) -> ApiResult<()> {
|
||||||
|
if !self.entity_exists(id) {
|
||||||
|
return Err(ApiError::UnknownId(id));
|
||||||
|
}
|
||||||
|
let layer_upper = normalize_layer_name(layer);
|
||||||
|
let target = self
|
||||||
|
.layers
|
||||||
|
.get(&layer_upper)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ApiError::validation(
|
||||||
|
"SetEntityLayer",
|
||||||
|
format!("layer '{layer_upper}' does not exist"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if target.flags.locked {
|
||||||
|
return Err(ApiError::Unsupported(format!(
|
||||||
|
"entity {id:?} cannot be moved to locked layer '{layer_upper}'"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
self.entity_layers.insert(id, layer_upper);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn layers(&self) -> ApiResult<Vec<ocs_doc_api::LayerInfo>> {
|
||||||
|
let mut v: Vec<_> = self.layers.values().cloned().collect();
|
||||||
|
v.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
fn entity_layer(&self, id: ObjectId) -> ApiResult<String> {
|
||||||
|
self.entity_layers
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or(ApiError::UnknownId(id))
|
||||||
|
}
|
||||||
|
fn enumerate_entities(
|
||||||
|
&self,
|
||||||
|
kind: Option<&str>,
|
||||||
|
layer: Option<&str>,
|
||||||
|
include_bounds: bool,
|
||||||
|
) -> ApiResult<Vec<EntityView>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for (id, kind_name) in &self.kinds {
|
||||||
|
if let Some(k) = kind {
|
||||||
|
if !k.eq_ignore_ascii_case(kind_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let entity_layer = self
|
||||||
|
.entity_layers
|
||||||
|
.get(id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "0".to_string());
|
||||||
|
if let Some(l) = layer {
|
||||||
|
if !l.eq_ignore_ascii_case(&entity_layer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bounds = if include_bounds {
|
||||||
|
self.curves.get(id).map(curve_bounds)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
out.push(EntityView {
|
||||||
|
id: *id,
|
||||||
|
kind: kind_name.clone(),
|
||||||
|
layer: entity_layer,
|
||||||
|
bounds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
fn point_position(&self, id: ObjectId) -> ApiResult<[f64; 3]> {
|
||||||
|
let spec = self.curves.get(&id).ok_or(ApiError::UnknownId(id))?;
|
||||||
|
match spec {
|
||||||
|
Curve2Spec::Point { position, .. } => Ok(*position),
|
||||||
|
_ => Err(ApiError::Unsupported(
|
||||||
|
"GetPointPosition is only for Point entities".into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn line_geometry(&self, id: ObjectId) -> ApiResult<([f64; 3], [f64; 3])> {
|
||||||
|
let spec = self.curves.get(&id).ok_or(ApiError::UnknownId(id))?;
|
||||||
|
match spec {
|
||||||
|
Curve2Spec::Line { start, end, .. } => Ok((*start, *end)),
|
||||||
|
_ => Err(ApiError::Unsupported(
|
||||||
|
"GetLineGeometry is only for Line entities".into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||||
if self.entity_exists(id) {
|
if self.entity_exists(id) {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -354,6 +634,9 @@ impl DocApiBackend for MockBackend {
|
|||||||
}
|
}
|
||||||
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool> {
|
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool> {
|
||||||
self.bodies.remove(&id);
|
self.bodies.remove(&id);
|
||||||
|
self.xdata_store.retain(|(k, _), _| *k != id);
|
||||||
|
self.xrecord_store.remove(&id);
|
||||||
|
self.entity_layers.remove(&id);
|
||||||
Ok(self.kinds.remove(&id).is_some())
|
Ok(self.kinds.remove(&id).is_some())
|
||||||
}
|
}
|
||||||
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView> {
|
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView> {
|
||||||
@@ -364,7 +647,17 @@ impl DocApiBackend for MockBackend {
|
|||||||
max: bb.max,
|
max: bb.max,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Ok(EntityView { id, kind, bounds })
|
let layer = self
|
||||||
|
.entity_layers
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "0".to_string());
|
||||||
|
Ok(EntityView {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
layer,
|
||||||
|
bounds,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
fn transform_entity(&mut self, id: ObjectId, placement: &PlacementSpec) -> ApiResult<()> {
|
fn transform_entity(&mut self, id: ObjectId, placement: &PlacementSpec) -> ApiResult<()> {
|
||||||
if !self.entity_exists(id) {
|
if !self.entity_exists(id) {
|
||||||
@@ -412,6 +705,9 @@ impl DocApiBackend for MockBackend {
|
|||||||
fn entity_exists(&self, id: ObjectId) -> bool {
|
fn entity_exists(&self, id: ObjectId) -> bool {
|
||||||
self.kinds.contains_key(&id)
|
self.kinds.contains_key(&id)
|
||||||
}
|
}
|
||||||
|
fn object_exists(&self, id: ObjectId) -> bool {
|
||||||
|
self.xrecord_store.contains_key(&id)
|
||||||
|
}
|
||||||
fn revision(&self) -> GeometryRevision {
|
fn revision(&self) -> GeometryRevision {
|
||||||
GeometryRevision(self.revision)
|
GeometryRevision(self.revision)
|
||||||
}
|
}
|
||||||
@@ -609,3 +905,178 @@ fn typed_handles_reject_foreign_sessions_and_tabs() {
|
|||||||
assert!(group.compensate(&doc).is_err());
|
assert!(group.compensate(&doc).is_err());
|
||||||
assert!(first.document(11).curves().create_point([0.0; 3]).is_err());
|
assert!(first.document(11).curves().create_point([0.0; 3]).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doc_api_layer_crud_via_facade_and_entity_assignment() {
|
||||||
|
let (api, _tp) = api();
|
||||||
|
let doc = api.document(api.active_tab());
|
||||||
|
|
||||||
|
let mut info = LayerInfo::new("Walls");
|
||||||
|
info.color = Color::Index(1);
|
||||||
|
info.line_weight = LineWeight::Value(25);
|
||||||
|
doc.create_layer(&info).unwrap();
|
||||||
|
|
||||||
|
let layers = doc.layers().unwrap();
|
||||||
|
assert!(layers.iter().any(|l| l.name == "WALLS"));
|
||||||
|
|
||||||
|
let mut updated = info.clone();
|
||||||
|
updated.color = Color::Index(2);
|
||||||
|
doc.update_layer("walls", &updated).unwrap();
|
||||||
|
|
||||||
|
let line = doc.curves().create_line([0.0; 3], [1.0; 3]).unwrap();
|
||||||
|
line.set_layer("Walls").unwrap();
|
||||||
|
assert_eq!(line.layer().unwrap(), "WALLS");
|
||||||
|
|
||||||
|
let err = doc.delete_layer("Walls").unwrap_err();
|
||||||
|
assert!(matches!(err, ApiError::Validation { .. }));
|
||||||
|
|
||||||
|
let other = LayerInfo::new("Other");
|
||||||
|
doc.create_layer(&other).unwrap();
|
||||||
|
line.set_layer("Other").unwrap();
|
||||||
|
doc.delete_layer("Walls").unwrap();
|
||||||
|
assert!(!doc.layers().unwrap().iter().any(|l| l.name == "WALLS"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_xdata_many_is_atomic_and_assigns_records() {
|
||||||
|
let (api, _tp) = api();
|
||||||
|
let doc = api.document(api.active_tab());
|
||||||
|
let line = doc.curves().create_line([0.0; 3], [1.0; 3]).unwrap();
|
||||||
|
let circle = doc.curves().create_circle([0.0; 3], 1.0).unwrap();
|
||||||
|
|
||||||
|
let record1 = XDataRecord {
|
||||||
|
application_name: "APP1".to_string(),
|
||||||
|
values: vec![XDataValue::Integer32(42)],
|
||||||
|
};
|
||||||
|
let record2 = XDataRecord {
|
||||||
|
application_name: "APP2".to_string(),
|
||||||
|
values: vec![XDataValue::Real(3.14)],
|
||||||
|
};
|
||||||
|
doc.set_xdata_many([
|
||||||
|
(line.id(), "APP1", Some(record1.clone())),
|
||||||
|
(circle.id(), "APP2", Some(record2.clone())),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let r1 = doc.entities().get(line.id()).unwrap().xdata("APP1").unwrap().unwrap();
|
||||||
|
assert_eq!(r1.values, vec![XDataValue::Integer32(42)]);
|
||||||
|
let r2 = doc.entities().get(circle.id()).unwrap().xdata("APP2").unwrap().unwrap();
|
||||||
|
assert_eq!(r2.values, vec![XDataValue::Real(3.14)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_xdata_many_with_unknown_id_fails_atomically() {
|
||||||
|
let (api, _tp) = api();
|
||||||
|
let doc = api.document(api.active_tab());
|
||||||
|
let line = doc.curves().create_line([0.0; 3], [1.0; 3]).unwrap();
|
||||||
|
let stale = ObjectId::from_u64(999_999);
|
||||||
|
|
||||||
|
let record = XDataRecord {
|
||||||
|
application_name: "APP1".to_string(),
|
||||||
|
values: vec![XDataValue::Integer32(42)],
|
||||||
|
};
|
||||||
|
let err = doc
|
||||||
|
.set_xdata_many([
|
||||||
|
(line.id(), "APP1", Some(record.clone())),
|
||||||
|
(stale, "APP2", Some(record)),
|
||||||
|
])
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, ApiError::UnknownId(..) | ApiError::Validation { .. }));
|
||||||
|
assert!(doc.entities().get(line.id()).unwrap().xdata("APP1").unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn xdata_rejects_a_mismatched_record_name() {
|
||||||
|
let (api, _tp) = api();
|
||||||
|
let doc = api.document(api.active_tab());
|
||||||
|
let line = doc.curves().create_line([0.0; 3], [1.0; 3]).unwrap();
|
||||||
|
let entity = doc.entities().get(line.id()).unwrap();
|
||||||
|
let result = entity.set_xdata(
|
||||||
|
"EXPECTED",
|
||||||
|
Some(XDataRecord {
|
||||||
|
application_name: "OTHER".into(),
|
||||||
|
values: vec![],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
assert!(matches!(result, Err(ApiError::Validation { .. })));
|
||||||
|
assert!(entity.xdata("EXPECTED").unwrap().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_xrecord_returns_the_typed_handle() {
|
||||||
|
let (api, _tp) = api();
|
||||||
|
let doc = api.document(api.active_tab());
|
||||||
|
let spec = ocs_doc_api::XRecordSpec {
|
||||||
|
name: "PLUGIN_DATA".into(),
|
||||||
|
cloning_flags: ocs_doc_api::XRecordCloningFlags::NotApplicable,
|
||||||
|
entries: vec![ocs_doc_api::XRecordEntry {
|
||||||
|
code: 1,
|
||||||
|
value: ocs_doc_api::XRecordValue::String("value".into()),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let record = doc.entities().create_xrecord(&spec).unwrap();
|
||||||
|
assert_eq!(record.payload().unwrap(), spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_many_applies_per_curve_layer() {
|
||||||
|
let (api, _tp) = api();
|
||||||
|
let doc = api.document(api.active_tab());
|
||||||
|
doc.create_layer(&LayerInfo::new("Walls")).unwrap();
|
||||||
|
|
||||||
|
let ids = doc
|
||||||
|
.entities()
|
||||||
|
.create_many(&[
|
||||||
|
ocs_doc_api::EntitySpec::Curve(Curve2Spec::Line {
|
||||||
|
start: [0.0; 3],
|
||||||
|
end: [1.0; 3],
|
||||||
|
layer: Some("Walls".to_string()),
|
||||||
|
}),
|
||||||
|
ocs_doc_api::EntitySpec::Curve(Curve2Spec::Point {
|
||||||
|
position: [2.0; 3],
|
||||||
|
layer: None,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(ids[0].layer().unwrap(), "WALLS");
|
||||||
|
assert_eq!(ids[1].layer().unwrap(), "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_many_rejects_missing_layer_before_mutating() {
|
||||||
|
let (api, tp) = api();
|
||||||
|
let doc = api.document(api.active_tab());
|
||||||
|
let rev0 = doc.revision().unwrap().as_u64();
|
||||||
|
|
||||||
|
let err = doc
|
||||||
|
.entities()
|
||||||
|
.create_many(&[
|
||||||
|
ocs_doc_api::EntitySpec::Curve(Curve2Spec::Line {
|
||||||
|
start: [0.0; 3],
|
||||||
|
end: [1.0; 3],
|
||||||
|
layer: Some("Missing".to_string()),
|
||||||
|
}),
|
||||||
|
ocs_doc_api::EntitySpec::Curve(Curve2Spec::Point {
|
||||||
|
position: [2.0; 3],
|
||||||
|
layer: None,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, ApiError::Validation { .. }));
|
||||||
|
|
||||||
|
let backend = tp.backend();
|
||||||
|
assert_eq!(backend.revision, rev0);
|
||||||
|
assert_eq!(backend.next_id, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_line_on_layer_requires_existing_layer() {
|
||||||
|
let (api, _tp) = api();
|
||||||
|
let doc = api.document(api.active_tab());
|
||||||
|
let err = doc
|
||||||
|
.curves()
|
||||||
|
.create_line_on_layer([0.0; 3], [1.0; 3], "Missing")
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(err, ApiError::Validation { .. }));
|
||||||
|
}
|
||||||
@@ -38,7 +38,8 @@ fn spec() -> Spec {
|
|||||||
/// The variant names of the `Operation` enum, in declaration order.
|
/// The variant names of the `Operation` enum, in declaration order.
|
||||||
fn operation_variants() -> Vec<String> {
|
fn operation_variants() -> Vec<String> {
|
||||||
use ocs_doc_api::ops::{
|
use ocs_doc_api::ops::{
|
||||||
BoolOp, Curve2Spec, InsertSpec, Operation, PlacementSpec, SolidPrimitive, ViewportSpec,
|
BoolOp, Curve2Spec, InsertSpec, LayerInfo, Operation, PlacementSpec, SolidPrimitive,
|
||||||
|
ViewportSpec,
|
||||||
};
|
};
|
||||||
use ocs_doc_api::ObjectId;
|
use ocs_doc_api::ObjectId;
|
||||||
let _marker: Vec<Operation> = vec![
|
let _marker: Vec<Operation> = vec![
|
||||||
@@ -46,7 +47,10 @@ fn operation_variants() -> Vec<String> {
|
|||||||
centre: [0.0; 3],
|
centre: [0.0; 3],
|
||||||
radius: 1.0,
|
radius: 1.0,
|
||||||
}),
|
}),
|
||||||
Operation::CreateCurve(Curve2Spec::Point { position: [0.0; 3] }),
|
Operation::CreateCurve(Curve2Spec::Point {
|
||||||
|
position: [0.0; 3],
|
||||||
|
layer: None,
|
||||||
|
}),
|
||||||
Operation::Extrude {
|
Operation::Extrude {
|
||||||
profile: ObjectId::from_u64(0),
|
profile: ObjectId::from_u64(0),
|
||||||
direction: [0.0; 3],
|
direction: [0.0; 3],
|
||||||
@@ -167,6 +171,37 @@ fn operation_variants() -> Vec<String> {
|
|||||||
second_point: [0.0; 3],
|
second_point: [0.0; 3],
|
||||||
arc_location: [0.0; 3],
|
arc_location: [0.0; 3],
|
||||||
}),
|
}),
|
||||||
|
Operation::SetXData {
|
||||||
|
id: ObjectId::from_u64(0),
|
||||||
|
application_name: String::new(),
|
||||||
|
record: None,
|
||||||
|
},
|
||||||
|
Operation::SetXDataMany(vec![]),
|
||||||
|
Operation::CreateXRecord(ocs_doc_api::ops::XRecordSpec {
|
||||||
|
name: String::new(),
|
||||||
|
cloning_flags: ocs_doc_api::ops::XRecordCloningFlags::NotApplicable,
|
||||||
|
entries: vec![],
|
||||||
|
}),
|
||||||
|
Operation::SetXRecord {
|
||||||
|
id: ObjectId::from_u64(0),
|
||||||
|
spec: ocs_doc_api::ops::XRecordSpec {
|
||||||
|
name: String::new(),
|
||||||
|
cloning_flags: ocs_doc_api::ops::XRecordCloningFlags::NotApplicable,
|
||||||
|
entries: vec![],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Operation::CreateLayer(LayerInfo::new("LAYER")),
|
||||||
|
Operation::UpdateLayer {
|
||||||
|
name: String::new(),
|
||||||
|
info: LayerInfo::new("LAYER"),
|
||||||
|
},
|
||||||
|
Operation::DeleteLayer {
|
||||||
|
name: String::new(),
|
||||||
|
},
|
||||||
|
Operation::SetEntityLayer {
|
||||||
|
id: ObjectId::from_u64(0),
|
||||||
|
layer: String::new(),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
_marker
|
_marker
|
||||||
.iter()
|
.iter()
|
||||||
@@ -178,7 +213,7 @@ fn operation_variants() -> Vec<String> {
|
|||||||
index
|
index
|
||||||
);
|
);
|
||||||
let dbg = format!("{:?}", op);
|
let dbg = format!("{:?}", op);
|
||||||
dbg.split(|c: char| c == '(' || c == ' ' || c == '{')
|
dbg.split(['(', ' ', '{'])
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string()
|
.to_string()
|
||||||
@@ -226,6 +261,17 @@ fn query_variants() -> Vec<String> {
|
|||||||
Query::GetViewportView {
|
Query::GetViewportView {
|
||||||
id: ObjectId::from_u64(0),
|
id: ObjectId::from_u64(0),
|
||||||
},
|
},
|
||||||
|
Query::GetXData {
|
||||||
|
id: ObjectId::from_u64(0),
|
||||||
|
application_name: String::new(),
|
||||||
|
},
|
||||||
|
Query::GetXRecord {
|
||||||
|
id: ObjectId::from_u64(0),
|
||||||
|
},
|
||||||
|
Query::ListLayers,
|
||||||
|
Query::GetEntityLayer {
|
||||||
|
id: ObjectId::from_u64(0),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
marker
|
marker
|
||||||
.iter()
|
.iter()
|
||||||
@@ -237,7 +283,7 @@ fn query_variants() -> Vec<String> {
|
|||||||
index
|
index
|
||||||
);
|
);
|
||||||
let dbg = format!("{:?}", q);
|
let dbg = format!("{:?}", q);
|
||||||
dbg.split(|c: char| c == '(' || c == ' ' || c == '{')
|
dbg.split(['(', ' ', '{'])
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string()
|
.to_string()
|
||||||
@@ -309,6 +355,14 @@ const OPERATION_BASELINE: &[&str] = &[
|
|||||||
"CreateAttributeDefinition",
|
"CreateAttributeDefinition",
|
||||||
"CreateTable",
|
"CreateTable",
|
||||||
"CreateDimensionAngular2Ln",
|
"CreateDimensionAngular2Ln",
|
||||||
|
"SetXData",
|
||||||
|
"SetXDataMany",
|
||||||
|
"CreateXRecord",
|
||||||
|
"SetXRecord",
|
||||||
|
"CreateLayer",
|
||||||
|
"UpdateLayer",
|
||||||
|
"DeleteLayer",
|
||||||
|
"SetEntityLayer",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Recorded baseline of the `Query` variant order.
|
/// Recorded baseline of the `Query` variant order.
|
||||||
@@ -325,6 +379,10 @@ const QUERY_BASELINE: &[&str] = &[
|
|||||||
"GetAttributes",
|
"GetAttributes",
|
||||||
"GetBlockEntities",
|
"GetBlockEntities",
|
||||||
"GetViewportView",
|
"GetViewportView",
|
||||||
|
"GetXData",
|
||||||
|
"GetXRecord",
|
||||||
|
"ListLayers",
|
||||||
|
"GetEntityLayer",
|
||||||
];
|
];
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
<meta name="twitter:description" content="{{description}}" />
|
<meta name="twitter:description" content="{{description}}" />
|
||||||
<meta name="twitter:image" content="https://www.opencadstudio.com{{workspace}}" />
|
<meta name="twitter:image" content="https://www.opencadstudio.com{{workspace}}" />
|
||||||
<script type="application/ld+json">{{schema}}</script>
|
<script type="application/ld+json">{{schema}}</script>
|
||||||
|
{{website_schema}}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a class="skip-link" href="#main">{{skip}}</a>
|
<a class="skip-link" href="#main">{{skip}}</a>
|
||||||
|
|||||||
@@ -1388,6 +1388,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = يتحكم في مربع التحديد المرئي ومنطقة النقر معًا.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = يتحكم في مربع التحديد المرئي ومنطقة النقر معًا.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = التطبيقات
|
||||||
|
.installed-plugins-and-their-sources = الإضافات المثبتة ومصادرها
|
||||||
|
.plugins-2 = الإضافات…
|
||||||
|
.keyboard-shortcuts-2 = اختصارات لوحة المفاتيح
|
||||||
|
.keyboard-shortcuts-3 = اختصارات لوحة المفاتيح…
|
||||||
|
.command-aliases-2 = الأسماء المستعارة للأوامر
|
||||||
|
.command-aliases-3 = الأسماء المستعارة للأوامر…
|
||||||
|
.file-safety = أمان الملفات
|
||||||
|
.automatic-save = الحفظ التلقائي
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = الدقائق بين عمليات حفظ الاسترداد في ملف .sv$؛ القيمة 0 تعطّله (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = الاحتفاظ بنسخة .bak عند استبدال رسم (ISAVEBAK)
|
||||||
|
.plotting = الطباعة
|
||||||
|
.plot-device-paper-scale-and-plot-styles = جهاز الطباعة والورق والمقياس وأنماط الطباعة
|
||||||
|
.plot-and-page-setup = الطباعة وإعداد الصفحة…
|
||||||
|
.fill-truetype-glyphs-textfill = ملء محارف TrueType (TEXTFILL)
|
||||||
|
.command-line = سطر الأوامر
|
||||||
|
.prompt-lines = أسطر المطالبة
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = أسطر المطالبة المؤقتة أعلى نافذة الأوامر (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = زمن تلاشي السجل
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = مدة بقاء أسطر السجل ظاهرة؛ القيمة 0 تتخطاها (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = النقر على الكائنات المتداخلة يفتح أداة اختيار
|
||||||
|
.block-edit = تحرير الكتلة
|
||||||
|
.double-click-to-edit-block-in-place-refedit = النقر المزدوج يحرر الكتلة في مكانها (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = النقر المزدوج على الكتل ذات السمات يحرر سماتها (ATTEDIT)
|
||||||
|
.user-preferences = تفضيلات المستخدم
|
||||||
|
.zoom = التكبير
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = عكس التكبير بعجلة الفأرة (ZOOMWHEEL)
|
||||||
|
.zoom-factor = معامل التكبير
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = مقدار التكبير لكل درجة من العجلة (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = النص والأبعاد
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = يحرر TEXTEDIT كائنًا واحدًا ثم ينتهي (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = الأبعاد المتصلة ترث طبقة ونمط البعد الأساسي (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = أولوية أصل QDIM
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = النقاط التي يقيس منها QDIM. يمكن ضبطها داخل الأمر أيضًا.
|
||||||
|
.add-scales-automatically = إضافة المقاييس تلقائيًا
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = الكائنات التوضيحية التي تتبنى مقياس توضيح تم ضبطه حديثًا (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = وحدات الطول والزاوية والإدراج
|
||||||
|
.drawing-units-2 = وحدات الرسم…
|
||||||
|
.drafting = الرسم
|
||||||
|
.drafting-rotation = دوران الرسم
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = يدير الشعيرات المتقاطعة وشبكة الالتقاط في نظام الإحداثيات النشط (SNAPANG).
|
||||||
|
.polar-tracking-increment = زيادة التتبع القطبي
|
||||||
|
.snap-and-grid = الالتقاط والشبكة
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = أوضاع الالتقاط وتباعد الشبكة والتقاط الكائنات
|
||||||
|
.drafting-settings-2 = إعدادات الرسم…
|
||||||
|
.modeling-3d = النمذجة ثلاثية الأبعاد
|
||||||
|
.display-tools = أدوات العرض
|
||||||
|
.show-the-navigation-cube-navvcube = إظهار مكعب التنقل (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = إظهار أيقونة نظام الإحداثيات (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = وضع أيقونة نظام الإحداثيات عند نقطة الأصل (UCSICON ORigin)
|
||||||
|
.display-resolution = دقة العرض
|
||||||
|
.applies-to-the-current-drawing = ينطبق على الرسم الحالي
|
||||||
|
.isolines-per-surface = خطوط التساوي لكل سطح
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = إظهار حواف الظل الخارجي على المجسمات (DISPSILH)
|
||||||
|
.surface-density-u = كثافة السطح U
|
||||||
|
.surface-density-v = كثافة السطح V
|
||||||
|
.surface-type = نوع السطح
|
||||||
|
.record-the-history-of-composite-solids-solidhist = تسجيل تاريخ المجسمات المركبة (SOLIDHIST)
|
||||||
|
.show-solid-history = إظهار تاريخ المجسمات
|
||||||
|
.not-available = غير متاح
|
||||||
|
.open-folder = فتح المجلد
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = المكان الذي يحفظ فيه التطبيق ملفاته الخاصة. هذه المواقع ثابتة.
|
||||||
|
.configuration = التهيئة
|
||||||
|
.plot-styles = أنماط الطباعة
|
||||||
|
.autosave-files = ملفات الحفظ التلقائي
|
||||||
.selection-modes = أوضاع التحديد
|
.selection-modes = أوضاع التحديد
|
||||||
.use-shift-to-add-to-selection-pickadd = استخدام Shift للإضافة إلى التحديد (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = استخدام Shift للإضافة إلى التحديد (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = الضغط والسحب يرسم مستطيلاً بدلاً من التحديد الحر (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = الضغط والسحب يرسم مستطيلاً بدلاً من التحديد الحر (PICKDRAG)
|
||||||
|
|||||||
@@ -1375,6 +1375,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Управлява видимото поле за избор и областта за щракване.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Управлява видимото поле за избор и областта за щракване.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Приложения
|
||||||
|
.installed-plugins-and-their-sources = Инсталирани добавки и техните източници
|
||||||
|
.plugins-2 = Добавки…
|
||||||
|
.keyboard-shortcuts-2 = Клавишни комбинации
|
||||||
|
.keyboard-shortcuts-3 = Клавишни комбинации…
|
||||||
|
.command-aliases-2 = Псевдоними на команди
|
||||||
|
.command-aliases-3 = Псевдоними на команди…
|
||||||
|
.file-safety = Безопасност на файловете
|
||||||
|
.automatic-save = Автоматично записване
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Минути между възстановителните записи във файл .sv$; 0 го изключва (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Запазване на копие .bak при презаписване на чертеж (ISAVEBAK)
|
||||||
|
.plotting = Чертане
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Устройство за чертане, хартия, мащаб и стилове на чертане
|
||||||
|
.plot-and-page-setup = Чертане и настройка на страница…
|
||||||
|
.fill-truetype-glyphs-textfill = Запълване на глифове TrueType (TEXTFILL)
|
||||||
|
.command-line = Команден ред
|
||||||
|
.prompt-lines = Редове с подкана
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Временни редове с подкана над командния прозорец (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Време на избледняване на историята
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Колко дълго остават видими редовете от историята; 0 ги пропуска (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Щракването върху припокриващи се обекти отваря избирач
|
||||||
|
.block-edit = Редактиране на блок
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Двойно щракване редактира блока на място (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Двойно щракване върху блокове с атрибути редактира атрибутите (ATTEDIT)
|
||||||
|
.user-preferences = Потребителски предпочитания
|
||||||
|
.zoom = Мащабиране
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Обръщане на мащабирането с колелцето (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Коефициент на мащабиране
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = Колко мащабира едно щракване на колелцето (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Текст и размери
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT редактира един обект и приключва (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Продължените размери наследяват слоя и стила на базовия размер (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = Приоритет на началото на QDIM
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = От кои точки мери QDIM. Настройва се и в самата команда.
|
||||||
|
.add-scales-automatically = Автоматично добавяне на мащаби
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Кои анотативни обекти приемат новозададен мащаб на анотация (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Единици за дължина, ъгъл и вмъкване
|
||||||
|
.drawing-units-2 = Единици на чертежа…
|
||||||
|
.drafting = Чертане на ръка
|
||||||
|
.drafting-rotation = Завъртане на чертането
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Завърта мерника и мрежата за прилепване в активната ПКС (SNAPANG).
|
||||||
|
.polar-tracking-increment = Стъпка на полярно проследяване
|
||||||
|
.snap-and-grid = Прилепване и мрежа
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Режими на прилепване, стъпка на мрежата и прилепване към обекти
|
||||||
|
.drafting-settings-2 = Настройки на чертането…
|
||||||
|
.modeling-3d = 3D моделиране
|
||||||
|
.display-tools = Инструменти за изглед
|
||||||
|
.show-the-navigation-cube-navvcube = Показвай навигационния куб (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = Показвай иконата на ПКС (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = Поставяй иконата на ПКС в началото (UCSICON ORigin)
|
||||||
|
.display-resolution = Разделителна способност на изгледа
|
||||||
|
.applies-to-the-current-drawing = отнася се за текущия чертеж
|
||||||
|
.isolines-per-surface = Изолинии на повърхнина
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Показвай силуетни ръбове на телата (DISPSILH)
|
||||||
|
.surface-density-u = Плътност на повърхнината U
|
||||||
|
.surface-density-v = Плътност на повърхнината V
|
||||||
|
.surface-type = Тип повърхнина
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Записвай историята на съставните тела (SOLIDHIST)
|
||||||
|
.show-solid-history = Показвай историята на телата
|
||||||
|
.not-available = Не е налично
|
||||||
|
.open-folder = Отвори папката
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Къде приложението държи собствените си файлове. Тези места са фиксирани.
|
||||||
|
.configuration = Конфигурация
|
||||||
|
.plot-styles = Стилове на чертане
|
||||||
|
.autosave-files = Файлове за автоматично записване
|
||||||
.selection-modes = Режими на избор
|
.selection-modes = Режими на избор
|
||||||
.use-shift-to-add-to-selection-pickadd = Използвай Shift за добавяне към избора (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Използвай Shift за добавяне към избора (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Натискане и влачене чертае правоъгълник вместо ласо (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Натискане и влачене чертае правоъгълник вместо ласо (PICKDRAG)
|
||||||
|
|||||||
@@ -1375,6 +1375,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Ovládá viditelný výběrový rámeček i oblast kliknutí.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Ovládá viditelný výběrový rámeček i oblast kliknutí.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Aplikace
|
||||||
|
.installed-plugins-and-their-sources = Nainstalované zásuvné moduly a jejich zdroje
|
||||||
|
.plugins-2 = Zásuvné moduly…
|
||||||
|
.keyboard-shortcuts-2 = Klávesové zkratky
|
||||||
|
.keyboard-shortcuts-3 = Klávesové zkratky…
|
||||||
|
.command-aliases-2 = Aliasy příkazů
|
||||||
|
.command-aliases-3 = Aliasy příkazů…
|
||||||
|
.file-safety = Bezpečnost souborů
|
||||||
|
.automatic-save = Automatické ukládání
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Minuty mezi obnovovacími uloženími do souboru .sv$; 0 je vypne (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Při přepsání výkresu ponechat kopii .bak (ISAVEBAK)
|
||||||
|
.plotting = Vykreslování
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Vykreslovací zařízení, papír, měřítko a styly vykreslení
|
||||||
|
.plot-and-page-setup = Vykreslit a nastavení stránky…
|
||||||
|
.fill-truetype-glyphs-textfill = Vyplňovat glyfy TrueType (TEXTFILL)
|
||||||
|
.command-line = Příkazový řádek
|
||||||
|
.prompt-lines = Řádky výzvy
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Dočasné řádky výzvy nad příkazovým oknem (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Doba mizení historie
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Jak dlouho zůstávají řádky historie viditelné; 0 je přeskočí (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Kliknutí na překrývající se objekty otevře výběr
|
||||||
|
.block-edit = Úprava bloku
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Dvojklik upraví blok na místě (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Dvojklik na bloky s atributy upraví atributy (ATTEDIT)
|
||||||
|
.user-preferences = Předvolby uživatele
|
||||||
|
.zoom = Zoom
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Obrátit zoom kolečkem myši (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Faktor zoomu
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = O kolik přiblíží jeden zub kolečka (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Text a kóty
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT upraví jeden objekt a skončí (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Navazující kóty dědí hladinu a styl základní kóty (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = Priorita počátku QDIM
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = Ze kterých bodů QDIM měří. Nastavitelné i uvnitř příkazu.
|
||||||
|
.add-scales-automatically = Přidávat měřítka automaticky
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Které anotativní objekty převezmou nově nastavené měřítko anotace (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Jednotky délky, úhlu a vložení
|
||||||
|
.drawing-units-2 = Jednotky výkresu…
|
||||||
|
.drafting = Kreslení
|
||||||
|
.drafting-rotation = Otočení kreslení
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Otáčí nitkový kříž a rastr uchopení v aktivním USS (SNAPANG).
|
||||||
|
.polar-tracking-increment = Krok polárního sledování
|
||||||
|
.snap-and-grid = Uchopení a rastr
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Režimy uchopení, rozteč rastru a uchopení objektů
|
||||||
|
.drafting-settings-2 = Nastavení kreslení…
|
||||||
|
.modeling-3d = 3D modelování
|
||||||
|
.display-tools = Nástroje zobrazení
|
||||||
|
.show-the-navigation-cube-navvcube = Zobrazit navigační kostku (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = Zobrazit ikonu USS (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = Umístit ikonu USS do počátku (UCSICON ORigin)
|
||||||
|
.display-resolution = Rozlišení zobrazení
|
||||||
|
.applies-to-the-current-drawing = platí pro aktuální výkres
|
||||||
|
.isolines-per-surface = Izočáry na plochu
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Zobrazit obrysové hrany těles (DISPSILH)
|
||||||
|
.surface-density-u = Hustota plochy U
|
||||||
|
.surface-density-v = Hustota plochy V
|
||||||
|
.surface-type = Typ plochy
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Zaznamenávat historii složených těles (SOLIDHIST)
|
||||||
|
.show-solid-history = Zobrazit historii těles
|
||||||
|
.not-available = Není k dispozici
|
||||||
|
.open-folder = Otevřít složku
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Kde aplikace uchovává vlastní soubory. Tato umístění jsou pevná.
|
||||||
|
.configuration = Konfigurace
|
||||||
|
.plot-styles = Styly vykreslení
|
||||||
|
.autosave-files = Soubory automatického ukládání
|
||||||
.selection-modes = Režimy výběru
|
.selection-modes = Režimy výběru
|
||||||
.use-shift-to-add-to-selection-pickadd = Použít Shift pro přidání do výběru (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Použít Shift pro přidání do výběru (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Tažení kreslí obdélník místo lasa (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Tažení kreslí obdélník místo lasa (PICKDRAG)
|
||||||
|
|||||||
@@ -1376,6 +1376,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Steuert sowohl das sichtbare Auswahlkästchen als auch den Klickbereich.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Steuert sowohl das sichtbare Auswahlkästchen als auch den Klickbereich.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Anwendungen
|
||||||
|
.installed-plugins-and-their-sources = Installierte Plug-ins und ihre Quellen
|
||||||
|
.plugins-2 = Plug-ins…
|
||||||
|
.keyboard-shortcuts-2 = Tastenkürzel
|
||||||
|
.keyboard-shortcuts-3 = Tastenkürzel…
|
||||||
|
.command-aliases-2 = Befehlsaliase
|
||||||
|
.command-aliases-3 = Befehlsaliase…
|
||||||
|
.file-safety = Dateisicherheit
|
||||||
|
.automatic-save = Automatisches Speichern
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Minuten zwischen Wiederherstellungsspeicherungen in eine .sv$-Datei; 0 schaltet es aus (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Beim Überschreiben eine .bak-Kopie behalten (ISAVEBAK)
|
||||||
|
.plotting = Plotten
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Plotgerät, Papier, Maßstab und Plotstile
|
||||||
|
.plot-and-page-setup = Plotten und Seiteneinrichtung…
|
||||||
|
.fill-truetype-glyphs-textfill = TrueType-Glyphen füllen (TEXTFILL)
|
||||||
|
.command-line = Befehlszeile
|
||||||
|
.prompt-lines = Eingabeaufforderungszeilen
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Temporäre Eingabeaufforderungszeilen über dem Befehlsfenster (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Ausblendzeit des Verlaufs
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Wie lange Verlaufszeilen sichtbar bleiben; 0 überspringt sie (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Klick auf überlappende Objekte öffnet eine Auswahl
|
||||||
|
.block-edit = Blockbearbeitung
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Doppelklick bearbeitet den Block an Ort und Stelle (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Doppelklick auf Blöcke mit Attributen bearbeitet diese (ATTEDIT)
|
||||||
|
.user-preferences = Benutzereinstellungen
|
||||||
|
.zoom = Zoom
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Mausradzoom umkehren (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Zoomfaktor
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = Wie weit eine Radrastung zoomt (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Text und Bemaßung
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT bearbeitet ein Objekt und endet (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Fortgesetzte Bemaßungen erben Layer und Stil der Basisbemaßung (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = QDIM-Ursprungspriorität
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = Von welchen Punkten QDIM misst. Auch im Befehl einstellbar.
|
||||||
|
.add-scales-automatically = Maßstäbe automatisch hinzufügen
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Welche annotativen Objekte einen neu gesetzten Beschriftungsmaßstab übernehmen (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Längen-, Winkel- und Einfügeeinheiten
|
||||||
|
.drawing-units-2 = Zeichnungseinheiten…
|
||||||
|
.drafting = Entwurf
|
||||||
|
.drafting-rotation = Entwurfsdrehung
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Dreht Fadenkreuz und Fangraster im aktiven BKS (SNAPANG).
|
||||||
|
.polar-tracking-increment = Polarer Verfolgungsschritt
|
||||||
|
.snap-and-grid = Fang und Raster
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Fangmodi, Rasterabstand und Objektfang
|
||||||
|
.drafting-settings-2 = Entwurfseinstellungen…
|
||||||
|
.modeling-3d = 3D-Modellierung
|
||||||
|
.display-tools = Anzeigewerkzeuge
|
||||||
|
.show-the-navigation-cube-navvcube = Navigationswürfel anzeigen (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = BKS-Symbol anzeigen (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = BKS-Symbol am Ursprung platzieren (UCSICON ORigin)
|
||||||
|
.display-resolution = Anzeigeauflösung
|
||||||
|
.applies-to-the-current-drawing = gilt für die aktuelle Zeichnung
|
||||||
|
.isolines-per-surface = Isolinien pro Fläche
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Silhouettenkanten an Volumenkörpern anzeigen (DISPSILH)
|
||||||
|
.surface-density-u = Flächendichte U
|
||||||
|
.surface-density-v = Flächendichte V
|
||||||
|
.surface-type = Flächentyp
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Verlauf zusammengesetzter Volumenkörper aufzeichnen (SOLIDHIST)
|
||||||
|
.show-solid-history = Volumenkörperverlauf anzeigen
|
||||||
|
.not-available = Nicht verfügbar
|
||||||
|
.open-folder = Ordner öffnen
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Wo die Anwendung ihre eigenen Dateien ablegt. Diese Orte sind fest.
|
||||||
|
.configuration = Konfiguration
|
||||||
|
.plot-styles = Plotstile
|
||||||
|
.autosave-files = Dateien für automatisches Speichern
|
||||||
.selection-modes = Auswahlmodi
|
.selection-modes = Auswahlmodi
|
||||||
.use-shift-to-add-to-selection-pickadd = Umschalt zum Hinzufügen zur Auswahl verwenden (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Umschalt zum Hinzufügen zur Auswahl verwenden (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Ziehen mit gedrückter Maustaste zeichnet ein Rechteck statt eines Lassos (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Ziehen mit gedrückter Maustaste zeichnet ein Rechteck statt eines Lassos (PICKDRAG)
|
||||||
|
|||||||
@@ -1377,6 +1377,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Ελέγχει τόσο το ορατό πλαίσιο επιλογής όσο και την περιοχή κλικ.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Ελέγχει τόσο το ορατό πλαίσιο επιλογής όσο και την περιοχή κλικ.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Εφαρμογές
|
||||||
|
.installed-plugins-and-their-sources = Εγκατεστημένα πρόσθετα και οι πηγές τους
|
||||||
|
.plugins-2 = Πρόσθετα…
|
||||||
|
.keyboard-shortcuts-2 = Συντομεύσεις πληκτρολογίου
|
||||||
|
.keyboard-shortcuts-3 = Συντομεύσεις πληκτρολογίου…
|
||||||
|
.command-aliases-2 = Ψευδώνυμα εντολών
|
||||||
|
.command-aliases-3 = Ψευδώνυμα εντολών…
|
||||||
|
.file-safety = Ασφάλεια αρχείων
|
||||||
|
.automatic-save = Αυτόματη αποθήκευση
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Λεπτά μεταξύ αποθηκεύσεων ανάκτησης σε αρχείο .sv$· το 0 την απενεργοποιεί (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Διατήρηση αντιγράφου .bak κατά την αντικατάσταση σχεδίου (ISAVEBAK)
|
||||||
|
.plotting = Εκτύπωση
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Συσκευή εκτύπωσης, χαρτί, κλίμακα και στυλ εκτύπωσης
|
||||||
|
.plot-and-page-setup = Εκτύπωση και διαμόρφωση σελίδας…
|
||||||
|
.fill-truetype-glyphs-textfill = Γέμισμα γραμματοσειρών TrueType (TEXTFILL)
|
||||||
|
.command-line = Γραμμή εντολών
|
||||||
|
.prompt-lines = Γραμμές προτροπής
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Προσωρινές γραμμές προτροπής πάνω από το παράθυρο εντολών (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Χρόνος εξασθένισης ιστορικού
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Πόσο μένουν ορατές οι γραμμές ιστορικού· το 0 τις παραλείπει (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Το κλικ σε επικαλυπτόμενα αντικείμενα ανοίγει επιλογέα
|
||||||
|
.block-edit = Επεξεργασία μπλοκ
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Διπλό κλικ για επεξεργασία μπλοκ επί τόπου (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Διπλό κλικ σε μπλοκ με χαρακτηριστικά για επεξεργασία τους (ATTEDIT)
|
||||||
|
.user-preferences = Προτιμήσεις χρήστη
|
||||||
|
.zoom = Ζουμ
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Αντιστροφή ζουμ με τον τροχό (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Συντελεστής ζουμ
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = Πόσο ζουμάρει ένα σκαλί του τροχού (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Κείμενο και διαστάσεις
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = Το TEXTEDIT επεξεργάζεται ένα αντικείμενο και τερματίζει (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Οι συνεχόμενες διαστάσεις κληρονομούν επίπεδο και στυλ από τη βασική (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = Προτεραιότητα αρχής QDIM
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = Από ποια σημεία μετρά το QDIM. Ρυθμίζεται και μέσα στην εντολή.
|
||||||
|
.add-scales-automatically = Αυτόματη προσθήκη κλιμάκων
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Ποια επισημειωματικά αντικείμενα παίρνουν μια νέα κλίμακα επισημείωσης (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Μονάδες μήκους, γωνίας και εισαγωγής
|
||||||
|
.drawing-units-2 = Μονάδες σχεδίου…
|
||||||
|
.drafting = Σχεδίαση
|
||||||
|
.drafting-rotation = Περιστροφή σχεδίασης
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Περιστρέφει το σταυρόνημα και το πλέγμα έλξης στο ενεργό UCS (SNAPANG).
|
||||||
|
.polar-tracking-increment = Βήμα πολικής παρακολούθησης
|
||||||
|
.snap-and-grid = Έλξη και πλέγμα
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Λειτουργίες έλξης, βήμα πλέγματος και έλξη αντικειμένων
|
||||||
|
.drafting-settings-2 = Ρυθμίσεις σχεδίασης…
|
||||||
|
.modeling-3d = Μοντελοποίηση 3D
|
||||||
|
.display-tools = Εργαλεία προβολής
|
||||||
|
.show-the-navigation-cube-navvcube = Εμφάνιση του κύβου πλοήγησης (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = Εμφάνιση του εικονιδίου UCS (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = Τοποθέτηση του εικονιδίου UCS στην αρχή των αξόνων (UCSICON ORigin)
|
||||||
|
.display-resolution = Ανάλυση εμφάνισης
|
||||||
|
.applies-to-the-current-drawing = ισχύει για το τρέχον σχέδιο
|
||||||
|
.isolines-per-surface = Ισοϋψείς ανά επιφάνεια
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Εμφάνιση ακμών περιγράμματος σε στερεά (DISPSILH)
|
||||||
|
.surface-density-u = Πυκνότητα επιφάνειας U
|
||||||
|
.surface-density-v = Πυκνότητα επιφάνειας V
|
||||||
|
.surface-type = Τύπος επιφάνειας
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Καταγραφή ιστορικού σύνθετων στερεών (SOLIDHIST)
|
||||||
|
.show-solid-history = Εμφάνιση ιστορικού στερεών
|
||||||
|
.not-available = Μη διαθέσιμο
|
||||||
|
.open-folder = Άνοιγμα φακέλου
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Πού κρατά η εφαρμογή τα δικά της αρχεία. Οι θέσεις αυτές είναι σταθερές.
|
||||||
|
.configuration = Ρυθμίσεις
|
||||||
|
.plot-styles = Στυλ εκτύπωσης
|
||||||
|
.autosave-files = Αρχεία αυτόματης αποθήκευσης
|
||||||
.selection-modes = Τρόποι επιλογής
|
.selection-modes = Τρόποι επιλογής
|
||||||
.use-shift-to-add-to-selection-pickadd = Χρήση Shift για προσθήκη στην επιλογή (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Χρήση Shift για προσθήκη στην επιλογή (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Το πάτημα-και-σύρσιμο σχεδιάζει ορθογώνιο αντί για λάσο (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Το πάτημα-και-σύρσιμο σχεδιάζει ορθογώνιο αντί για λάσο (PICKDRAG)
|
||||||
|
|||||||
@@ -1377,6 +1377,71 @@ common =
|
|||||||
.pick-box-size = Pick box size
|
.pick-box-size = Pick box size
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Controls both the visible selection box and the click aperture.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Controls both the visible selection box and the click aperture.
|
||||||
.visual-effect-settings = Visual Effect Settings
|
.visual-effect-settings = Visual Effect Settings
|
||||||
|
.applications = Applications
|
||||||
|
.installed-plugins-and-their-sources = Installed plugins and their sources
|
||||||
|
.plugins-2 = Plugins…
|
||||||
|
.keyboard-shortcuts-2 = Keyboard shortcuts
|
||||||
|
.keyboard-shortcuts-3 = Keyboard Shortcuts…
|
||||||
|
.command-aliases-2 = Command aliases
|
||||||
|
.command-aliases-3 = Command Aliases…
|
||||||
|
.file-safety = File Safety
|
||||||
|
.automatic-save = Automatic save
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Minutes between recovery saves to a .sv$ file; 0 turns it off (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Keep a .bak copy when overwriting a drawing (ISAVEBAK)
|
||||||
|
.plotting = Plotting
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Plot device, paper, scale and plot styles
|
||||||
|
.plot-and-page-setup = Plot and Page Setup…
|
||||||
|
.fill-truetype-glyphs-textfill = Fill TrueType glyphs (TEXTFILL)
|
||||||
|
.command-line = Command Line
|
||||||
|
.prompt-lines = Prompt lines
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Temporary prompt lines shown above the command window (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = History fade time
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = How long overlay history lines stay visible; 0 skips them (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Clicking overlapping objects opens a picker
|
||||||
|
.block-edit = Block Edit
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Double-click to edit block in-place (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Double-click attributed blocks to edit attributes (ATTEDIT)
|
||||||
|
.user-preferences = User Preferences
|
||||||
|
.zoom = Zoom
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Reverse mouse wheel zoom (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Zoom factor
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = How far one wheel notch zooms (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Text and Dimensions
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT edits one object and ends (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Continued dimensions inherit the base dimension's layer and style (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = QDIM origin priority
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = Which points QDIM measures from. Also settable inside the command.
|
||||||
|
.add-scales-automatically = Add scales automatically
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Which annotative objects pick up a newly set annotation scale (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Length, angle and insertion units
|
||||||
|
.drawing-units-2 = Drawing Units…
|
||||||
|
.drafting = Drafting
|
||||||
|
.drafting-rotation = Drafting rotation
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Rotates the crosshair and the snap grid in the active UCS (SNAPANG).
|
||||||
|
.polar-tracking-increment = Polar tracking increment
|
||||||
|
.snap-and-grid = Snap and Grid
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Snap modes, grid spacing and object snap
|
||||||
|
.drafting-settings-2 = Drafting Settings…
|
||||||
|
.modeling-3d = 3D Modeling
|
||||||
|
.display-tools = Display Tools
|
||||||
|
.show-the-navigation-cube-navvcube = Show the navigation cube (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = Show the UCS icon (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = Place the UCS icon at the origin (UCSICON ORigin)
|
||||||
|
.display-resolution = Display Resolution
|
||||||
|
.applies-to-the-current-drawing = applies to the current drawing
|
||||||
|
.isolines-per-surface = Isolines per surface
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Show silhouette edges on solids (DISPSILH)
|
||||||
|
.surface-density-u = Surface density U
|
||||||
|
.surface-density-v = Surface density V
|
||||||
|
.surface-type = Surface type
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Record the history of composite solids (SOLIDHIST)
|
||||||
|
.show-solid-history = Show solid history
|
||||||
|
.not-available = Not available
|
||||||
|
.open-folder = Open folder
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Where the application keeps its own files. These locations are fixed.
|
||||||
|
.configuration = Configuration
|
||||||
|
.plot-styles = Plot styles
|
||||||
|
.autosave-files = Autosave files
|
||||||
.selection-modes = Selection Modes
|
.selection-modes = Selection Modes
|
||||||
.use-shift-to-add-to-selection-pickadd = Use Shift to add to selection (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Use Shift to add to selection (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Press and drag draws a rectangle instead of a lasso (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Press and drag draws a rectangle instead of a lasso (PICKDRAG)
|
||||||
|
|||||||
@@ -1376,6 +1376,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Controla tanto el cuadro de selección visible como el área de clic.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Controla tanto el cuadro de selección visible como el área de clic.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Aplicaciones
|
||||||
|
.installed-plugins-and-their-sources = Complementos instalados y sus orígenes
|
||||||
|
.plugins-2 = Complementos…
|
||||||
|
.keyboard-shortcuts-2 = Atajos de teclado
|
||||||
|
.keyboard-shortcuts-3 = Atajos de teclado…
|
||||||
|
.command-aliases-2 = Alias de comandos
|
||||||
|
.command-aliases-3 = Alias de comandos…
|
||||||
|
.file-safety = Seguridad de archivos
|
||||||
|
.automatic-save = Guardado automático
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Minutos entre guardados de recuperación en un archivo .sv$; 0 lo desactiva (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Conservar una copia .bak al sobrescribir un dibujo (ISAVEBAK)
|
||||||
|
.plotting = Trazado
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Dispositivo de trazado, papel, escala y estilos de trazado
|
||||||
|
.plot-and-page-setup = Trazar y configurar página…
|
||||||
|
.fill-truetype-glyphs-textfill = Rellenar glifos TrueType (TEXTFILL)
|
||||||
|
.command-line = Línea de comandos
|
||||||
|
.prompt-lines = Líneas de solicitud
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Líneas de solicitud temporales sobre la ventana de comandos (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Tiempo de desvanecimiento del historial
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Cuánto permanecen visibles las líneas del historial; 0 las omite (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Al pulsar sobre objetos superpuestos se abre un selector
|
||||||
|
.block-edit = Edición de bloque
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Doble clic para editar el bloque in situ (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Doble clic en bloques con atributos para editarlos (ATTEDIT)
|
||||||
|
.user-preferences = Preferencias de usuario
|
||||||
|
.zoom = Zoom
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Invertir el zoom de la rueda del ratón (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Factor de zoom
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = Cuánto amplía un paso de la rueda (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Texto y acotación
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT edita un objeto y termina (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Las cotas continuadas heredan la capa y el estilo de la cota base (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = Prioridad de origen de QDIM
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = Desde qué puntos mide QDIM. También se ajusta dentro del comando.
|
||||||
|
.add-scales-automatically = Añadir escalas automáticamente
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Qué objetos anotativos adoptan una escala de anotación recién definida (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Unidades de longitud, ángulo e inserción
|
||||||
|
.drawing-units-2 = Unidades del dibujo…
|
||||||
|
.drafting = Dibujo
|
||||||
|
.drafting-rotation = Rotación de dibujo
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Gira la mira y la rejilla de forzado en el SCP activo (SNAPANG).
|
||||||
|
.polar-tracking-increment = Incremento de rastreo polar
|
||||||
|
.snap-and-grid = Forzado y rejilla
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Modos de forzado, espaciado de rejilla y referencia a objetos
|
||||||
|
.drafting-settings-2 = Parámetros de dibujo…
|
||||||
|
.modeling-3d = Modelado 3D
|
||||||
|
.display-tools = Herramientas de visualización
|
||||||
|
.show-the-navigation-cube-navvcube = Mostrar el cubo de navegación (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = Mostrar el icono SCP (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = Situar el icono SCP en el origen (UCSICON ORigin)
|
||||||
|
.display-resolution = Resolución de visualización
|
||||||
|
.applies-to-the-current-drawing = se aplica al dibujo actual
|
||||||
|
.isolines-per-surface = Isolíneas por superficie
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Mostrar aristas de silueta en sólidos (DISPSILH)
|
||||||
|
.surface-density-u = Densidad de superficie U
|
||||||
|
.surface-density-v = Densidad de superficie V
|
||||||
|
.surface-type = Tipo de superficie
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Registrar el historial de sólidos compuestos (SOLIDHIST)
|
||||||
|
.show-solid-history = Mostrar el historial de sólidos
|
||||||
|
.not-available = No disponible
|
||||||
|
.open-folder = Abrir carpeta
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Dónde guarda la aplicación sus propios archivos. Estas ubicaciones son fijas.
|
||||||
|
.configuration = Configuración
|
||||||
|
.plot-styles = Estilos de trazado
|
||||||
|
.autosave-files = Archivos de guardado automático
|
||||||
.selection-modes = Modos de selección
|
.selection-modes = Modos de selección
|
||||||
.use-shift-to-add-to-selection-pickadd = Usar Mayús para añadir a la selección (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Usar Mayús para añadir a la selección (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Arrastrar dibuja un rectángulo en lugar de un lazo (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Arrastrar dibuja un rectángulo en lugar de un lazo (PICKDRAG)
|
||||||
|
|||||||
@@ -1375,6 +1375,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Säätää sekä näkyvää valintaruutua että napsautusaluetta.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Säätää sekä näkyvää valintaruutua että napsautusaluetta.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Sovellukset
|
||||||
|
.installed-plugins-and-their-sources = Asennetut laajennukset ja niiden lähteet
|
||||||
|
.plugins-2 = Laajennukset…
|
||||||
|
.keyboard-shortcuts-2 = Pikanäppäimet
|
||||||
|
.keyboard-shortcuts-3 = Pikanäppäimet…
|
||||||
|
.command-aliases-2 = Komentojen aliakset
|
||||||
|
.command-aliases-3 = Komentojen aliakset…
|
||||||
|
.file-safety = Tiedostoturvallisuus
|
||||||
|
.automatic-save = Automaattinen tallennus
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Minuutit palautustallennusten välillä .sv$-tiedostoon; 0 poistaa käytöstä (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Säilytä .bak-kopio piirustusta korvattaessa (ISAVEBAK)
|
||||||
|
.plotting = Tulostus
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Tulostuslaite, paperi, mittakaava ja tulostustyylit
|
||||||
|
.plot-and-page-setup = Tulosta ja sivun asetukset…
|
||||||
|
.fill-truetype-glyphs-textfill = Täytä TrueType-merkit (TEXTFILL)
|
||||||
|
.command-line = Komentorivi
|
||||||
|
.prompt-lines = Kehoterivit
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Väliaikaiset kehoterivit komentoikkunan yläpuolella (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Historian häivytysaika
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Kuinka kauan historiarivit pysyvät näkyvissä; 0 ohittaa ne (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Päällekkäisten objektien napsauttaminen avaa valitsimen
|
||||||
|
.block-edit = Lohkon muokkaus
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Kaksoisnapsautus muokkaa lohkoa paikallaan (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Kaksoisnapsautus määritteellisiin lohkoihin muokkaa määritteitä (ATTEDIT)
|
||||||
|
.user-preferences = Käyttäjäasetukset
|
||||||
|
.zoom = Zoomaus
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Käännä hiiren rullan zoomaus (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Zoomauskerroin
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = Kuinka paljon yksi rullan pykälä zoomaa (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Teksti ja mitoitus
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT muokkaa yhden objektin ja päättyy (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Jatketut mitat perivät perusmitan tason ja tyylin (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = QDIM-lähtöpisteen prioriteetti
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = Mistä pisteistä QDIM mittaa. Asetettavissa myös komennon sisällä.
|
||||||
|
.add-scales-automatically = Lisää mittakaavat automaattisesti
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Mitkä selittävät objektit omaksuvat juuri asetetun selitemittakaavan (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Pituus-, kulma- ja lisäysyksiköt
|
||||||
|
.drawing-units-2 = Piirustuksen yksiköt…
|
||||||
|
.drafting = Piirto
|
||||||
|
.drafting-rotation = Piirron kierto
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Kiertää hiusristikkoa ja tarttumaruudukkoa aktiivisessa UCS:ssä (SNAPANG).
|
||||||
|
.polar-tracking-increment = Polaarisen seurannan askel
|
||||||
|
.snap-and-grid = Tartunta ja ruudukko
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Tarttumatilat, ruudukkoväli ja objektitartunta
|
||||||
|
.drafting-settings-2 = Piirtoasetukset…
|
||||||
|
.modeling-3d = 3D-mallinnus
|
||||||
|
.display-tools = Näyttötyökalut
|
||||||
|
.show-the-navigation-cube-navvcube = Näytä navigointikuutio (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = Näytä UCS-kuvake (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = Sijoita UCS-kuvake origoon (UCSICON ORigin)
|
||||||
|
.display-resolution = Näytön tarkkuus
|
||||||
|
.applies-to-the-current-drawing = koskee nykyistä piirustusta
|
||||||
|
.isolines-per-surface = Isoviivoja pintaa kohti
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Näytä siluettireunat kappaleissa (DISPSILH)
|
||||||
|
.surface-density-u = Pinnan tiheys U
|
||||||
|
.surface-density-v = Pinnan tiheys V
|
||||||
|
.surface-type = Pinnan tyyppi
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Tallenna koostekappaleiden historia (SOLIDHIST)
|
||||||
|
.show-solid-history = Näytä kappalehistoria
|
||||||
|
.not-available = Ei käytettävissä
|
||||||
|
.open-folder = Avaa kansio
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Missä sovellus säilyttää omat tiedostonsa. Nämä sijainnit ovat kiinteät.
|
||||||
|
.configuration = Määritykset
|
||||||
|
.plot-styles = Tulostustyylit
|
||||||
|
.autosave-files = Automaattitallennuksen tiedostot
|
||||||
.selection-modes = Valintatilat
|
.selection-modes = Valintatilat
|
||||||
.use-shift-to-add-to-selection-pickadd = Käytä Vaihto-näppäintä valintaan lisäämiseen (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Käytä Vaihto-näppäintä valintaan lisäämiseen (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Painallus ja veto piirtää suorakulmion lasson sijaan (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Painallus ja veto piirtää suorakulmion lasson sijaan (PICKDRAG)
|
||||||
|
|||||||
@@ -1376,6 +1376,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Contrôle à la fois la case de sélection visible et la zone de clic.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Contrôle à la fois la case de sélection visible et la zone de clic.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Applications
|
||||||
|
.installed-plugins-and-their-sources = Plug-ins installés et leurs sources
|
||||||
|
.plugins-2 = Plug-ins…
|
||||||
|
.keyboard-shortcuts-2 = Raccourcis clavier
|
||||||
|
.keyboard-shortcuts-3 = Raccourcis clavier…
|
||||||
|
.command-aliases-2 = Alias de commandes
|
||||||
|
.command-aliases-3 = Alias de commandes…
|
||||||
|
.file-safety = Sécurité des fichiers
|
||||||
|
.automatic-save = Enregistrement automatique
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Minutes entre les enregistrements de récupération dans un fichier .sv$ ; 0 le désactive (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Conserver une copie .bak lors de l'écrasement d'un dessin (ISAVEBAK)
|
||||||
|
.plotting = Traçage
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Périphérique de traçage, papier, échelle et styles de tracé
|
||||||
|
.plot-and-page-setup = Tracer et mise en page…
|
||||||
|
.fill-truetype-glyphs-textfill = Remplir les glyphes TrueType (TEXTFILL)
|
||||||
|
.command-line = Ligne de commande
|
||||||
|
.prompt-lines = Lignes d'invite
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Lignes d'invite temporaires au-dessus de la fenêtre de commande (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Durée d'estompage de l'historique
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Combien de temps les lignes d'historique restent visibles ; 0 les ignore (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Cliquer sur des objets superposés ouvre un sélecteur
|
||||||
|
.block-edit = Édition de bloc
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Double-clic pour modifier le bloc sur place (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Double-clic sur les blocs avec attributs pour les modifier (ATTEDIT)
|
||||||
|
.user-preferences = Préférences utilisateur
|
||||||
|
.zoom = Zoom
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Inverser le zoom à la molette (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Facteur de zoom
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = De combien zoome un cran de molette (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Texte et cotation
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT modifie un objet puis se termine (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Les cotes continues héritent du calque et du style de la cote de base (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = Priorité d'origine QDIM
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = Depuis quels points QDIM mesure. Réglable aussi dans la commande.
|
||||||
|
.add-scales-automatically = Ajouter les échelles automatiquement
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Quels objets annotatifs adoptent une nouvelle échelle d'annotation (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Unités de longueur, d'angle et d'insertion
|
||||||
|
.drawing-units-2 = Unités du dessin…
|
||||||
|
.drafting = Dessin
|
||||||
|
.drafting-rotation = Rotation du dessin
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Fait pivoter le réticule et la grille d'accrochage dans le SCU actif (SNAPANG).
|
||||||
|
.polar-tracking-increment = Incrément de repérage polaire
|
||||||
|
.snap-and-grid = Accrochage et grille
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Modes d'accrochage, pas de la grille et accrochage aux objets
|
||||||
|
.drafting-settings-2 = Paramètres de dessin…
|
||||||
|
.modeling-3d = Modélisation 3D
|
||||||
|
.display-tools = Outils d'affichage
|
||||||
|
.show-the-navigation-cube-navvcube = Afficher le cube de navigation (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = Afficher l'icône SCU (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = Placer l'icône SCU à l'origine (UCSICON ORigin)
|
||||||
|
.display-resolution = Résolution d'affichage
|
||||||
|
.applies-to-the-current-drawing = s'applique au dessin en cours
|
||||||
|
.isolines-per-surface = Isolignes par surface
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Afficher les arêtes de silhouette sur les solides (DISPSILH)
|
||||||
|
.surface-density-u = Densité de surface U
|
||||||
|
.surface-density-v = Densité de surface V
|
||||||
|
.surface-type = Type de surface
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Enregistrer l'historique des solides composés (SOLIDHIST)
|
||||||
|
.show-solid-history = Afficher l'historique des solides
|
||||||
|
.not-available = Non disponible
|
||||||
|
.open-folder = Ouvrir le dossier
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Où l'application range ses propres fichiers. Ces emplacements sont fixes.
|
||||||
|
.configuration = Configuration
|
||||||
|
.plot-styles = Styles de tracé
|
||||||
|
.autosave-files = Fichiers d'enregistrement automatique
|
||||||
.selection-modes = Modes de sélection
|
.selection-modes = Modes de sélection
|
||||||
.use-shift-to-add-to-selection-pickadd = Utiliser Maj pour ajouter à la sélection (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Utiliser Maj pour ajouter à la sélection (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Le cliquer-glisser trace un rectangle au lieu d'un lasso (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Le cliquer-glisser trace un rectangle au lieu d'un lasso (PICKDRAG)
|
||||||
|
|||||||
@@ -1364,6 +1364,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = दिखने वाले चयन बॉक्स और क्लिक क्षेत्र दोनों को नियंत्रित करता है।
|
.controls-both-the-visible-selection-box-and-the-click-aperture = दिखने वाले चयन बॉक्स और क्लिक क्षेत्र दोनों को नियंत्रित करता है।
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = अनुप्रयोग
|
||||||
|
.installed-plugins-and-their-sources = स्थापित प्लगइन और उनके स्रोत
|
||||||
|
.plugins-2 = प्लगइन…
|
||||||
|
.keyboard-shortcuts-2 = कीबोर्ड शॉर्टकट
|
||||||
|
.keyboard-shortcuts-3 = कीबोर्ड शॉर्टकट…
|
||||||
|
.command-aliases-2 = कमांड उपनाम
|
||||||
|
.command-aliases-3 = कमांड उपनाम…
|
||||||
|
.file-safety = फ़ाइल सुरक्षा
|
||||||
|
.automatic-save = स्वतः सहेजना
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = .sv$ फ़ाइल में पुनर्प्राप्ति सहेजने के बीच मिनट; 0 इसे बंद करता है (SAVETIME)।
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = ड्रॉइंग को अधिलेखित करते समय .bak प्रति रखें (ISAVEBAK)
|
||||||
|
.plotting = प्लॉटिंग
|
||||||
|
.plot-device-paper-scale-and-plot-styles = प्लॉट डिवाइस, कागज़, स्केल और प्लॉट शैलियाँ
|
||||||
|
.plot-and-page-setup = प्लॉट और पेज सेटअप…
|
||||||
|
.fill-truetype-glyphs-textfill = TrueType ग्लिफ़ भरें (TEXTFILL)
|
||||||
|
.command-line = कमांड लाइन
|
||||||
|
.prompt-lines = प्रॉम्प्ट पंक्तियाँ
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = कमांड विंडो के ऊपर अस्थायी प्रॉम्प्ट पंक्तियाँ (CLIPROMPTLINES)।
|
||||||
|
.history-fade-time = इतिहास फ़ेड समय
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = इतिहास पंक्तियाँ कितनी देर दिखती हैं; 0 उन्हें छोड़ देता है (COMMANDLINEFADETIME)।
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = अतिव्यापी ऑब्जेक्ट पर क्लिक करने से चयनकर्ता खुलता है
|
||||||
|
.block-edit = ब्लॉक संपादन
|
||||||
|
.double-click-to-edit-block-in-place-refedit = ब्लॉक को यथास्थान संपादित करने के लिए डबल-क्लिक करें (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = विशेषता वाले ब्लॉक पर डबल-क्लिक कर विशेषताएँ संपादित करें (ATTEDIT)
|
||||||
|
.user-preferences = उपयोगकर्ता प्राथमिकताएँ
|
||||||
|
.zoom = ज़ूम
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = माउस व्हील ज़ूम उलटें (ZOOMWHEEL)
|
||||||
|
.zoom-factor = ज़ूम गुणक
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = व्हील का एक पायदान कितना ज़ूम करता है (ZOOMFACTOR)।
|
||||||
|
.text-and-dimensions = पाठ और आयाम
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT एक ऑब्जेक्ट संपादित कर समाप्त होता है (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = निरंतर आयाम आधार आयाम की लेयर और शैली प्राप्त करते हैं (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = QDIM मूल प्राथमिकता
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = QDIM किन बिंदुओं से मापता है। कमांड के भीतर भी सेट किया जा सकता है।
|
||||||
|
.add-scales-automatically = स्केल स्वतः जोड़ें
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = कौन-से एनोटेटिव ऑब्जेक्ट नया एनोटेशन स्केल लेते हैं (ANNOAUTOSCALE)।
|
||||||
|
.length-angle-and-insertion-units = लंबाई, कोण और सम्मिलन इकाइयाँ
|
||||||
|
.drawing-units-2 = ड्रॉइंग इकाइयाँ…
|
||||||
|
.drafting = ड्राफ्टिंग
|
||||||
|
.drafting-rotation = ड्राफ्टिंग घूर्णन
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = सक्रिय UCS में क्रॉसहेयर और स्नैप ग्रिड घुमाता है (SNAPANG)।
|
||||||
|
.polar-tracking-increment = ध्रुवीय ट्रैकिंग वृद्धि
|
||||||
|
.snap-and-grid = स्नैप और ग्रिड
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = स्नैप मोड, ग्रिड अंतराल और ऑब्जेक्ट स्नैप
|
||||||
|
.drafting-settings-2 = ड्राफ्टिंग सेटिंग्स…
|
||||||
|
.modeling-3d = 3D मॉडलिंग
|
||||||
|
.display-tools = प्रदर्शन उपकरण
|
||||||
|
.show-the-navigation-cube-navvcube = नेविगेशन क्यूब दिखाएँ (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = UCS आइकन दिखाएँ (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = UCS आइकन मूल बिंदु पर रखें (UCSICON ORigin)
|
||||||
|
.display-resolution = प्रदर्शन विभेदन
|
||||||
|
.applies-to-the-current-drawing = वर्तमान ड्रॉइंग पर लागू
|
||||||
|
.isolines-per-surface = प्रति सतह आइसोलाइन
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = ठोसों पर सिल्हूट किनारे दिखाएँ (DISPSILH)
|
||||||
|
.surface-density-u = सतह घनत्व U
|
||||||
|
.surface-density-v = सतह घनत्व V
|
||||||
|
.surface-type = सतह प्रकार
|
||||||
|
.record-the-history-of-composite-solids-solidhist = संयुक्त ठोसों का इतिहास रिकॉर्ड करें (SOLIDHIST)
|
||||||
|
.show-solid-history = ठोस इतिहास दिखाएँ
|
||||||
|
.not-available = उपलब्ध नहीं
|
||||||
|
.open-folder = फ़ोल्डर खोलें
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = अनुप्रयोग अपनी फ़ाइलें कहाँ रखता है। ये स्थान निश्चित हैं।
|
||||||
|
.configuration = विन्यास
|
||||||
|
.plot-styles = प्लॉट शैलियाँ
|
||||||
|
.autosave-files = स्वतः सहेजी फ़ाइलें
|
||||||
.selection-modes = चयन मोड
|
.selection-modes = चयन मोड
|
||||||
.use-shift-to-add-to-selection-pickadd = चयन में जोड़ने के लिए Shift का उपयोग करें (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = चयन में जोड़ने के लिए Shift का उपयोग करें (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = दबाकर खींचने पर लैस्सो के बजाय आयत बनता है (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = दबाकर खींचने पर लैस्सो के बजाय आयत बनता है (PICKDRAG)
|
||||||
|
|||||||
@@ -1375,6 +1375,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = A látható kijelölőnégyzetet és a kattintási területet is szabályozza.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = A látható kijelölőnégyzetet és a kattintási területet is szabályozza.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Alkalmazások
|
||||||
|
.installed-plugins-and-their-sources = Telepített bővítmények és forrásaik
|
||||||
|
.plugins-2 = Bővítmények…
|
||||||
|
.keyboard-shortcuts-2 = Billentyűparancsok
|
||||||
|
.keyboard-shortcuts-3 = Billentyűparancsok…
|
||||||
|
.command-aliases-2 = Parancs-álnevek
|
||||||
|
.command-aliases-3 = Parancs-álnevek…
|
||||||
|
.file-safety = Fájlbiztonság
|
||||||
|
.automatic-save = Automatikus mentés
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Percek a .sv$ helyreállítási mentések között; a 0 kikapcsolja (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Rajz felülírásakor .bak másolat megőrzése (ISAVEBAK)
|
||||||
|
.plotting = Nyomtatás
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Nyomtatóeszköz, papír, méretarány és nyomtatási stílusok
|
||||||
|
.plot-and-page-setup = Nyomtatás és oldalbeállítás…
|
||||||
|
.fill-truetype-glyphs-textfill = TrueType karakterek kitöltése (TEXTFILL)
|
||||||
|
.command-line = Parancssor
|
||||||
|
.prompt-lines = Kérdéssorok
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Ideiglenes kérdéssorok a parancsablak fölött (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Előzmények elhalványulási ideje
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Meddig maradnak láthatók az előzménysorok; a 0 kihagyja őket (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Átfedő objektumokra kattintva választó nyílik
|
||||||
|
.block-edit = Blokkszerkesztés
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Dupla kattintás a blokk helyben szerkesztéséhez (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Dupla kattintás az attribútumos blokkok attribútumainak szerkesztéséhez (ATTEDIT)
|
||||||
|
.user-preferences = Felhasználói beállítások
|
||||||
|
.zoom = Nagyítás
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Egérgörgős nagyítás megfordítása (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Nagyítási tényező
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = Mennyit nagyít egy görgőkattanás (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Szöveg és méretezés
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = A TEXTEDIT egy objektumot szerkeszt, majd befejezi (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = A folytatott méretek öröklik az alapméret fóliáját és stílusát (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = QDIM kiindulási prioritás
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = Mely pontoktól mér a QDIM. A parancson belül is beállítható.
|
||||||
|
.add-scales-automatically = Méretarányok automatikus hozzáadása
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Mely annotatív objektumok veszik át az újonnan beállított feliratozási méretarányt (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Hossz-, szög- és beszúrási egységek
|
||||||
|
.drawing-units-2 = Rajzegységek…
|
||||||
|
.drafting = Rajzolás
|
||||||
|
.drafting-rotation = Rajzolási elforgatás
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Elforgatja a szálkeresztet és az illesztőrácsot az aktív FKR-ben (SNAPANG).
|
||||||
|
.polar-tracking-increment = Poláris követési lépés
|
||||||
|
.snap-and-grid = Illesztés és rács
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Illesztési módok, rácstávolság és tárgyraszter
|
||||||
|
.drafting-settings-2 = Rajzolási beállítások…
|
||||||
|
.modeling-3d = 3D modellezés
|
||||||
|
.display-tools = Megjelenítési eszközök
|
||||||
|
.show-the-navigation-cube-navvcube = Navigációs kocka megjelenítése (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = FKR ikon megjelenítése (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = FKR ikon elhelyezése az origóban (UCSICON ORigin)
|
||||||
|
.display-resolution = Megjelenítési felbontás
|
||||||
|
.applies-to-the-current-drawing = az aktuális rajzra vonatkozik
|
||||||
|
.isolines-per-surface = Izovonalak felületenként
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Sziluettélek megjelenítése testeken (DISPSILH)
|
||||||
|
.surface-density-u = Felületsűrűség U
|
||||||
|
.surface-density-v = Felületsűrűség V
|
||||||
|
.surface-type = Felülettípus
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Összetett testek előzményeinek rögzítése (SOLIDHIST)
|
||||||
|
.show-solid-history = Testelőzmények megjelenítése
|
||||||
|
.not-available = Nem érhető el
|
||||||
|
.open-folder = Mappa megnyitása
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Hol tárolja az alkalmazás a saját fájljait. Ezek a helyek rögzítettek.
|
||||||
|
.configuration = Konfiguráció
|
||||||
|
.plot-styles = Nyomtatási stílusok
|
||||||
|
.autosave-files = Automatikus mentés fájljai
|
||||||
.selection-modes = Kijelölési módok
|
.selection-modes = Kijelölési módok
|
||||||
.use-shift-to-add-to-selection-pickadd = Shift használata a kijelöléshez adáshoz (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Shift használata a kijelöléshez adáshoz (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = A lenyomva húzás téglalapot rajzol lasszó helyett (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = A lenyomva húzás téglalapot rajzol lasszó helyett (PICKDRAG)
|
||||||
|
|||||||
@@ -1375,6 +1375,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Controlla sia il riquadro di selezione visibile sia l’area di clic.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Controlla sia il riquadro di selezione visibile sia l’area di clic.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Applicazioni
|
||||||
|
.installed-plugins-and-their-sources = Plug-in installati e relative origini
|
||||||
|
.plugins-2 = Plug-in…
|
||||||
|
.keyboard-shortcuts-2 = Scorciatoie da tastiera
|
||||||
|
.keyboard-shortcuts-3 = Scorciatoie da tastiera…
|
||||||
|
.command-aliases-2 = Alias dei comandi
|
||||||
|
.command-aliases-3 = Alias dei comandi…
|
||||||
|
.file-safety = Sicurezza dei file
|
||||||
|
.automatic-save = Salvataggio automatico
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Minuti tra i salvataggi di ripristino in un file .sv$; 0 lo disattiva (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Conserva una copia .bak quando si sovrascrive un disegno (ISAVEBAK)
|
||||||
|
.plotting = Stampa
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Dispositivo di stampa, foglio, scala e stili di stampa
|
||||||
|
.plot-and-page-setup = Stampa e imposta pagina…
|
||||||
|
.fill-truetype-glyphs-textfill = Riempi i glifi TrueType (TEXTFILL)
|
||||||
|
.command-line = Riga di comando
|
||||||
|
.prompt-lines = Righe di richiesta
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Righe di richiesta temporanee sopra la finestra dei comandi (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Tempo di dissolvenza della cronologia
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Per quanto restano visibili le righe di cronologia; 0 le salta (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Fare clic su oggetti sovrapposti apre un selettore
|
||||||
|
.block-edit = Modifica blocco
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Doppio clic per modificare il blocco sul posto (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Doppio clic sui blocchi con attributi per modificarli (ATTEDIT)
|
||||||
|
.user-preferences = Preferenze utente
|
||||||
|
.zoom = Zoom
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Inverti lo zoom con la rotellina (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Fattore di zoom
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = Di quanto ingrandisce uno scatto della rotellina (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Testo e quote
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT modifica un oggetto e termina (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Le quote continue ereditano layer e stile dalla quota di base (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = Priorità di origine QDIM
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = Da quali punti misura QDIM. Impostabile anche dentro il comando.
|
||||||
|
.add-scales-automatically = Aggiungi scale automaticamente
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Quali oggetti annotativi adottano una scala di annotazione appena impostata (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Unità di lunghezza, angolo e inserimento
|
||||||
|
.drawing-units-2 = Unità del disegno…
|
||||||
|
.drafting = Disegno
|
||||||
|
.drafting-rotation = Rotazione di disegno
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Ruota il mirino e la griglia di snap nell'UCS attivo (SNAPANG).
|
||||||
|
.polar-tracking-increment = Incremento di puntamento polare
|
||||||
|
.snap-and-grid = Snap e griglia
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Modalità di snap, passo della griglia e snap agli oggetti
|
||||||
|
.drafting-settings-2 = Impostazioni disegno…
|
||||||
|
.modeling-3d = Modellazione 3D
|
||||||
|
.display-tools = Strumenti di visualizzazione
|
||||||
|
.show-the-navigation-cube-navvcube = Mostra il cubo di navigazione (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = Mostra l'icona UCS (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = Colloca l'icona UCS nell'origine (UCSICON ORigin)
|
||||||
|
.display-resolution = Risoluzione di visualizzazione
|
||||||
|
.applies-to-the-current-drawing = si applica al disegno corrente
|
||||||
|
.isolines-per-surface = Isolinee per superficie
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Mostra i bordi di silhouette sui solidi (DISPSILH)
|
||||||
|
.surface-density-u = Densità superficie U
|
||||||
|
.surface-density-v = Densità superficie V
|
||||||
|
.surface-type = Tipo di superficie
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Registra la cronologia dei solidi composti (SOLIDHIST)
|
||||||
|
.show-solid-history = Mostra la cronologia dei solidi
|
||||||
|
.not-available = Non disponibile
|
||||||
|
.open-folder = Apri cartella
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Dove l'applicazione tiene i propri file. Queste posizioni sono fisse.
|
||||||
|
.configuration = Configurazione
|
||||||
|
.plot-styles = Stili di stampa
|
||||||
|
.autosave-files = File di salvataggio automatico
|
||||||
.selection-modes = Modalità di selezione
|
.selection-modes = Modalità di selezione
|
||||||
.use-shift-to-add-to-selection-pickadd = Usa Maiusc per aggiungere alla selezione (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Usa Maiusc per aggiungere alla selezione (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Premi e trascina disegna un rettangolo invece di un lazo (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Premi e trascina disegna un rettangolo invece di un lazo (PICKDRAG)
|
||||||
|
|||||||
@@ -1366,6 +1366,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = 表示される選択ボックスとクリック範囲の両方を制御します。
|
.controls-both-the-visible-selection-box-and-the-click-aperture = 表示される選択ボックスとクリック範囲の両方を制御します。
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = アプリケーション
|
||||||
|
.installed-plugins-and-their-sources = インストール済みプラグインとその提供元
|
||||||
|
.plugins-2 = プラグイン…
|
||||||
|
.keyboard-shortcuts-2 = キーボード ショートカット
|
||||||
|
.keyboard-shortcuts-3 = キーボード ショートカット…
|
||||||
|
.command-aliases-2 = コマンド エイリアス
|
||||||
|
.command-aliases-3 = コマンド エイリアス…
|
||||||
|
.file-safety = ファイルの安全性
|
||||||
|
.automatic-save = 自動保存
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = .sv$ ファイルへの復旧保存の間隔 (分)。0 で無効 (SAVETIME)。
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = 図面を上書きするとき .bak のコピーを残す (ISAVEBAK)
|
||||||
|
.plotting = 印刷
|
||||||
|
.plot-device-paper-scale-and-plot-styles = 印刷デバイス、用紙、尺度、印刷スタイル
|
||||||
|
.plot-and-page-setup = 印刷とページ設定…
|
||||||
|
.fill-truetype-glyphs-textfill = TrueType グリフを塗りつぶす (TEXTFILL)
|
||||||
|
.command-line = コマンド ライン
|
||||||
|
.prompt-lines = プロンプト行
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = コマンド ウィンドウの上に表示する一時プロンプト行 (CLIPROMPTLINES)。
|
||||||
|
.history-fade-time = 履歴のフェード時間
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = 履歴行が表示され続ける時間。0 で表示しない (COMMANDLINEFADETIME)。
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = 重なったオブジェクトをクリックすると選択リストが開く
|
||||||
|
.block-edit = ブロック編集
|
||||||
|
.double-click-to-edit-block-in-place-refedit = ダブルクリックでブロックをその場で編集 (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = 属性付きブロックをダブルクリックして属性を編集 (ATTEDIT)
|
||||||
|
.user-preferences = ユーザー設定
|
||||||
|
.zoom = ズーム
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = マウス ホイールのズーム方向を反転 (ZOOMWHEEL)
|
||||||
|
.zoom-factor = ズーム倍率
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = ホイール 1 ノッチのズーム量 (ZOOMFACTOR)。
|
||||||
|
.text-and-dimensions = 文字と寸法
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT は 1 つのオブジェクトを編集して終了 (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = 連続寸法は基準寸法の画層とスタイルを引き継ぐ (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = QDIM の起点優先
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = QDIM が測定する点。コマンド内でも設定できます。
|
||||||
|
.add-scales-automatically = 尺度を自動的に追加
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = 新しく設定した注釈尺度を受け取る注釈オブジェクト (ANNOAUTOSCALE)。
|
||||||
|
.length-angle-and-insertion-units = 長さ、角度、挿入の単位
|
||||||
|
.drawing-units-2 = 図面単位…
|
||||||
|
.drafting = 作図
|
||||||
|
.drafting-rotation = 作図の回転
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = アクティブな UCS でクロスヘアとスナップ グリッドを回転します (SNAPANG)。
|
||||||
|
.polar-tracking-increment = 極トラッキングの増分
|
||||||
|
.snap-and-grid = スナップとグリッド
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = スナップ モード、グリッド間隔、オブジェクト スナップ
|
||||||
|
.drafting-settings-2 = 作図設定…
|
||||||
|
.modeling-3d = 3D モデリング
|
||||||
|
.display-tools = 表示ツール
|
||||||
|
.show-the-navigation-cube-navvcube = ナビゲーション キューブを表示 (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = UCS アイコンを表示 (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = UCS アイコンを原点に配置 (UCSICON ORigin)
|
||||||
|
.display-resolution = 表示解像度
|
||||||
|
.applies-to-the-current-drawing = 現在の図面に適用されます
|
||||||
|
.isolines-per-surface = 面あたりのアイソライン
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = ソリッドのシルエット エッジを表示 (DISPSILH)
|
||||||
|
.surface-density-u = サーフェス密度 U
|
||||||
|
.surface-density-v = サーフェス密度 V
|
||||||
|
.surface-type = サーフェス タイプ
|
||||||
|
.record-the-history-of-composite-solids-solidhist = 複合ソリッドの履歴を記録 (SOLIDHIST)
|
||||||
|
.show-solid-history = ソリッドの履歴を表示
|
||||||
|
.not-available = 利用できません
|
||||||
|
.open-folder = フォルダーを開く
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = アプリケーションが自身のファイルを置く場所です。これらの場所は固定です。
|
||||||
|
.configuration = 構成
|
||||||
|
.plot-styles = 印刷スタイル
|
||||||
|
.autosave-files = 自動保存ファイル
|
||||||
.selection-modes = 選択モード
|
.selection-modes = 選択モード
|
||||||
.use-shift-to-add-to-selection-pickadd = Shift キーで選択に追加する (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Shift キーで選択に追加する (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = ドラッグで投げ縄ではなく矩形を描く (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = ドラッグで投げ縄ではなく矩形を描く (PICKDRAG)
|
||||||
|
|||||||
@@ -1375,6 +1375,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = 표시되는 선택 상자와 클릭 영역을 모두 제어합니다.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = 표시되는 선택 상자와 클릭 영역을 모두 제어합니다.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = 응용 프로그램
|
||||||
|
.installed-plugins-and-their-sources = 설치된 플러그인 및 해당 소스
|
||||||
|
.plugins-2 = 플러그인…
|
||||||
|
.keyboard-shortcuts-2 = 키보드 단축키
|
||||||
|
.keyboard-shortcuts-3 = 키보드 단축키…
|
||||||
|
.command-aliases-2 = 명령 별칭
|
||||||
|
.command-aliases-3 = 명령 별칭…
|
||||||
|
.file-safety = 파일 안전
|
||||||
|
.automatic-save = 자동 저장
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = .sv$ 파일로 복구 저장하는 간격(분). 0이면 해제 (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = 도면을 덮어쓸 때 .bak 사본 유지 (ISAVEBAK)
|
||||||
|
.plotting = 플롯
|
||||||
|
.plot-device-paper-scale-and-plot-styles = 플롯 장치, 용지, 축척 및 플롯 스타일
|
||||||
|
.plot-and-page-setup = 플롯 및 페이지 설정…
|
||||||
|
.fill-truetype-glyphs-textfill = TrueType 글리프 채우기 (TEXTFILL)
|
||||||
|
.command-line = 명령행
|
||||||
|
.prompt-lines = 프롬프트 행
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = 명령 창 위에 표시할 임시 프롬프트 행 (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = 히스토리 페이드 시간
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = 히스토리 행이 표시되는 시간. 0이면 건너뜀 (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = 겹친 객체를 클릭하면 선택기가 열림
|
||||||
|
.block-edit = 블록 편집
|
||||||
|
.double-click-to-edit-block-in-place-refedit = 두 번 클릭하여 블록을 제자리에서 편집 (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = 속성이 있는 블록을 두 번 클릭하여 속성 편집 (ATTEDIT)
|
||||||
|
.user-preferences = 사용자 환경설정
|
||||||
|
.zoom = 줌
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = 마우스 휠 줌 방향 반전 (ZOOMWHEEL)
|
||||||
|
.zoom-factor = 줌 배율
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = 휠 한 칸이 확대하는 정도 (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = 문자 및 치수
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT는 객체 하나를 편집하고 종료 (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = 연속 치수가 기준 치수의 도면층과 스타일을 상속 (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = QDIM 원점 우선순위
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = QDIM이 측정하는 점. 명령 안에서도 설정할 수 있습니다.
|
||||||
|
.add-scales-automatically = 축척 자동 추가
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = 새로 설정한 주석 축척을 적용받을 주석 객체 (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = 길이, 각도 및 삽입 단위
|
||||||
|
.drawing-units-2 = 도면 단위…
|
||||||
|
.drafting = 제도
|
||||||
|
.drafting-rotation = 제도 회전
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = 활성 UCS에서 십자선과 스냅 그리드를 회전합니다 (SNAPANG).
|
||||||
|
.polar-tracking-increment = 극좌표 추적 증분
|
||||||
|
.snap-and-grid = 스냅 및 그리드
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = 스냅 모드, 그리드 간격 및 객체 스냅
|
||||||
|
.drafting-settings-2 = 제도 설정…
|
||||||
|
.modeling-3d = 3D 모델링
|
||||||
|
.display-tools = 표시 도구
|
||||||
|
.show-the-navigation-cube-navvcube = 탐색 큐브 표시 (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = UCS 아이콘 표시 (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = UCS 아이콘을 원점에 배치 (UCSICON ORigin)
|
||||||
|
.display-resolution = 표시 해상도
|
||||||
|
.applies-to-the-current-drawing = 현재 도면에 적용됩니다
|
||||||
|
.isolines-per-surface = 곡면당 아이소라인
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = 솔리드의 실루엣 모서리 표시 (DISPSILH)
|
||||||
|
.surface-density-u = 곡면 밀도 U
|
||||||
|
.surface-density-v = 곡면 밀도 V
|
||||||
|
.surface-type = 곡면 유형
|
||||||
|
.record-the-history-of-composite-solids-solidhist = 복합 솔리드의 히스토리 기록 (SOLIDHIST)
|
||||||
|
.show-solid-history = 솔리드 히스토리 표시
|
||||||
|
.not-available = 사용할 수 없음
|
||||||
|
.open-folder = 폴더 열기
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = 응용 프로그램이 자체 파일을 두는 위치입니다. 이 위치는 고정입니다.
|
||||||
|
.configuration = 구성
|
||||||
|
.plot-styles = 플롯 스타일
|
||||||
|
.autosave-files = 자동 저장 파일
|
||||||
.selection-modes = 선택 모드
|
.selection-modes = 선택 모드
|
||||||
.use-shift-to-add-to-selection-pickadd = Shift 키로 선택에 추가 (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Shift 키로 선택에 추가 (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = 누른 채 끌면 올가미 대신 사각형을 그림 (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = 누른 채 끌면 올가미 대신 사각형을 그림 (PICKDRAG)
|
||||||
|
|||||||
@@ -1374,6 +1374,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Bepaalt zowel het zichtbare selectievak als het klikgebied.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Bepaalt zowel het zichtbare selectievak als het klikgebied.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Toepassingen
|
||||||
|
.installed-plugins-and-their-sources = Geïnstalleerde plug-ins en hun bronnen
|
||||||
|
.plugins-2 = Plug-ins…
|
||||||
|
.keyboard-shortcuts-2 = Sneltoetsen
|
||||||
|
.keyboard-shortcuts-3 = Sneltoetsen…
|
||||||
|
.command-aliases-2 = Opdrachtaliassen
|
||||||
|
.command-aliases-3 = Opdrachtaliassen…
|
||||||
|
.file-safety = Bestandsveiligheid
|
||||||
|
.automatic-save = Automatisch opslaan
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Minuten tussen herstelopslagen naar een .sv$-bestand; 0 schakelt het uit (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Een .bak-kopie bewaren bij het overschrijven van een tekening (ISAVEBAK)
|
||||||
|
.plotting = Plotten
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Plotapparaat, papier, schaal en plotstijlen
|
||||||
|
.plot-and-page-setup = Plotten en pagina-instelling…
|
||||||
|
.fill-truetype-glyphs-textfill = TrueType-glyphs vullen (TEXTFILL)
|
||||||
|
.command-line = Opdrachtregel
|
||||||
|
.prompt-lines = Promptregels
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Tijdelijke promptregels boven het opdrachtvenster (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Vervaagtijd van geschiedenis
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Hoe lang geschiedenisregels zichtbaar blijven; 0 slaat ze over (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Klikken op overlappende objecten opent een kiezer
|
||||||
|
.block-edit = Blok bewerken
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Dubbelklik om het blok ter plaatse te bewerken (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Dubbelklik op blokken met attributen om die te bewerken (ATTEDIT)
|
||||||
|
.user-preferences = Gebruikersvoorkeuren
|
||||||
|
.zoom = Zoomen
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Muiswielzoom omkeren (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Zoomfactor
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = Hoeveel één wielklik zoomt (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Tekst en maatvoering
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT bewerkt één object en stopt (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Doorlopende maten erven laag en stijl van de basismaat (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = QDIM-oorsprongprioriteit
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = Vanaf welke punten QDIM meet. Ook in te stellen binnen de opdracht.
|
||||||
|
.add-scales-automatically = Schalen automatisch toevoegen
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Welke annotatieve objecten een nieuw ingestelde annotatieschaal overnemen (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Lengte-, hoek- en invoegeenheden
|
||||||
|
.drawing-units-2 = Tekeneenheden…
|
||||||
|
.drafting = Tekenen
|
||||||
|
.drafting-rotation = Tekenrotatie
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Draait het draadkruis en het vangraster in het actieve BCS (SNAPANG).
|
||||||
|
.polar-tracking-increment = Polaire volgstap
|
||||||
|
.snap-and-grid = Vang en raster
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Vangmodi, rasterafstand en objectvang
|
||||||
|
.drafting-settings-2 = Tekeninstellingen…
|
||||||
|
.modeling-3d = 3D-modelleren
|
||||||
|
.display-tools = Weergavegereedschappen
|
||||||
|
.show-the-navigation-cube-navvcube = Navigatiekubus tonen (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = BCS-pictogram tonen (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = BCS-pictogram op de oorsprong plaatsen (UCSICON ORigin)
|
||||||
|
.display-resolution = Weergaveresolutie
|
||||||
|
.applies-to-the-current-drawing = geldt voor de huidige tekening
|
||||||
|
.isolines-per-surface = Isolijnen per oppervlak
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Silhouetranden op vaste lichamen tonen (DISPSILH)
|
||||||
|
.surface-density-u = Oppervlakdichtheid U
|
||||||
|
.surface-density-v = Oppervlakdichtheid V
|
||||||
|
.surface-type = Oppervlaktype
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Geschiedenis van samengestelde vaste lichamen vastleggen (SOLIDHIST)
|
||||||
|
.show-solid-history = Geschiedenis van vaste lichamen tonen
|
||||||
|
.not-available = Niet beschikbaar
|
||||||
|
.open-folder = Map openen
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Waar de toepassing haar eigen bestanden bewaart. Deze locaties liggen vast.
|
||||||
|
.configuration = Configuratie
|
||||||
|
.plot-styles = Plotstijlen
|
||||||
|
.autosave-files = Bestanden voor automatisch opslaan
|
||||||
.selection-modes = Selectiemodi
|
.selection-modes = Selectiemodi
|
||||||
.use-shift-to-add-to-selection-pickadd = Shift gebruiken om aan de selectie toe te voegen (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Shift gebruiken om aan de selectie toe te voegen (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Slepen tekent een rechthoek in plaats van een lasso (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Slepen tekent een rechthoek in plaats van een lasso (PICKDRAG)
|
||||||
|
|||||||
@@ -1375,6 +1375,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Steruje widocznym polem wyboru oraz obszarem kliknięcia.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Steruje widocznym polem wyboru oraz obszarem kliknięcia.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Aplikacje
|
||||||
|
.installed-plugins-and-their-sources = Zainstalowane wtyczki i ich źródła
|
||||||
|
.plugins-2 = Wtyczki…
|
||||||
|
.keyboard-shortcuts-2 = Skróty klawiszowe
|
||||||
|
.keyboard-shortcuts-3 = Skróty klawiszowe…
|
||||||
|
.command-aliases-2 = Aliasy poleceń
|
||||||
|
.command-aliases-3 = Aliasy poleceń…
|
||||||
|
.file-safety = Bezpieczeństwo plików
|
||||||
|
.automatic-save = Automatyczny zapis
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Minuty między zapisami odzyskiwania do pliku .sv$; 0 wyłącza (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Zachowaj kopię .bak przy nadpisywaniu rysunku (ISAVEBAK)
|
||||||
|
.plotting = Kreślenie
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Urządzenie kreślące, papier, skala i style wydruku
|
||||||
|
.plot-and-page-setup = Kreśl i ustawienia strony…
|
||||||
|
.fill-truetype-glyphs-textfill = Wypełniaj glify TrueType (TEXTFILL)
|
||||||
|
.command-line = Wiersz poleceń
|
||||||
|
.prompt-lines = Wiersze zachęty
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Tymczasowe wiersze zachęty nad oknem poleceń (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Czas zanikania historii
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Jak długo wiersze historii pozostają widoczne; 0 je pomija (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Kliknięcie nakładających się obiektów otwiera selektor
|
||||||
|
.block-edit = Edycja bloku
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Dwukrotne kliknięcie edytuje blok w miejscu (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Dwukrotne kliknięcie bloków z atrybutami edytuje atrybuty (ATTEDIT)
|
||||||
|
.user-preferences = Preferencje użytkownika
|
||||||
|
.zoom = Powiększenie
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Odwróć powiększanie kółkiem myszy (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Współczynnik powiększenia
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = O ile powiększa jedno kliknięcie kółka (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Tekst i wymiary
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT edytuje jeden obiekt i kończy (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Wymiary kontynuowane dziedziczą warstwę i styl wymiaru bazowego (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = Priorytet początku QDIM
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = Od których punktów mierzy QDIM. Ustawiane też wewnątrz polecenia.
|
||||||
|
.add-scales-automatically = Dodawaj skale automatycznie
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Które obiekty adnotacyjne przyjmują nowo ustawioną skalę adnotacji (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Jednostki długości, kąta i wstawiania
|
||||||
|
.drawing-units-2 = Jednostki rysunku…
|
||||||
|
.drafting = Kreślenie ręczne
|
||||||
|
.drafting-rotation = Obrót rysowania
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Obraca celownik i siatkę przyciągania w aktywnym LUW (SNAPANG).
|
||||||
|
.polar-tracking-increment = Przyrost śledzenia biegunowego
|
||||||
|
.snap-and-grid = Przyciąganie i siatka
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Tryby przyciągania, odstęp siatki i przyciąganie do obiektów
|
||||||
|
.drafting-settings-2 = Ustawienia rysowania…
|
||||||
|
.modeling-3d = Modelowanie 3D
|
||||||
|
.display-tools = Narzędzia wyświetlania
|
||||||
|
.show-the-navigation-cube-navvcube = Pokaż kostkę nawigacji (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = Pokaż ikonę LUW (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = Umieść ikonę LUW w początku układu (UCSICON ORigin)
|
||||||
|
.display-resolution = Rozdzielczość wyświetlania
|
||||||
|
.applies-to-the-current-drawing = dotyczy bieżącego rysunku
|
||||||
|
.isolines-per-surface = Izolinie na powierzchnię
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Pokaż krawędzie sylwetki na bryłach (DISPSILH)
|
||||||
|
.surface-density-u = Gęstość powierzchni U
|
||||||
|
.surface-density-v = Gęstość powierzchni V
|
||||||
|
.surface-type = Typ powierzchni
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Zapisuj historię brył złożonych (SOLIDHIST)
|
||||||
|
.show-solid-history = Pokaż historię brył
|
||||||
|
.not-available = Niedostępne
|
||||||
|
.open-folder = Otwórz folder
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Gdzie aplikacja przechowuje własne pliki. Te lokalizacje są stałe.
|
||||||
|
.configuration = Konfiguracja
|
||||||
|
.plot-styles = Style wydruku
|
||||||
|
.autosave-files = Pliki automatycznego zapisu
|
||||||
.selection-modes = Tryby zaznaczania
|
.selection-modes = Tryby zaznaczania
|
||||||
.use-shift-to-add-to-selection-pickadd = Użyj klawisza Shift, aby dodać do zaznaczenia (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Użyj klawisza Shift, aby dodać do zaznaczenia (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Przeciąganie rysuje prostokąt zamiast lassa (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Przeciąganie rysuje prostokąt zamiast lassa (PICKDRAG)
|
||||||
|
|||||||
@@ -1376,6 +1376,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Controla tanto a caixa de seleção visível quanto a área de clique.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Controla tanto a caixa de seleção visível quanto a área de clique.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Aplicativos
|
||||||
|
.installed-plugins-and-their-sources = Plug-ins instalados e suas origens
|
||||||
|
.plugins-2 = Plug-ins…
|
||||||
|
.keyboard-shortcuts-2 = Atalhos de teclado
|
||||||
|
.keyboard-shortcuts-3 = Atalhos de teclado…
|
||||||
|
.command-aliases-2 = Apelidos de comandos
|
||||||
|
.command-aliases-3 = Apelidos de comandos…
|
||||||
|
.file-safety = Segurança de arquivos
|
||||||
|
.automatic-save = Salvamento automático
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Minutos entre salvamentos de recuperação em um arquivo .sv$; 0 desativa (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Manter uma cópia .bak ao sobrescrever um desenho (ISAVEBAK)
|
||||||
|
.plotting = Plotagem
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Dispositivo de plotagem, papel, escala e estilos de plotagem
|
||||||
|
.plot-and-page-setup = Plotar e configurar página…
|
||||||
|
.fill-truetype-glyphs-textfill = Preencher glifos TrueType (TEXTFILL)
|
||||||
|
.command-line = Linha de comando
|
||||||
|
.prompt-lines = Linhas de solicitação
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Linhas de solicitação temporárias acima da janela de comandos (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Tempo de esmaecimento do histórico
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Quanto tempo as linhas de histórico ficam visíveis; 0 as ignora (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Clicar em objetos sobrepostos abre um seletor
|
||||||
|
.block-edit = Edição de bloco
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Duplo clique para editar o bloco no local (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Duplo clique em blocos com atributos para editá-los (ATTEDIT)
|
||||||
|
.user-preferences = Preferências do usuário
|
||||||
|
.zoom = Zoom
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Inverter o zoom da roda do mouse (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Fator de zoom
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = Quanto um passo da roda amplia (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Texto e cotas
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT edita um objeto e encerra (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Cotas continuadas herdam camada e estilo da cota base (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = Prioridade de origem do QDIM
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = De quais pontos o QDIM mede. Também ajustável dentro do comando.
|
||||||
|
.add-scales-automatically = Adicionar escalas automaticamente
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Quais objetos anotativos recebem uma escala de anotação recém-definida (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Unidades de comprimento, ângulo e inserção
|
||||||
|
.drawing-units-2 = Unidades do desenho…
|
||||||
|
.drafting = Desenho
|
||||||
|
.drafting-rotation = Rotação de desenho
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Gira a mira e a grade de precisão no UCS ativo (SNAPANG).
|
||||||
|
.polar-tracking-increment = Incremento de rastreamento polar
|
||||||
|
.snap-and-grid = Precisão e grade
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Modos de precisão, espaçamento da grade e osnap
|
||||||
|
.drafting-settings-2 = Configurações de desenho…
|
||||||
|
.modeling-3d = Modelagem 3D
|
||||||
|
.display-tools = Ferramentas de exibição
|
||||||
|
.show-the-navigation-cube-navvcube = Mostrar o cubo de navegação (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = Mostrar o ícone UCS (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = Colocar o ícone UCS na origem (UCSICON ORigin)
|
||||||
|
.display-resolution = Resolução de exibição
|
||||||
|
.applies-to-the-current-drawing = aplica-se ao desenho atual
|
||||||
|
.isolines-per-surface = Isolinhas por superfície
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Mostrar arestas de silhueta em sólidos (DISPSILH)
|
||||||
|
.surface-density-u = Densidade de superfície U
|
||||||
|
.surface-density-v = Densidade de superfície V
|
||||||
|
.surface-type = Tipo de superfície
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Registrar o histórico de sólidos compostos (SOLIDHIST)
|
||||||
|
.show-solid-history = Mostrar o histórico de sólidos
|
||||||
|
.not-available = Não disponível
|
||||||
|
.open-folder = Abrir pasta
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Onde o aplicativo guarda seus próprios arquivos. Esses locais são fixos.
|
||||||
|
.configuration = Configuração
|
||||||
|
.plot-styles = Estilos de plotagem
|
||||||
|
.autosave-files = Arquivos de salvamento automático
|
||||||
.selection-modes = Modos de seleção
|
.selection-modes = Modos de seleção
|
||||||
.use-shift-to-add-to-selection-pickadd = Usar Shift para adicionar à seleção (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Usar Shift para adicionar à seleção (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Pressionar e arrastar desenha um retângulo em vez de um laço (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Pressionar e arrastar desenha um retângulo em vez de um laço (PICKDRAG)
|
||||||
|
|||||||
@@ -1379,6 +1379,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Управляет видимым прицелом выбора и областью щелчка.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Управляет видимым прицелом выбора и областью щелчка.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Приложения
|
||||||
|
.installed-plugins-and-their-sources = Установленные плагины и их источники
|
||||||
|
.plugins-2 = Плагины…
|
||||||
|
.keyboard-shortcuts-2 = Сочетания клавиш
|
||||||
|
.keyboard-shortcuts-3 = Сочетания клавиш…
|
||||||
|
.command-aliases-2 = Псевдонимы команд
|
||||||
|
.command-aliases-3 = Псевдонимы команд…
|
||||||
|
.file-safety = Безопасность файлов
|
||||||
|
.automatic-save = Автосохранение
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Минуты между сохранениями восстановления в файл .sv$; 0 отключает (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Сохранять копию .bak при перезаписи чертежа (ISAVEBAK)
|
||||||
|
.plotting = Печать
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Устройство печати, бумага, масштаб и стили печати
|
||||||
|
.plot-and-page-setup = Печать и параметры листа…
|
||||||
|
.fill-truetype-glyphs-textfill = Заливать глифы TrueType (TEXTFILL)
|
||||||
|
.command-line = Командная строка
|
||||||
|
.prompt-lines = Строки запроса
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Временные строки запроса над окном команд (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Время затухания журнала
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Как долго строки журнала остаются видимыми; 0 их пропускает (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Щелчок по перекрывающимся объектам открывает список выбора
|
||||||
|
.block-edit = Редактирование блока
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Двойной щелчок редактирует блок на месте (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Двойной щелчок по блокам с атрибутами редактирует атрибуты (ATTEDIT)
|
||||||
|
.user-preferences = Пользовательские настройки
|
||||||
|
.zoom = Зумирование
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Обратить зумирование колесом мыши (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Коэффициент зумирования
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = Насколько приближает один щелчок колеса (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Текст и размеры
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT редактирует один объект и завершается (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Продолженные размеры наследуют слой и стиль базового размера (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = Приоритет начала QDIM
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = От каких точек измеряет QDIM. Настраивается и внутри команды.
|
||||||
|
.add-scales-automatically = Добавлять масштабы автоматически
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Какие аннотативные объекты принимают вновь заданный масштаб аннотаций (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Единицы длины, угла и вставки
|
||||||
|
.drawing-units-2 = Единицы чертежа…
|
||||||
|
.drafting = Черчение
|
||||||
|
.drafting-rotation = Поворот черчения
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Поворачивает перекрестье и сетку привязки в активной ПСК (SNAPANG).
|
||||||
|
.polar-tracking-increment = Шаг полярного отслеживания
|
||||||
|
.snap-and-grid = Привязка и сетка
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Режимы привязки, шаг сетки и объектная привязка
|
||||||
|
.drafting-settings-2 = Параметры черчения…
|
||||||
|
.modeling-3d = 3D-моделирование
|
||||||
|
.display-tools = Инструменты отображения
|
||||||
|
.show-the-navigation-cube-navvcube = Показывать куб навигации (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = Показывать значок ПСК (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = Размещать значок ПСК в начале координат (UCSICON ORigin)
|
||||||
|
.display-resolution = Разрешение отображения
|
||||||
|
.applies-to-the-current-drawing = применяется к текущему чертежу
|
||||||
|
.isolines-per-surface = Изолиний на поверхность
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Показывать силуэтные рёбра тел (DISPSILH)
|
||||||
|
.surface-density-u = Плотность поверхности U
|
||||||
|
.surface-density-v = Плотность поверхности V
|
||||||
|
.surface-type = Тип поверхности
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Записывать историю составных тел (SOLIDHIST)
|
||||||
|
.show-solid-history = Показывать историю тел
|
||||||
|
.not-available = Недоступно
|
||||||
|
.open-folder = Открыть папку
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Где приложение хранит собственные файлы. Эти расположения фиксированы.
|
||||||
|
.configuration = Конфигурация
|
||||||
|
.plot-styles = Стили печати
|
||||||
|
.autosave-files = Файлы автосохранения
|
||||||
.selection-modes = Режимы выбора
|
.selection-modes = Режимы выбора
|
||||||
.use-shift-to-add-to-selection-pickadd = Использовать Shift для добавления к выбору (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Использовать Shift для добавления к выбору (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Нажатие и перетаскивание рисует прямоугольник вместо лассо (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Нажатие и перетаскивание рисует прямоугольник вместо лассо (PICKDRAG)
|
||||||
|
|||||||
@@ -464,8 +464,8 @@ common =
|
|||||||
.fixed-height = Sabit Yükseklik:
|
.fixed-height = Sabit Yükseklik:
|
||||||
.fixed-length-dimfxl = Sabit uzunluk (DIMFXL)
|
.fixed-length-dimfxl = Sabit uzunluk (DIMFXL)
|
||||||
.fixed-length-ext-lines-dimfxlon = Sabit uzunluklu uzatma çizgileri (DIMFXLON)
|
.fixed-length-ext-lines-dimfxlon = Sabit uzunluklu uzatma çizgileri (DIMFXLON)
|
||||||
.flat-shaded = Düz Gölgelendirilmiş
|
.flat-shaded = Düz Gölge
|
||||||
.flat-shaded-edges = Düz Gölgelendirilmiş + Kenarlar
|
.flat-shaded-edges = Düz Gölge + Kenarlar
|
||||||
.flow-direction = Akış yönü:
|
.flow-direction = Akış yönü:
|
||||||
.font-load-failed-script-e = Yazı tipi yüklenemedi (__ocs_fmt_0__): __ocs_fmt_1__
|
.font-load-failed-script-e = Yazı tipi yüklenemedi (__ocs_fmt_0__): __ocs_fmt_1__
|
||||||
.force-text-inside-dimtix = Metni içeride olmaya zorla (DIMTIX)
|
.force-text-inside-dimtix = Metni içeride olmaya zorla (DIMTIX)
|
||||||
@@ -479,8 +479,8 @@ common =
|
|||||||
.general = Genel
|
.general = Genel
|
||||||
.generate-tolerances-dimtol = Toleransları oluştur (DIMTOL)
|
.generate-tolerances-dimtol = Toleransları oluştur (DIMTOL)
|
||||||
.geometry = Geometri
|
.geometry = Geometri
|
||||||
.gouraud-shaded = Gouraud Gölgelendirilmiş
|
.gouraud-shaded = Gouraud Gölge
|
||||||
.gouraud-shaded-edges = Gouraud Gölgelendirilmiş + Kenarlar
|
.gouraud-shaded-edges = Gouraud Gölge + Kenarlar
|
||||||
.green = Yeşil
|
.green = Yeşil
|
||||||
.group = Grup
|
.group = Grup
|
||||||
.group-created = "__ocs_fmt_0__" grubu oluşturuldu.
|
.group-created = "__ocs_fmt_0__" grubu oluşturuldu.
|
||||||
@@ -1362,6 +1362,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = Hem görünür seçim kutusunu hem de tıklama alanını denetler.
|
.controls-both-the-visible-selection-box-and-the-click-aperture = Hem görünür seçim kutusunu hem de tıklama alanını denetler.
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = Uygulamalar
|
||||||
|
.installed-plugins-and-their-sources = Yüklü eklentiler ve kaynakları
|
||||||
|
.plugins-2 = Eklentiler…
|
||||||
|
.keyboard-shortcuts-2 = Klavye kısayolları
|
||||||
|
.keyboard-shortcuts-3 = Klavye kısayolları…
|
||||||
|
.command-aliases-2 = Komut takma adları
|
||||||
|
.command-aliases-3 = Komut takma adları…
|
||||||
|
.file-safety = Dosya güvenliği
|
||||||
|
.automatic-save = Otomatik kaydetme
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = Bir .sv$ dosyasına kurtarma kayıtları arasındaki dakika; 0 kapatır (SAVETIME).
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = Bir çizimin üzerine yazarken .bak kopyası tut (ISAVEBAK)
|
||||||
|
.plotting = Çizdirme
|
||||||
|
.plot-device-paper-scale-and-plot-styles = Çizim aygıtı, kâğıt, ölçek ve çizim stilleri
|
||||||
|
.plot-and-page-setup = Çizdir ve sayfa düzeni…
|
||||||
|
.fill-truetype-glyphs-textfill = TrueType karakterlerini doldur (TEXTFILL)
|
||||||
|
.command-line = Komut satırı
|
||||||
|
.prompt-lines = İstem satırları
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = Komut penceresinin üstündeki geçici istem satırları (CLIPROMPTLINES).
|
||||||
|
.history-fade-time = Geçmiş solma süresi
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = Geçmiş satırlarının ne kadar görünür kalacağı; 0 atlar (COMMANDLINEFADETIME).
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = Üst üste binen nesnelere tıklamak bir seçici açar
|
||||||
|
.block-edit = Blok düzenleme
|
||||||
|
.double-click-to-edit-block-in-place-refedit = Bloğu yerinde düzenlemek için çift tıklayın (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = Öznitelikli bloklara çift tıklayarak özniteliklerini düzenleyin (ATTEDIT)
|
||||||
|
.user-preferences = Kullanıcı tercihleri
|
||||||
|
.zoom = Yakınlaştırma
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = Fare tekerleği yakınlaştırmasını ters çevir (ZOOMWHEEL)
|
||||||
|
.zoom-factor = Yakınlaştırma çarpanı
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = Bir tekerlek adımının ne kadar yakınlaştıracağı (ZOOMFACTOR).
|
||||||
|
.text-and-dimensions = Metin ve ölçülendirme
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT bir nesneyi düzenler ve biter (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = Devam eden ölçüler temel ölçünün katmanını ve stilini devralır (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = QDIM başlangıç önceliği
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = QDIM'in hangi noktalardan ölçtüğü. Komutun içinden de ayarlanır.
|
||||||
|
.add-scales-automatically = Ölçekleri otomatik ekle
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = Yeni ayarlanan bir açıklama ölçeğini hangi açıklamalı nesnelerin alacağı (ANNOAUTOSCALE).
|
||||||
|
.length-angle-and-insertion-units = Uzunluk, açı ve ekleme birimleri
|
||||||
|
.drawing-units-2 = Çizim birimleri…
|
||||||
|
.drafting = Taslak
|
||||||
|
.drafting-rotation = Taslak dönüşü
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = Etkin UCS'de artı imleci ve yakalama ızgarasını döndürür (SNAPANG).
|
||||||
|
.polar-tracking-increment = Kutupsal izleme artışı
|
||||||
|
.snap-and-grid = Yakalama ve ızgara
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = Yakalama modları, ızgara aralığı ve nesne yakalama
|
||||||
|
.drafting-settings-2 = Taslak ayarları…
|
||||||
|
.modeling-3d = 3B modelleme
|
||||||
|
.display-tools = Görüntüleme araçları
|
||||||
|
.show-the-navigation-cube-navvcube = Gezinme küpünü göster (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = UCS simgesini göster (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = UCS simgesini başlangıç noktasına yerleştir (UCSICON ORigin)
|
||||||
|
.display-resolution = Görüntüleme çözünürlüğü
|
||||||
|
.applies-to-the-current-drawing = geçerli çizim için geçerlidir
|
||||||
|
.isolines-per-surface = Yüzey başına eş çizgiler
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = Katılarda siluet kenarlarını göster (DISPSILH)
|
||||||
|
.surface-density-u = Yüzey yoğunluğu U
|
||||||
|
.surface-density-v = Yüzey yoğunluğu V
|
||||||
|
.surface-type = Yüzey türü
|
||||||
|
.record-the-history-of-composite-solids-solidhist = Bileşik katıların geçmişini kaydet (SOLIDHIST)
|
||||||
|
.show-solid-history = Katı geçmişini göster
|
||||||
|
.not-available = Kullanılamıyor
|
||||||
|
.open-folder = Klasörü aç
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = Uygulamanın kendi dosyalarını tuttuğu yer. Bu konumlar sabittir.
|
||||||
|
.configuration = Yapılandırma
|
||||||
|
.plot-styles = Çizim stilleri
|
||||||
|
.autosave-files = Otomatik kaydetme dosyaları
|
||||||
.selection-modes = Seçim modları
|
.selection-modes = Seçim modları
|
||||||
.use-shift-to-add-to-selection-pickadd = Seçime eklemek için Shift kullan (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = Seçime eklemek için Shift kullan (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Basılı tutup sürükleme kement yerine dikdörtgen çizer (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = Basılı tutup sürükleme kement yerine dikdörtgen çizer (PICKDRAG)
|
||||||
@@ -3433,7 +3498,7 @@ view =
|
|||||||
.syncpviewports-master-is-not-a-viewport = SYNCPVIEWPORTS: Ana nesne bir görünüm alanı değil.
|
.syncpviewports-master-is-not-a-viewport = SYNCPVIEWPORTS: Ana nesne bir görünüm alanı değil.
|
||||||
.syncpviewports-synced-n-viewport-s-to-the-mast = SYNCPVIEWPORTS: __ocs_fmt_0__ görünüm alanı ana görünüm alanıyla eşitlendi.
|
.syncpviewports-synced-n-viewport-s-to-the-mast = SYNCPVIEWPORTS: __ocs_fmt_0__ görünüm alanı ana görünüm alanıyla eşitlendi.
|
||||||
.scale-arg-already-exists = __ocs_fmt_0__ ölçeği zaten var.
|
.scale-arg-already-exists = __ocs_fmt_0__ ölçeği zaten var.
|
||||||
.shaded = Gölgelendirilmiş
|
.shaded = Gölge
|
||||||
.sheet-set-manager = Pafta Seti Yöneticisi
|
.sheet-set-manager = Pafta Seti Yöneticisi
|
||||||
.shortcut-key-not-found = '__ocs_fmt_0__' kısayolu bulunamadı.
|
.shortcut-key-not-found = '__ocs_fmt_0__' kısayolu bulunamadı.
|
||||||
.shortcut-key-removed = '__ocs_fmt_0__' kısayolu kaldırıldı.
|
.shortcut-key-removed = '__ocs_fmt_0__' kısayolu kaldırıldı.
|
||||||
@@ -4365,7 +4430,7 @@ styles =
|
|||||||
.usage-lwdisplay-on-off = Kullanım: LWDISPLAY [ON|OFF]
|
.usage-lwdisplay-on-off = Kullanım: LWDISPLAY [ON|OFF]
|
||||||
.usage-rename-type-old-new-types-layer-block-st = Kullanım: RENAME <type> <old> <new> (türler: LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW)
|
.usage-rename-type-old-new-types-layer-block-st = Kullanım: RENAME <type> <old> <new> (türler: LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW)
|
||||||
.usage-scaletext-factor-or-scaletext-h-height = Kullanım: SCALETEXT <factor> veya SCALETEXT H <height>
|
.usage-scaletext-factor-or-scaletext-h-height = Kullanım: SCALETEXT <factor> veya SCALETEXT H <height>
|
||||||
.vscurrent-visual-style-shaded-wireframe-hidden = VSCURRENT Görsel stil [Gölgelendirilmiş / Tel Kafes / Gizli / Gerçekçi / Kavramsal / X Işını]:
|
.vscurrent-visual-style-shaded-wireframe-hidden = VSCURRENT Görsel stil [Gölge / Tel Kafes / Gizli / Gerçekçi / Kavramsal / X Işını]:
|
||||||
.windows-desktop = Windows masaüstü
|
.windows-desktop = Windows masaüstü
|
||||||
.big-font-file = büyük yazı tipi dosyası…
|
.big-font-file = büyük yazı tipi dosyası…
|
||||||
.font-file-picker-lowercase = yazı tipi dosyası…
|
.font-file-picker-lowercase = yazı tipi dosyası…
|
||||||
|
|||||||
@@ -1363,6 +1363,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = 同时控制可见选择框和单击范围。
|
.controls-both-the-visible-selection-box-and-the-click-aperture = 同时控制可见选择框和单击范围。
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = 应用程序
|
||||||
|
.installed-plugins-and-their-sources = 已安装的插件及其来源
|
||||||
|
.plugins-2 = 插件…
|
||||||
|
.keyboard-shortcuts-2 = 键盘快捷键
|
||||||
|
.keyboard-shortcuts-3 = 键盘快捷键…
|
||||||
|
.command-aliases-2 = 命令别名
|
||||||
|
.command-aliases-3 = 命令别名…
|
||||||
|
.file-safety = 文件安全
|
||||||
|
.automatic-save = 自动保存
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = 两次保存到 .sv$ 恢复文件之间的分钟数;0 表示关闭 (SAVETIME)。
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = 覆盖图形时保留 .bak 副本 (ISAVEBAK)
|
||||||
|
.plotting = 打印
|
||||||
|
.plot-device-paper-scale-and-plot-styles = 打印设备、图纸、比例和打印样式
|
||||||
|
.plot-and-page-setup = 打印和页面设置…
|
||||||
|
.fill-truetype-glyphs-textfill = 填充 TrueType 字形 (TEXTFILL)
|
||||||
|
.command-line = 命令行
|
||||||
|
.prompt-lines = 提示行
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = 命令窗口上方显示的临时提示行 (CLIPROMPTLINES)。
|
||||||
|
.history-fade-time = 历史淡出时间
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = 历史行保持可见的时长;0 表示跳过 (COMMANDLINEFADETIME)。
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = 单击重叠对象时打开选择器
|
||||||
|
.block-edit = 块编辑
|
||||||
|
.double-click-to-edit-block-in-place-refedit = 双击就地编辑块 (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = 双击带属性的块以编辑属性 (ATTEDIT)
|
||||||
|
.user-preferences = 用户首选项
|
||||||
|
.zoom = 缩放
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = 反转鼠标滚轮缩放方向 (ZOOMWHEEL)
|
||||||
|
.zoom-factor = 缩放系数
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = 滚轮每格的缩放幅度 (ZOOMFACTOR)。
|
||||||
|
.text-and-dimensions = 文字和标注
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT 编辑一个对象后结束 (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = 连续标注继承基准标注的图层和样式 (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = QDIM 原点优先级
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = QDIM 从哪些点测量。也可在命令内设置。
|
||||||
|
.add-scales-automatically = 自动添加比例
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = 哪些注释性对象接受新设置的注释比例 (ANNOAUTOSCALE)。
|
||||||
|
.length-angle-and-insertion-units = 长度、角度和插入单位
|
||||||
|
.drawing-units-2 = 图形单位…
|
||||||
|
.drafting = 草图
|
||||||
|
.drafting-rotation = 草图旋转
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = 在当前 UCS 中旋转十字光标和捕捉栅格 (SNAPANG)。
|
||||||
|
.polar-tracking-increment = 极轴追踪增量
|
||||||
|
.snap-and-grid = 捕捉和栅格
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = 捕捉模式、栅格间距和对象捕捉
|
||||||
|
.drafting-settings-2 = 草图设置…
|
||||||
|
.modeling-3d = 三维建模
|
||||||
|
.display-tools = 显示工具
|
||||||
|
.show-the-navigation-cube-navvcube = 显示导航立方体 (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = 显示 UCS 图标 (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = 将 UCS 图标置于原点 (UCSICON ORigin)
|
||||||
|
.display-resolution = 显示分辨率
|
||||||
|
.applies-to-the-current-drawing = 应用于当前图形
|
||||||
|
.isolines-per-surface = 每个曲面的素线数
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = 在实体上显示轮廓边 (DISPSILH)
|
||||||
|
.surface-density-u = 曲面密度 U
|
||||||
|
.surface-density-v = 曲面密度 V
|
||||||
|
.surface-type = 曲面类型
|
||||||
|
.record-the-history-of-composite-solids-solidhist = 记录复合实体的历史 (SOLIDHIST)
|
||||||
|
.show-solid-history = 显示实体历史
|
||||||
|
.not-available = 不可用
|
||||||
|
.open-folder = 打开文件夹
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = 应用程序存放自身文件的位置。这些位置是固定的。
|
||||||
|
.configuration = 配置
|
||||||
|
.plot-styles = 打印样式
|
||||||
|
.autosave-files = 自动保存文件
|
||||||
.selection-modes = 选择模式
|
.selection-modes = 选择模式
|
||||||
.use-shift-to-add-to-selection-pickadd = 使用 Shift 添加到选择集 (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = 使用 Shift 添加到选择集 (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = 按住并拖动时绘制矩形而非套索 (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = 按住并拖动时绘制矩形而非套索 (PICKDRAG)
|
||||||
|
|||||||
@@ -1375,6 +1375,71 @@ common =
|
|||||||
.pick-box-size = { common.pickbox }: { common.size }
|
.pick-box-size = { common.pickbox }: { common.size }
|
||||||
.controls-both-the-visible-selection-box-and-the-click-aperture = 同時控制可見選取框與點選範圍。
|
.controls-both-the-visible-selection-box-and-the-click-aperture = 同時控制可見選取框與點選範圍。
|
||||||
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
.visual-effect-settings = { common.visual-effects } · { properties.settings }
|
||||||
|
.applications = 應用程式
|
||||||
|
.installed-plugins-and-their-sources = 已安裝的外掛程式及其來源
|
||||||
|
.plugins-2 = 外掛程式…
|
||||||
|
.keyboard-shortcuts-2 = 鍵盤快速鍵
|
||||||
|
.keyboard-shortcuts-3 = 鍵盤快速鍵…
|
||||||
|
.command-aliases-2 = 指令別名
|
||||||
|
.command-aliases-3 = 指令別名…
|
||||||
|
.file-safety = 檔案安全
|
||||||
|
.automatic-save = 自動儲存
|
||||||
|
.minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime = 兩次儲存到 .sv$ 復原檔之間的分鐘數;0 表示關閉 (SAVETIME)。
|
||||||
|
.keep-a-bak-copy-when-overwriting-a-drawing-isavebak = 覆寫圖面時保留 .bak 副本 (ISAVEBAK)
|
||||||
|
.plotting = 出圖
|
||||||
|
.plot-device-paper-scale-and-plot-styles = 出圖裝置、紙張、比例與出圖樣式
|
||||||
|
.plot-and-page-setup = 出圖與頁面設定…
|
||||||
|
.fill-truetype-glyphs-textfill = 填滿 TrueType 字符 (TEXTFILL)
|
||||||
|
.command-line = 指令行
|
||||||
|
.prompt-lines = 提示行
|
||||||
|
.temporary-prompt-lines-shown-above-the-command-window-clipromptlines = 指令視窗上方顯示的暫時提示行 (CLIPROMPTLINES)。
|
||||||
|
.history-fade-time = 歷程淡出時間
|
||||||
|
.how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime = 歷程行保持顯示的時間;0 表示略過 (COMMANDLINEFADETIME)。
|
||||||
|
.clicking-overlapping-objects-opens-a-picker = 點選重疊的物件時開啟選擇器
|
||||||
|
.block-edit = 圖塊編輯
|
||||||
|
.double-click-to-edit-block-in-place-refedit = 雙擊以就地編輯圖塊 (REFEDIT)
|
||||||
|
.double-click-attributed-blocks-to-edit-attributes-attedit = 雙擊具屬性的圖塊以編輯屬性 (ATTEDIT)
|
||||||
|
.user-preferences = 使用者偏好設定
|
||||||
|
.zoom = 縮放
|
||||||
|
.reverse-mouse-wheel-zoom-zoomwheel = 反轉滑鼠滾輪縮放方向 (ZOOMWHEEL)
|
||||||
|
.zoom-factor = 縮放係數
|
||||||
|
.how-far-one-wheel-notch-zooms-zoomfactor = 滾輪每一格的縮放幅度 (ZOOMFACTOR)。
|
||||||
|
.text-and-dimensions = 文字與標註
|
||||||
|
.textedit-edits-one-object-and-ends-texteditmode = TEXTEDIT 編輯一個物件後結束 (TEXTEDITMODE)
|
||||||
|
.continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode = 連續標註繼承基準標註的圖層與樣式 (DIMCONTINUEMODE)
|
||||||
|
.qdim-origin-priority = QDIM 原點優先順序
|
||||||
|
.which-points-qdim-measures-from-also-settable-inside-the-command = QDIM 從哪些點量測。也可在指令內設定。
|
||||||
|
.add-scales-automatically = 自動加入比例
|
||||||
|
.which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale = 哪些註解性物件接受新設定的註解比例 (ANNOAUTOSCALE)。
|
||||||
|
.length-angle-and-insertion-units = 長度、角度與插入單位
|
||||||
|
.drawing-units-2 = 圖面單位…
|
||||||
|
.drafting = 製圖
|
||||||
|
.drafting-rotation = 製圖旋轉
|
||||||
|
.rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang = 在使用中的 UCS 內旋轉十字游標與鎖點格線 (SNAPANG)。
|
||||||
|
.polar-tracking-increment = 極座標追蹤增量
|
||||||
|
.snap-and-grid = 鎖點與格線
|
||||||
|
.snap-modes-grid-spacing-and-object-snap = 鎖點模式、格線間距與物件鎖點
|
||||||
|
.drafting-settings-2 = 製圖設定…
|
||||||
|
.modeling-3d = 3D 塑型
|
||||||
|
.display-tools = 顯示工具
|
||||||
|
.show-the-navigation-cube-navvcube = 顯示導覽方塊 (NAVVCUBE)
|
||||||
|
.show-the-ucs-icon-ucsicon = 顯示 UCS 圖示 (UCSICON)
|
||||||
|
.place-the-ucs-icon-at-the-origin-ucsicon-origin = 將 UCS 圖示置於原點 (UCSICON ORigin)
|
||||||
|
.display-resolution = 顯示解析度
|
||||||
|
.applies-to-the-current-drawing = 套用至目前的圖面
|
||||||
|
.isolines-per-surface = 每個曲面的等值線
|
||||||
|
.show-silhouette-edges-on-solids-dispsilh = 在實體上顯示輪廓邊 (DISPSILH)
|
||||||
|
.surface-density-u = 曲面密度 U
|
||||||
|
.surface-density-v = 曲面密度 V
|
||||||
|
.surface-type = 曲面類型
|
||||||
|
.record-the-history-of-composite-solids-solidhist = 記錄複合實體的歷程 (SOLIDHIST)
|
||||||
|
.show-solid-history = 顯示實體歷程
|
||||||
|
.not-available = 無法使用
|
||||||
|
.open-folder = 開啟資料夾
|
||||||
|
.where-the-application-keeps-its-own-files-these-locations-are-fixed = 應用程式存放自身檔案的位置。這些位置是固定的。
|
||||||
|
.configuration = 組態
|
||||||
|
.plot-styles = 出圖樣式
|
||||||
|
.autosave-files = 自動儲存檔案
|
||||||
.selection-modes = 選取模式
|
.selection-modes = 選取模式
|
||||||
.use-shift-to-add-to-selection-pickadd = 使用 Shift 加入選取集 (PICKADD)
|
.use-shift-to-add-to-selection-pickadd = 使用 Shift 加入選取集 (PICKADD)
|
||||||
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = 按住並拖曳時繪製矩形而非套索 (PICKDRAG)
|
.press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag = 按住並拖曳時繪製矩形而非套索 (PICKDRAG)
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ def locale_path(locale):
|
|||||||
return f"/{locale}/"
|
return f"/{locale}/"
|
||||||
|
|
||||||
|
|
||||||
|
def public_path(locale):
|
||||||
|
return "/" if locale == "en-US" else locale_path(locale)
|
||||||
|
|
||||||
|
|
||||||
def load_catalogs():
|
def load_catalogs():
|
||||||
catalogs = {p.stem: json.loads(p.read_text()) for p in sorted((ROOT / "site/locales").glob("*.json"))}
|
catalogs = {p.stem: json.loads(p.read_text()) for p in sorted((ROOT / "site/locales").glob("*.json"))}
|
||||||
supported = {p.parent.name for p in (ROOT / "locales").glob("*/opencadstudio.ftl")}
|
supported = {p.parent.name for p in (ROOT / "locales").glob("*/opencadstudio.ftl")}
|
||||||
@@ -65,14 +69,14 @@ def build(output):
|
|||||||
script = "const SITE_LOCALES = " + json.dumps(list(catalogs)) + ";\n" + (ROOT / "site/site.js").read_text()
|
script = "const SITE_LOCALES = " + json.dumps(list(catalogs)) + ";\n" + (ROOT / "site/site.js").read_text()
|
||||||
(output / "site.js").write_text(script, encoding="utf-8")
|
(output / "site.js").write_text(script, encoding="utf-8")
|
||||||
js_version = hashlib.sha256(script.encode()).hexdigest()[:12]
|
js_version = hashlib.sha256(script.encode()).hexdigest()[:12]
|
||||||
alternates = '\n'.join(f'<link rel="alternate" hreflang="{lang}" href="{BASE}{locale_path(lang)}" />' for lang in catalogs)
|
alternates = '\n'.join(f'<link rel="alternate" hreflang="{lang}" href="{BASE}{public_path(lang)}" />' for lang in catalogs)
|
||||||
alternates += f'\n<link rel="alternate" hreflang="x-default" href="{BASE}/" />'
|
alternates += f'\n<link rel="alternate" hreflang="x-default" href="{BASE}/" />'
|
||||||
for locale, messages in catalogs.items():
|
for locale, messages in catalogs.items():
|
||||||
path = locale_path(locale)
|
path = public_path(locale)
|
||||||
destination = output / path.lstrip("/")
|
destination = output / locale_path(locale).lstrip("/")
|
||||||
destination.mkdir(parents=True, exist_ok=True)
|
destination.mkdir(parents=True, exist_ok=True)
|
||||||
language_links = '\n'.join(
|
language_links = '\n'.join(
|
||||||
f'<a href="{locale_path(lang)}" lang="{lang}" hreflang="{lang}" dir="auto"'
|
f'<a href="{public_path(lang)}" lang="{lang}" hreflang="{lang}" dir="auto"'
|
||||||
+ (' aria-current="page"' if lang == locale else '')
|
+ (' aria-current="page"' if lang == locale else '')
|
||||||
+ f'>{escape(labels["name"])}</a>' for lang, labels in catalogs.items()
|
+ f'>{escape(labels["name"])}</a>' for lang, labels in catalogs.items()
|
||||||
)
|
)
|
||||||
@@ -85,6 +89,11 @@ def build(output):
|
|||||||
"downloadUrl": REPO + "/releases/latest", "installUrl": BASE + "/app/",
|
"downloadUrl": REPO + "/releases/latest", "installUrl": BASE + "/app/",
|
||||||
"offers": {"@type": "Offer", "price": "0", "priceCurrency": "USD"},
|
"offers": {"@type": "Offer", "price": "0", "priceCurrency": "USD"},
|
||||||
}
|
}
|
||||||
|
website_schema = {
|
||||||
|
"@context": "https://schema.org", "@type": "WebSite",
|
||||||
|
"name": "Open CAD Studio", "alternateName": ["OpenCADStudio", "opencadstudio.com"],
|
||||||
|
"url": BASE + "/",
|
||||||
|
}
|
||||||
values = {key: escape(value) for key, value in messages.items()}
|
values = {key: escape(value) for key, value in messages.items()}
|
||||||
values.update(locale=locale, direction="rtl" if locale == "ar-SA" else "ltr",
|
values.update(locale=locale, direction="rtl" if locale == "ar-SA" else "ltr",
|
||||||
path=path, url=BASE + path, og_locale=locale.replace("-", "_"),
|
path=path, url=BASE + path, og_locale=locale.replace("-", "_"),
|
||||||
@@ -92,6 +101,8 @@ def build(output):
|
|||||||
css_version=css_version, js_version=js_version, alternates=alternates,
|
css_version=css_version, js_version=js_version, alternates=alternates,
|
||||||
languages=escape(messages["languages"].format(count=len(catalogs))),
|
languages=escape(messages["languages"].format(count=len(catalogs))),
|
||||||
language_links=language_links,
|
language_links=language_links,
|
||||||
|
website_schema=(f'<script type="application/ld+json">{json.dumps(website_schema)}</script>'
|
||||||
|
if locale == "en-US" else ""),
|
||||||
schema=json.dumps(schema, ensure_ascii=False).replace("<", "\\u003c"))
|
schema=json.dumps(schema, ensure_ascii=False).replace("<", "\\u003c"))
|
||||||
rendered = re.sub(r"\{\{(\w+)\}\}", lambda match: values[match[1]], template)
|
rendered = re.sub(r"\{\{(\w+)\}\}", lambda match: values[match[1]], template)
|
||||||
(destination / "index.html").write_text(rendered, encoding="utf-8")
|
(destination / "index.html").write_text(rendered, encoding="utf-8")
|
||||||
@@ -101,7 +112,7 @@ def build(output):
|
|||||||
(destination / "site.webmanifest").write_text(json.dumps(localized_manifest, ensure_ascii=False, indent=2) + '\n', encoding="utf-8")
|
(destination / "site.webmanifest").write_text(json.dumps(localized_manifest, ensure_ascii=False, indent=2) + '\n', encoding="utf-8")
|
||||||
for filename in ("index.html", "site.webmanifest"):
|
for filename in ("index.html", "site.webmanifest"):
|
||||||
shutil.copyfile(output / "en-US" / filename, output / filename)
|
shutil.copyfile(output / "en-US" / filename, output / filename)
|
||||||
urls = f'<url><loc>{BASE}/</loc></url>\n' + ''.join(f'<url><loc>{BASE}{locale_path(locale)}</loc></url>\n' for locale in catalogs)
|
urls = ''.join(f'<url><loc>{BASE}{public_path(locale)}</loc></url>\n' for locale in catalogs)
|
||||||
(output / "sitemap.xml").write_text('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' + urls + '</urlset>\n')
|
(output / "sitemap.xml").write_text('<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' + urls + '</urlset>\n')
|
||||||
|
|
||||||
app = output / "app/index.html"
|
app = output / "app/index.html"
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ from urllib.parse import urlsplit
|
|||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
subprocess.run(['python3', 'scripts/test_locales.py'], cwd=ROOT, check=True)
|
|
||||||
subprocess.run(['python3', 'scripts/export-locales.py', '--check'], cwd=ROOT, check=True)
|
|
||||||
subprocess.run(['node', 'scripts/test_web_locales.cjs'], cwd=ROOT, check=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Page(HTMLParser):
|
class Page(HTMLParser):
|
||||||
@@ -49,7 +46,7 @@ with tempfile.TemporaryDirectory() as directory:
|
|||||||
svg = charts.render_svg('HakanSeven12/OpenCADStudio', [date], [('v1', date, 10)], 'dark', chart, messages)
|
svg = charts.render_svg('HakanSeven12/OpenCADStudio', [date], [('v1', date, 10)], 'dark', chart, messages)
|
||||||
tree = ET.fromstring(svg)
|
tree = ET.fromstring(svg)
|
||||||
assert tree.find('{http://www.w3.org/2000/svg}title').text == messages['stars' if chart == 'stars' else 'downloads']
|
assert tree.find('{http://www.w3.org/2000/svg}title').text == messages['stars' if chart == 'stars' else 'downloads']
|
||||||
(output / locale / f'{chart[:-1]}-history-dark.svg').write_text(svg)
|
(output / ('' if locale == 'en-US' else locale) / f'{chart[:-1]}-history-dark.svg').write_text(svg)
|
||||||
html = (output / locale / 'index.html').read_text()
|
html = (output / locale / 'index.html').read_text()
|
||||||
assert '{{' not in html and '{count}' not in html, locale
|
assert '{{' not in html and '{count}' not in html, locale
|
||||||
page = Page(html)
|
page = Page(html)
|
||||||
@@ -57,7 +54,8 @@ with tempfile.TemporaryDirectory() as directory:
|
|||||||
links = [attrs for tag, attrs in page.tags if tag == 'link']
|
links = [attrs for tag, attrs in page.tags if tag == 'link']
|
||||||
assert {link['href'] for link in links if link.get('rel') == 'icon'} == {'/favicon.ico', '/favicon.png', '/favicon.svg'}
|
assert {link['href'] for link in links if link.get('rel') == 'icon'} == {'/favicon.ico', '/favicon.png', '/favicon.svg'}
|
||||||
assert {link['hreflang'] for link in links if link.get('rel') == 'alternate'} == supported | {'x-default'}
|
assert {link['hreflang'] for link in links if link.get('rel') == 'alternate'} == supported | {'x-default'}
|
||||||
assert next(link['href'] for link in links if link.get('rel') == 'canonical') == f'https://www.opencadstudio.com/{locale}/'
|
expected_path = '/' if locale == 'en-US' else f'/{locale}/'
|
||||||
|
assert next(link['href'] for link in links if link.get('rel') == 'canonical') == f'https://www.opencadstudio.com{expected_path}'
|
||||||
choices = [attrs for tag, attrs in page.tags if tag == 'a' and 'hreflang' in attrs]
|
choices = [attrs for tag, attrs in page.tags if tag == 'a' and 'hreflang' in attrs]
|
||||||
assert {choice['hreflang'] for choice in choices} == supported
|
assert {choice['hreflang'] for choice in choices} == supported
|
||||||
assert [choice['hreflang'] for choice in choices if choice.get('aria-current') == 'page'] == [locale]
|
assert [choice['hreflang'] for choice in choices if choice.get('aria-current') == 'page'] == [locale]
|
||||||
@@ -74,7 +72,7 @@ with tempfile.TemporaryDirectory() as directory:
|
|||||||
path = output / urlsplit(target).path.lstrip('/')
|
path = output / urlsplit(target).path.lstrip('/')
|
||||||
assert path.exists(), (locale, target)
|
assert path.exists(), (locale, target)
|
||||||
manifest = json.loads((output / locale / 'site.webmanifest').read_text())
|
manifest = json.loads((output / locale / 'site.webmanifest').read_text())
|
||||||
assert manifest['lang'] == locale and manifest['start_url'] == f'/{locale}/'
|
assert manifest['lang'] == locale and manifest['start_url'] == expected_path
|
||||||
for icon in manifest['icons']:
|
for icon in manifest['icons']:
|
||||||
assert (output / icon['src'].lstrip('/')).exists()
|
assert (output / icon['src'].lstrip('/')).exists()
|
||||||
if locale != 'en-US':
|
if locale != 'en-US':
|
||||||
@@ -87,7 +85,13 @@ with tempfile.TemporaryDirectory() as directory:
|
|||||||
assert icon == '/favicon.svg'
|
assert icon == '/favicon.svg'
|
||||||
assert (output / icon.lstrip('/')).read_bytes() == (ROOT / 'site/favicon.svg').read_bytes()
|
assert (output / icon.lstrip('/')).read_bytes() == (ROOT / 'site/favicon.svg').read_bytes()
|
||||||
sitemap = ET.parse(output / 'sitemap.xml')
|
sitemap = ET.parse(output / 'sitemap.xml')
|
||||||
assert len(sitemap.getroot()) == len(supported) + 1
|
locations = {url[0].text for url in sitemap.getroot()}
|
||||||
|
assert len(locations) == len(supported)
|
||||||
|
assert 'https://www.opencadstudio.com/' in locations
|
||||||
|
assert 'https://www.opencadstudio.com/en-US/' not in locations
|
||||||
assert (output / 'index.html').read_bytes() == (output / 'en-US/index.html').read_bytes()
|
assert (output / 'index.html').read_bytes() == (output / 'en-US/index.html').read_bytes()
|
||||||
|
homepage_schemas = [json.loads(value) for value in re.findall(r'<script type="application/ld\+json">(.*?)</script>', (output / 'index.html').read_text())]
|
||||||
|
website = next(schema for schema in homepage_schemas if schema['@type'] == 'WebSite')
|
||||||
|
assert website['url'] == 'https://www.opencadstudio.com/' and 'OpenCADStudio' in website['alternateName']
|
||||||
subprocess.run(['node', 'scripts/test_site.cjs', str(output / 'site.js')], cwd=ROOT, check=True)
|
subprocess.run(['node', 'scripts/test_site.cjs', str(output / 'site.js')], cwd=ROOT, check=True)
|
||||||
print(f'All {len(supported)} translations, metadata, local links, charts, manifests and icons passed')
|
print(f'All {len(supported)} translations, metadata, local links, charts, manifests and icons passed')
|
||||||
@@ -1516,4 +1516,101 @@ mod tests {
|
|||||||
// Finish
|
// Finish
|
||||||
let _ = app.update(Message::CommandOptionPick(String::new()));
|
let _ = app.update(Message::CommandOptionPick(String::new()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mtp_in_line_command() {
|
||||||
|
use crate::app::Message;
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
{
|
||||||
|
app.tabs[0].scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
|
||||||
|
app.tabs[0].scene.sync_tiles_from_panes(1920.0, 1080.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start LINE
|
||||||
|
let _ = app.update(Message::CommandInput("LINE".to_string()));
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("LINE"));
|
||||||
|
|
||||||
|
// Type M2P
|
||||||
|
let _ = app.update(Message::CommandInput("M2P".to_string()));
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
|
||||||
|
assert!(app.tabs[0].suspended_cmd.is_some());
|
||||||
|
assert_eq!(app.tabs[0].suspended_cmd.as_ref().map(|c| c.name()), Some("LINE"));
|
||||||
|
|
||||||
|
// Point 1: 0,0
|
||||||
|
let _ = app.update(Message::CommandInput("0,0".to_string()));
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
|
||||||
|
|
||||||
|
// Point 2: 10,20
|
||||||
|
let _ = app.update(Message::CommandInput("10,20".to_string()));
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
|
||||||
|
// MTP should have finished and restored LINE, with midpoint (5, 10, 0)
|
||||||
|
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("LINE"));
|
||||||
|
assert!(app.tabs[0].suspended_cmd.is_none());
|
||||||
|
assert_eq!(app.last_point, Some(glam::DVec3::new(5.0, 10.0, 0.0)));
|
||||||
|
|
||||||
|
// Cancel LINE
|
||||||
|
let _ = app.update(Message::CommandEscape);
|
||||||
|
assert!(app.tabs[0].active_cmd.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mtp_escape_restores_parent() {
|
||||||
|
use crate::app::Message;
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
|
||||||
|
// Start LINE
|
||||||
|
let _ = app.update(Message::CommandInput("LINE".to_string()));
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("LINE"));
|
||||||
|
|
||||||
|
// Type MTP
|
||||||
|
let _ = app.update(Message::CommandInput("MTP".to_string()));
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
|
||||||
|
|
||||||
|
// Escape during MTP
|
||||||
|
let _ = app.update(Message::CommandEscape);
|
||||||
|
// Parent LINE must be restored, not cancelled!
|
||||||
|
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("LINE"));
|
||||||
|
assert!(app.tabs[0].suspended_cmd.is_none());
|
||||||
|
|
||||||
|
// Escape again cancels LINE
|
||||||
|
let _ = app.update(Message::CommandEscape);
|
||||||
|
assert!(app.tabs[0].active_cmd.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mtp_typing_routing_with_dyn_input() {
|
||||||
|
use crate::app::Message;
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
app.dyn_input = true;
|
||||||
|
|
||||||
|
// Start LINE
|
||||||
|
let _ = app.update(Message::CommandInput("LINE".to_string()));
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("LINE"));
|
||||||
|
|
||||||
|
// Simulate typing 'm', '2', 'p' character by character
|
||||||
|
let _ = app.update(Message::CommandAppendChar("m".to_string()));
|
||||||
|
assert_eq!(app.command_line.input, "M");
|
||||||
|
|
||||||
|
// The digit '2' must stay in command_line.input instead of routing to dyn fields!
|
||||||
|
let _ = app.update(Message::CommandAppendChar("2".to_string()));
|
||||||
|
assert_eq!(app.command_line.input, "M2");
|
||||||
|
|
||||||
|
let _ = app.update(Message::CommandAppendChar("p".to_string()));
|
||||||
|
assert_eq!(app.command_line.input, "M2P");
|
||||||
|
|
||||||
|
// Submit triggers MTP
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,6 +671,29 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
let is_mtp = token.trim_start_matches('_').eq_ignore_ascii_case("MTP")
|
||||||
|
|| token.trim_start_matches('_').eq_ignore_ascii_case("M2P");
|
||||||
|
if is_mtp {
|
||||||
|
let is_point_step = !self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.input_kind().wants_text())
|
||||||
|
.unwrap_or(true)
|
||||||
|
|| self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.point_step_accepts_keywords())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let not_entity_pick = !self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.needs_entity_pick())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if is_point_step && not_entity_pick {
|
||||||
|
self.start_mtp_modifier(i);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some((coord, kind)) = super::helpers::parse_coord(token) {
|
if let Some((coord, kind)) = super::helpers::parse_coord(token) {
|
||||||
// Match the GUI command line: typed coordinates are in the active
|
// Match the GUI command line: typed coordinates are in the active
|
||||||
// UCS (relative offsets are rotated by the UCS axes), so a multi-
|
// UCS (relative offsets are rotated by the UCS axes), so a multi-
|
||||||
@@ -712,7 +735,22 @@ impl OpenCADStudio {
|
|||||||
/// pick-first selector relaunching MOVE on the picked set works this way).
|
/// pick-first selector relaunching MOVE on the picked set works this way).
|
||||||
/// Pure-selection commands (SELECTALL, QSELECT, …) run without an active
|
/// Pure-selection commands (SELECTALL, QSELECT, …) run without an active
|
||||||
/// command, so `was_active` is false and their selection is preserved.
|
/// command, so `was_active` is false and their selection is preserved.
|
||||||
pub(super) fn apply_cmd_result(&mut self, result: CmdResult) -> Task<Message> {
|
pub(super) fn apply_cmd_result(&mut self, mut result: CmdResult) -> Task<Message> {
|
||||||
|
let i = self.active_tab;
|
||||||
|
if let CmdResult::ReturnPoint(pt) = result {
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
if let Some(mut suspended) = self.tabs[i].suspended_cmd.take() {
|
||||||
|
self.last_point = Some(pt);
|
||||||
|
let next_res = suspended.on_point(pt);
|
||||||
|
self.tabs[i].active_cmd = Some(suspended);
|
||||||
|
result = next_res;
|
||||||
|
} else {
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
}
|
||||||
let settings = self.tabs[self.active_tab]
|
let settings = self.tabs[self.active_tab]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1192,6 +1230,18 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(in crate::app) fn start_mtp_modifier(&mut self, i: usize) {
|
||||||
|
let parent = self.tabs[i].active_cmd.take();
|
||||||
|
self.tabs[i].suspended_cmd = parent;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
let cmd = crate::command::Mid2PointCommand::new();
|
||||||
|
let prompt = crate::command::CadCommand::prompt(&cmd);
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
|
self.command_line.push_info(&prompt);
|
||||||
|
self.command_line.set_step_options(Vec::new());
|
||||||
|
self.refresh_active_cmd_preview(i);
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task<Message> {
|
fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let preserve_commit_layer = self.tabs[i]
|
let preserve_commit_layer = self.tabs[i]
|
||||||
@@ -2108,7 +2158,7 @@ impl OpenCADStudio {
|
|||||||
} => {
|
} => {
|
||||||
let label = self.history_label_from_active_cmd(i, "SOLID");
|
let label = self.history_label_from_active_cmd(i, "SOLID");
|
||||||
let erase_source = erase_source.filter(|handle| {
|
let erase_source = erase_source.filter(|handle| {
|
||||||
self.tabs[i].scene.document.header.delete_objects
|
self.delete_objects != 0
|
||||||
&& !self.tabs[i].scene.is_layer_locked(*handle)
|
&& !self.tabs[i].scene.is_layer_locked(*handle)
|
||||||
});
|
});
|
||||||
let pending = self.begin_undo(
|
let pending = self.begin_undo(
|
||||||
@@ -2876,14 +2926,30 @@ impl OpenCADStudio {
|
|||||||
cancel @ (CmdResult::Cancel | CmdResult::CancelForSpaceChange) => {
|
cancel @ (CmdResult::Cancel | CmdResult::CancelForSpaceChange) => {
|
||||||
let space_changed = matches!(cancel, CmdResult::CancelForSpaceChange);
|
let space_changed = matches!(cancel, CmdResult::CancelForSpaceChange);
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
self.tabs[i].active_cmd = None;
|
if !space_changed && self.tabs[i].suspended_cmd.is_some() {
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].active_cmd = self.tabs[i].suspended_cmd.take();
|
||||||
self.restore_pre_cmd_tangent();
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||||
self.command_line.push_info(crate::t!(if space_changed {
|
if let Some(p) = prompt {
|
||||||
"Command cancelled because the active drawing space changed."
|
self.command_line.push_info(&p);
|
||||||
|
}
|
||||||
|
let opts = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.options())
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.command_line.set_step_options(opts);
|
||||||
|
self.refresh_active_cmd_preview(i);
|
||||||
} else {
|
} else {
|
||||||
"Command cancelled."
|
self.tabs[i].suspended_cmd = None;
|
||||||
}).as_ref());
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
self.command_line.push_info(crate::t!(if space_changed {
|
||||||
|
"Command cancelled because the active drawing space changed."
|
||||||
|
} else {
|
||||||
|
"Command cancelled."
|
||||||
|
}).as_ref());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
CmdResult::SelectByPath {
|
CmdResult::SelectByPath {
|
||||||
path,
|
path,
|
||||||
@@ -2971,7 +3037,7 @@ impl OpenCADStudio {
|
|||||||
CmdResult::EditTableCell { handle, point } => {
|
CmdResult::EditTableCell { handle, point } => {
|
||||||
// TABLEDIT's pick: end the pick phase and hand (table, point)
|
// TABLEDIT's pick: end the pick phase and hand (table, point)
|
||||||
// to the shared cell-edit launcher. A miss or a locked cell
|
// to the shared cell-edit launcher. A miss or a locked cell
|
||||||
// re-prompts, matching AutoCAD's select-until-valid loop.
|
// re-prompts until a valid table cell is selected.
|
||||||
use crate::modules::annotate::table_cmd::{table_cell_at, TableCellEditStart};
|
use crate::modules::annotate::table_cmd::{table_cell_at, TableCellEditStart};
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
@@ -4098,7 +4164,7 @@ impl OpenCADStudio {
|
|||||||
taper_angle,
|
taper_angle,
|
||||||
color: _,
|
color: _,
|
||||||
} => {
|
} => {
|
||||||
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
|
let delete_sources = self.delete_objects != 0;
|
||||||
if handles.is_empty()
|
if handles.is_empty()
|
||||||
|| handles
|
|| handles
|
||||||
.iter()
|
.iter()
|
||||||
@@ -4193,10 +4259,36 @@ impl OpenCADStudio {
|
|||||||
let direction = direction
|
let direction = direction
|
||||||
.or(path_direction)
|
.or(path_direction)
|
||||||
.unwrap_or(glam::DVec3::ZERO);
|
.unwrap_or(glam::DVec3::ZERO);
|
||||||
self.add_surface_model(
|
let history = path
|
||||||
empty_extruded_surface(direction, taper_angle),
|
.is_none()
|
||||||
body,
|
.then(|| {
|
||||||
)
|
sweep_model::extrusion_history(
|
||||||
|
&entity,
|
||||||
|
None,
|
||||||
|
direction.to_array(),
|
||||||
|
taper_angle,
|
||||||
|
profile.plane.origin,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
let mut surface = empty_extruded_surface(direction, taper_angle);
|
||||||
|
if let (
|
||||||
|
Some(acadrust::objects::SolidHistoryOperation::Extrusion(value)),
|
||||||
|
acadrust::EntityType::Surface(entity),
|
||||||
|
) = (&history, &mut surface)
|
||||||
|
{
|
||||||
|
if let Some(data) =
|
||||||
|
crate::scene::model::solid_history::extrusion_surface_data(value)
|
||||||
|
{
|
||||||
|
entity.surface_data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match history {
|
||||||
|
Some(history) => {
|
||||||
|
self.add_surface_model_with_history(surface, body, history)
|
||||||
|
}
|
||||||
|
None => self.add_surface_model(surface, body),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let direction = direction.unwrap_or(glam::DVec3::ZERO);
|
let direction = direction.unwrap_or(glam::DVec3::ZERO);
|
||||||
let history = path
|
let history = path
|
||||||
@@ -4385,7 +4477,7 @@ impl OpenCADStudio {
|
|||||||
self.restore_pre_cmd_tangent();
|
self.restore_pre_cmd_tangent();
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
|
let delete_sources = self.delete_objects != 0;
|
||||||
use crate::command::ExtrudeMode;
|
use crate::command::ExtrudeMode;
|
||||||
use crate::modules::insert::solid3d_cmds::{
|
use crate::modules::insert::solid3d_cmds::{
|
||||||
empty_revolved_surface, empty_solid3d,
|
empty_revolved_surface, empty_solid3d,
|
||||||
@@ -4519,7 +4611,8 @@ impl OpenCADStudio {
|
|||||||
use crate::command::ExtrudeMode;
|
use crate::command::ExtrudeMode;
|
||||||
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
||||||
use crate::scene::model::sweep_model;
|
use crate::scene::model::sweep_model;
|
||||||
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
|
let delete_objects = self.delete_objects;
|
||||||
|
let delete_profiles = delete_objects != 0;
|
||||||
let path = self.tabs[i].scene.document.get_entity(path_handle).cloned();
|
let path = self.tabs[i].scene.document.get_entity(path_handle).cloned();
|
||||||
let mut profiles = Vec::new();
|
let mut profiles = Vec::new();
|
||||||
let mut failed = 0usize;
|
let mut failed = 0usize;
|
||||||
@@ -4557,6 +4650,14 @@ impl OpenCADStudio {
|
|||||||
failed += 1;
|
failed += 1;
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let deletes_path = crate::app::delobj_deletes_auxiliary(
|
||||||
|
delete_objects,
|
||||||
|
surface,
|
||||||
|
);
|
||||||
|
if deletes_path && self.tabs[i].scene.is_layer_locked(path_handle) {
|
||||||
|
failed += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let created = if surface {
|
let created = if surface {
|
||||||
self.add_surface_model(sweep_model::swept_surface_entity(&record), body)
|
self.add_surface_model(sweep_model::swept_surface_entity(&record), body)
|
||||||
} else {
|
} else {
|
||||||
@@ -4569,9 +4670,12 @@ impl OpenCADStudio {
|
|||||||
failed += 1;
|
failed += 1;
|
||||||
} else {
|
} else {
|
||||||
created_handles.push(created);
|
created_handles.push(created);
|
||||||
if delete_sources {
|
if delete_profiles {
|
||||||
consumed.push(handle);
|
consumed.push(handle);
|
||||||
}
|
}
|
||||||
|
if deletes_path {
|
||||||
|
consumed.push(path_handle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !created_handles.is_empty() {
|
if !created_handles.is_empty() {
|
||||||
@@ -4612,12 +4716,14 @@ impl OpenCADStudio {
|
|||||||
}).collect::<Vec<_>>();
|
}).collect::<Vec<_>>();
|
||||||
sources.sort_unstable_by_key(|handle| handle.value());
|
sources.sort_unstable_by_key(|handle| handle.value());
|
||||||
sources.dedup();
|
sources.dedup();
|
||||||
let delete_sources = self.tabs[i].scene.document.header.delete_objects;
|
let delete_objects = self.delete_objects;
|
||||||
let locked = delete_sources && sources.iter().any(|handle| self.tabs[i].scene.is_layer_locked(*handle));
|
let delete_sections = delete_objects != 0;
|
||||||
|
let section_locked = delete_sections
|
||||||
|
&& sources.iter().any(|handle| self.tabs[i].scene.is_layer_locked(*handle));
|
||||||
let available = sources.iter().chain(guides.iter()).copied().chain(path)
|
let available = sources.iter().chain(guides.iter()).copied().chain(path)
|
||||||
.filter_map(|handle| self.tabs[i].scene.document.get_entity(handle)
|
.filter_map(|handle| self.tabs[i].scene.document.get_entity(handle)
|
||||||
.cloned().map(|entity| (handle, entity))).collect::<Vec<_>>();
|
.cloned().map(|entity| (handle, entity))).collect::<Vec<_>>();
|
||||||
let result = if locked {
|
let result = if section_locked {
|
||||||
Err("LOFT: a source is on a locked layer; disable source deletion or unlock it.".to_string())
|
Err("LOFT: a source is on a locked layer; disable source deletion or unlock it.".to_string())
|
||||||
} else {
|
} else {
|
||||||
loft_command_model::record(§ions, &guides, path, &available, mode, options)
|
loft_command_model::record(§ions, &guides, path, &available, mode, options)
|
||||||
@@ -4626,6 +4732,19 @@ impl OpenCADStudio {
|
|||||||
match result {
|
match result {
|
||||||
Ok((body, record)) => {
|
Ok((body, record)) => {
|
||||||
let surface = record.parameters.as_ref().is_some_and(|settings| settings.surface);
|
let surface = record.parameters.as_ref().is_some_and(|settings| settings.surface);
|
||||||
|
let delete_auxiliary = crate::app::delobj_deletes_auxiliary(
|
||||||
|
delete_objects,
|
||||||
|
surface,
|
||||||
|
);
|
||||||
|
if delete_auxiliary
|
||||||
|
&& guides.iter().copied().chain(path)
|
||||||
|
.any(|handle| self.tabs[i].scene.is_layer_locked(handle))
|
||||||
|
{
|
||||||
|
self.command_line.push_error(
|
||||||
|
"LOFT: a source is on a locked layer; disable source deletion or unlock it.",
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
let dirty_before = self.tabs[i].dirty;
|
let dirty_before = self.tabs[i].dirty;
|
||||||
let pending = self.begin_undo(i, "LOFT", 1, true);
|
let pending = self.begin_undo(i, "LOFT", 1, true);
|
||||||
let created = if surface {
|
let created = if surface {
|
||||||
@@ -4647,7 +4766,18 @@ impl OpenCADStudio {
|
|||||||
self.command_line.push_error(crate::t!("LOFT could not create a complete display. The source sections were preserved.").as_ref());
|
self.command_line.push_error(crate::t!("LOFT could not create a complete display. The source sections were preserved.").as_ref());
|
||||||
return Task::none();
|
return Task::none();
|
||||||
} else {
|
} else {
|
||||||
if delete_sources { self.tabs[i].scene.erase_entities(&sources); }
|
let mut consumed = if delete_sections {
|
||||||
|
sources.clone()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
if delete_auxiliary {
|
||||||
|
consumed.extend(guides.iter().copied());
|
||||||
|
consumed.extend(path);
|
||||||
|
}
|
||||||
|
consumed.sort_unstable_by_key(|handle| handle.value());
|
||||||
|
consumed.dedup();
|
||||||
|
self.tabs[i].scene.erase_entities(&consumed);
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
self.tabs[i].scene.select_entity(created, true);
|
self.tabs[i].scene.select_entity(created, true);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
@@ -5124,6 +5254,12 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
self.restore_pre_cmd_tangent();
|
self.restore_pre_cmd_tangent();
|
||||||
}
|
}
|
||||||
|
CmdResult::ReturnPoint(_) => {
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// When no command is running the ribbon tool button still has to
|
// When no command is running the ribbon tool button still has to
|
||||||
// visually deactivate. Keyboard focus is assigned below to whichever
|
// visually deactivate. Keyboard focus is assigned below to whichever
|
||||||
@@ -6682,6 +6818,71 @@ mod sketch_constraint_undo_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||||
|
mod delobj_tests {
|
||||||
|
use super::*;
|
||||||
|
use acadrust::types::Vector3;
|
||||||
|
|
||||||
|
fn sweep_source_presence(value: i16, mode: crate::command::ExtrudeMode) -> (bool, bool) {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
let mut circle = acadrust::Circle::new();
|
||||||
|
circle.radius = 2.0;
|
||||||
|
let profile = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.add_entity(acadrust::EntityType::Circle(circle));
|
||||||
|
let path = app.tabs[i].scene.add_entity(acadrust::EntityType::Line(
|
||||||
|
acadrust::Line::from_points(
|
||||||
|
Vector3::new(0.0, 0.0, 0.0),
|
||||||
|
Vector3::new(0.0, 0.0, 5.0),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
app.delete_objects = value;
|
||||||
|
|
||||||
|
let _ = app.apply_cmd_result(CmdResult::SweepEntities {
|
||||||
|
handles: vec![profile],
|
||||||
|
path_handle: path,
|
||||||
|
mode,
|
||||||
|
options: crate::command::SweepOptions::default(),
|
||||||
|
color: [1.0; 4],
|
||||||
|
});
|
||||||
|
|
||||||
|
let created = app.tabs[i].scene.document.entities().any(|entity| match mode {
|
||||||
|
crate::command::ExtrudeMode::Solid => {
|
||||||
|
matches!(entity, acadrust::EntityType::Solid3D(_))
|
||||||
|
}
|
||||||
|
crate::command::ExtrudeMode::Surface => {
|
||||||
|
matches!(entity, acadrust::EntityType::Surface(_))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert!(created, "SWEEP must produce the requested result");
|
||||||
|
(
|
||||||
|
app.tabs[i].scene.document.get_entity(profile).is_some(),
|
||||||
|
app.tabs[i].scene.document.get_entity(path).is_some(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sweep_applies_all_four_source_deletion_modes() {
|
||||||
|
let solid_expectations = [(true, true), (false, true), (false, false), (false, false)];
|
||||||
|
for (value, expected) in solid_expectations.into_iter().enumerate() {
|
||||||
|
assert_eq!(
|
||||||
|
sweep_source_presence(value as i16, crate::command::ExtrudeMode::Solid),
|
||||||
|
expected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
sweep_source_presence(2, crate::command::ExtrudeMode::Surface),
|
||||||
|
(false, false),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sweep_source_presence(3, crate::command::ExtrudeMode::Surface),
|
||||||
|
(false, true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||||
mod thicken_tests {
|
mod thicken_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -502,13 +502,9 @@ impl OpenCADStudio {
|
|||||||
self.command_line.push_output(crate::t!("REDRAWALL: viewports refreshed.").as_ref());
|
self.command_line.push_output(crate::t!("REDRAWALL: viewports refreshed.").as_ref());
|
||||||
return Some(Task::none());
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
// REGEN — full model regeneration (bump_geometry: geometry_epoch AND
|
// Rebuild tessellation caches without modifying the document.
|
||||||
// block_epoch; C4). No undo, no DB mutation, so do NOT touch
|
|
||||||
// self.tabs[i].dirty — a newly opened drawing must not become
|
|
||||||
// "modified" merely because tessellation caches were invalidated (C7).
|
|
||||||
// REGENALL is functionally identical (C5).
|
|
||||||
"REGEN" | "REGENALL" => {
|
"REGEN" | "REGENALL" => {
|
||||||
self.tabs[i].scene.bump_geometry();
|
self.tabs[i].scene.populate_meshes_from_document();
|
||||||
self.command_line.push_output(crate::t!("REGEN: regenerated model.").as_ref());
|
self.command_line.push_output(crate::t!("REGEN: regenerated model.").as_ref());
|
||||||
return Some(Task::none());
|
return Some(Task::none());
|
||||||
}
|
}
|
||||||
@@ -1892,4 +1888,86 @@ mod tests {
|
|||||||
let _ = app.run_command_line("REGENALL");
|
let _ = app.run_command_line("REGENALL");
|
||||||
assert!(!app.tabs[i].dirty, "REGENALL must not dirty the document either");
|
assert!(!app.tabs[i].dirty, "REGENALL must not dirty the document either");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn regen_rebuilds_the_mesh_map_rather_than_only_bumping_the_epoch() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
|
||||||
|
let stale = acadrust::Handle::new(0xDEAD);
|
||||||
|
app.tabs[i].scene.meshes.insert(stale, stale_mesh());
|
||||||
|
let epoch_before = app.tabs[i].scene.geometry_epoch;
|
||||||
|
|
||||||
|
let _ = app.run_command_line("REGEN");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!app.tabs[i].scene.meshes.contains_key(&stale),
|
||||||
|
"REGEN left a stale mesh",
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
app.tabs[i].scene.geometry_epoch, epoch_before,
|
||||||
|
"REGEN did not bump the geometry epoch",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_isolines_slider_rebuilds_once_on_release_and_not_while_dragging() {
|
||||||
|
use crate::app::Message;
|
||||||
|
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
|
||||||
|
let seed = |app: &mut OpenCADStudio, i: usize| {
|
||||||
|
let stale = acadrust::Handle::new(0xBEEF);
|
||||||
|
app.tabs[i].scene.meshes.insert(stale, stale_mesh());
|
||||||
|
stale
|
||||||
|
};
|
||||||
|
let stale = seed(&mut app, i);
|
||||||
|
|
||||||
|
for value in [4i16, 3, 2, 1, 0] {
|
||||||
|
let _ = app.update(Message::IsolinesChanged(value));
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].scene.meshes.contains_key(&stale),
|
||||||
|
"the drag itself must not rebuild the meshes",
|
||||||
|
);
|
||||||
|
assert_eq!(app.tabs[i].scene.document.header.isolines, 0);
|
||||||
|
|
||||||
|
let _ = app.update(Message::IsolinesReleased);
|
||||||
|
assert!(
|
||||||
|
!app.tabs[i].scene.meshes.contains_key(&stale),
|
||||||
|
"releasing after a change rebuilds them",
|
||||||
|
);
|
||||||
|
|
||||||
|
let stale = seed(&mut app, i);
|
||||||
|
let _ = app.update(Message::IsolinesChanged(0));
|
||||||
|
let _ = app.update(Message::IsolinesReleased);
|
||||||
|
assert!(
|
||||||
|
app.tabs[i].scene.meshes.contains_key(&stale),
|
||||||
|
"releasing without a change must rebuild nothing",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stale_mesh() -> crate::scene::model::mesh_model::MeshLodSet {
|
||||||
|
crate::scene::model::mesh_model::MeshLodSet {
|
||||||
|
lods: Vec::new(),
|
||||||
|
material: None,
|
||||||
|
face_materials: Default::default(),
|
||||||
|
visual_style: None,
|
||||||
|
complete: true,
|
||||||
|
edge_verts: Vec::new(),
|
||||||
|
edge_verts_low: Vec::new(),
|
||||||
|
curved_gens: Vec::new(),
|
||||||
|
metrics: Default::default(),
|
||||||
|
world_aabb: [0.0; 4],
|
||||||
|
z_aabb: [0.0; 2],
|
||||||
|
instance_source: None,
|
||||||
|
instance_transform: None,
|
||||||
|
instance_handle: None,
|
||||||
|
instance_color: None,
|
||||||
|
instance_aabb: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ impl OpenCADStudio {
|
|||||||
return Some(Task::done(Message::ImagePick));
|
return Some(Task::done(Message::ImagePick));
|
||||||
}
|
}
|
||||||
|
|
||||||
"REVCLOUD" => {
|
"REVCLOUD" | "REVCLOUD_RECTANGULAR" | "REVCLOUD_POLYGONAL" | "REVCLOUD_FREEHAND" => {
|
||||||
use crate::modules::draw::draw::revcloud::RevCloudCommand;
|
use crate::modules::draw::draw::revcloud::RevCloudCommand;
|
||||||
let view_height = self.tabs[i].scene.camera.borrow().ortho_size() as f64 * 2.0;
|
let view_height = self.tabs[i].scene.camera.borrow().ortho_size() as f64 * 2.0;
|
||||||
let default_arc_length = (view_height * 0.0125).max(1.0e-6);
|
let default_arc_length = (view_height * 0.0125).max(1.0e-6);
|
||||||
@@ -72,9 +72,12 @@ impl OpenCADStudio {
|
|||||||
.entities()
|
.entities()
|
||||||
.map(|entity| (entity.common().handle, entity.clone()))
|
.map(|entity| (entity.common().handle, entity.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
let cmd = RevCloudCommand::new(default_arc_length, sources);
|
let mut command = RevCloudCommand::new(default_arc_length, sources);
|
||||||
self.command_line.push_info(&cmd.prompt());
|
if let Some(mode) = cmd.strip_prefix("REVCLOUD_") {
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
command.on_text_input(mode);
|
||||||
|
}
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
}
|
}
|
||||||
|
|
||||||
"ATTDEF" => {
|
"ATTDEF" => {
|
||||||
@@ -872,9 +875,13 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
"SPLINE" => {
|
"SPLINE" | "SPLINECV" => {
|
||||||
use crate::modules::draw::draw::spline::SplineCommand;
|
use crate::modules::draw::draw::spline::SplineCommand;
|
||||||
let new_cmd = SplineCommand::new();
|
let new_cmd = if cmd == "SPLINECV" {
|
||||||
|
SplineCommand::control_vertices()
|
||||||
|
} else {
|
||||||
|
SplineCommand::new()
|
||||||
|
};
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
@@ -1299,10 +1306,18 @@ impl OpenCADStudio {
|
|||||||
// REGION — convert selected closed boundaries (closed polylines /
|
// REGION — convert selected closed boundaries (closed polylines /
|
||||||
// circles) into Region entities (one wire loop each).
|
// circles) into Region entities (one wire loop each).
|
||||||
"REGION" | "REG" => {
|
"REGION" | "REG" => {
|
||||||
|
if self.tabs[i].scene.selected_entities().is_empty() {
|
||||||
|
use crate::modules::draw::select::SelectObjectsCommand;
|
||||||
|
let command = SelectObjectsCommand::new("REGION");
|
||||||
|
self.command_line.push_info(&command.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
|
return Some(iced::Task::none());
|
||||||
|
}
|
||||||
use acadrust::entities::Region;
|
use acadrust::entities::Region;
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
let mut regions = Vec::new();
|
let mut regions = Vec::new();
|
||||||
for (_, e) in self.tabs[i].scene.selected_entities().iter() {
|
let mut sources = Vec::new();
|
||||||
|
for (handle, e) in self.tabs[i].scene.selected_entities().iter() {
|
||||||
let supported = matches!(
|
let supported = matches!(
|
||||||
e,
|
e,
|
||||||
acadrust::EntityType::LwPolyline(pl)
|
acadrust::EntityType::LwPolyline(pl)
|
||||||
@@ -1325,6 +1340,7 @@ impl OpenCADStudio {
|
|||||||
);
|
);
|
||||||
region.common.layer = self.tabs[i].active_layer.clone();
|
region.common.layer = self.tabs[i].active_layer.clone();
|
||||||
regions.push((region, body));
|
regions.push((region, body));
|
||||||
|
sources.push(*handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if regions.is_empty() {
|
if regions.is_empty() {
|
||||||
@@ -1343,6 +1359,10 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
created.push(handle);
|
created.push(handle);
|
||||||
}
|
}
|
||||||
|
if self.delete_objects != 0 {
|
||||||
|
self.tabs[i].scene.erase_entities(&sources);
|
||||||
|
self.refresh_properties();
|
||||||
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::tf!("REGION: created {count} region(s).").as_ref());
|
.push_output(crate::tf!("REGION: created {count} region(s).").as_ref());
|
||||||
@@ -1719,3 +1739,47 @@ impl OpenCADStudio {
|
|||||||
Some(self.finish_dispatch(cmd))
|
Some(self.finish_dispatch(cmd))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod region_tests {
|
||||||
|
use crate::app::OpenCADStudio;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn region_respects_delobj_and_preserves_unconverted_sources() {
|
||||||
|
for delete_sources in [false, true] {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
app.automation_op(r#"{"op":"run","cmd":"CIRCLE 5,5 3"}"#);
|
||||||
|
app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,10"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
let sources: Vec<_> = app.tabs[i].scene.document.entities()
|
||||||
|
.map(|entity| {
|
||||||
|
(entity.common().handle, matches!(entity, acadrust::EntityType::Circle(_)))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(sources.len(), 2);
|
||||||
|
let handles: Vec<_> = sources.iter().map(|(handle, _)| *handle).collect();
|
||||||
|
app.tabs[i].scene.select_entities(&handles);
|
||||||
|
app.delete_objects = i16::from(delete_sources);
|
||||||
|
|
||||||
|
let _ = app.dispatch_command("REGION");
|
||||||
|
|
||||||
|
for (handle, is_circle) in &sources {
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].scene.document.get_entity(*handle).is_some(),
|
||||||
|
!delete_sources || !is_circle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let region_count = app.tabs[i].scene.document.entities()
|
||||||
|
.filter(|entity| matches!(entity, acadrust::EntityType::Region(_)))
|
||||||
|
.count();
|
||||||
|
assert_eq!(region_count, 1);
|
||||||
|
|
||||||
|
app.automation_op(r#"{"op":"undo"}"#);
|
||||||
|
for (handle, _) in &sources {
|
||||||
|
assert!(app.tabs[i].scene.document.get_entity(*handle).is_some());
|
||||||
|
}
|
||||||
|
assert_eq!(app.tabs[i].scene.document.entities().count(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,6 +75,12 @@ impl OpenCADStudio {
|
|||||||
.dispatch_families(cmd, i)
|
.dispatch_families(cmd, i)
|
||||||
.unwrap_or_else(Task::none);
|
.unwrap_or_else(Task::none);
|
||||||
}
|
}
|
||||||
|
// A new command abandons any grip edit and its numeric input.
|
||||||
|
let had_pending_grip_input = self.grip_pending.take().is_some();
|
||||||
|
let had_active_grip = self.cancel_active_grip_edit();
|
||||||
|
if had_pending_grip_input || had_active_grip {
|
||||||
|
self.command_line.input.clear();
|
||||||
|
}
|
||||||
// Starting a command closes any open ribbon dropdown (e.g. a style
|
// Starting a command closes any open ribbon dropdown (e.g. a style
|
||||||
// combo left open) so it does not stay stuck behind the new tool.
|
// combo left open) so it does not stay stuck behind the new tool.
|
||||||
self.ribbon.close_dropdown();
|
self.ribbon.close_dropdown();
|
||||||
@@ -553,6 +559,11 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||||||
"GRIPHOT",
|
"GRIPHOT",
|
||||||
"GRIPHOVER",
|
"GRIPHOVER",
|
||||||
"GRIPOBJLIMIT",
|
"GRIPOBJLIMIT",
|
||||||
|
// Dispatched all along, but absent from the registry, so command-line
|
||||||
|
// completion never offered them.
|
||||||
|
"ISAVEBAK",
|
||||||
|
"SAVETIME",
|
||||||
|
"FILEASSOC",
|
||||||
// Reset selected entities' overrides to follow their layer.
|
// Reset selected entities' overrides to follow their layer.
|
||||||
"SETBYLAYER",
|
"SETBYLAYER",
|
||||||
// Remove duplicate objects; set drawing base point; audit integrity;
|
// Remove duplicate objects; set drawing base point; audit integrity;
|
||||||
@@ -702,7 +713,10 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod marquee_cancel_tests {
|
mod marquee_cancel_tests {
|
||||||
use crate::app::OpenCADStudio;
|
use crate::app::{GripPendingValue, OpenCADStudio};
|
||||||
|
use crate::scene::model::object::GripMenuAction;
|
||||||
|
use crate::scene::pick::grip::GripEdit;
|
||||||
|
use acadrust::Handle;
|
||||||
use iced::time::Instant;
|
use iced::time::Instant;
|
||||||
|
|
||||||
fn fresh() -> OpenCADStudio {
|
fn fresh() -> OpenCADStudio {
|
||||||
@@ -816,4 +830,33 @@ mod marquee_cancel_tests {
|
|||||||
assert!(sel.box_crossing);
|
assert!(sel.box_crossing);
|
||||||
assert!(sel.box_crossing_locked);
|
assert!(sel.box_crossing_locked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn starting_a_command_cancels_a_grip_value_edit() {
|
||||||
|
let mut app = fresh();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let handle = Handle::new(42);
|
||||||
|
app.tabs[i].active_grip = Some(GripEdit::radius(
|
||||||
|
handle,
|
||||||
|
1,
|
||||||
|
glam::DVec3::new(2.0, 0.0, 0.0),
|
||||||
|
));
|
||||||
|
app.grip_pending = Some(GripPendingValue {
|
||||||
|
handle,
|
||||||
|
grip_id: 1,
|
||||||
|
action: GripMenuAction::Radius,
|
||||||
|
label: "New radius",
|
||||||
|
});
|
||||||
|
app.command_line.input = "5".to_string();
|
||||||
|
|
||||||
|
let _ = app.dispatch_command("LINE");
|
||||||
|
|
||||||
|
assert!(app.tabs[i].active_grip.is_none());
|
||||||
|
assert!(app.grip_pending.is_none());
|
||||||
|
assert!(app.command_line.input.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
app.tabs[i].active_cmd.as_deref().map(|cmd| cmd.name()),
|
||||||
|
Some("LINE")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1159,27 +1159,45 @@ impl OpenCADStudio {
|
|||||||
"0" | "OFF" | "FALSE" => Some(false),
|
"0" | "OFF" | "FALSE" => Some(false),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
let current_delete_objects = self.delete_objects;
|
||||||
|
let requested_delete_objects = (name == "DELOBJ")
|
||||||
|
.then(|| value.as_deref()?.parse::<i16>().ok())
|
||||||
|
.flatten()
|
||||||
|
.filter(|value| (0..=3).contains(value));
|
||||||
let outcome: Result<(String, bool), String> = {
|
let outcome: Result<(String, bool), String> = {
|
||||||
let h = &mut self.tabs[i].scene.document.header;
|
let h = &mut self.tabs[i].scene.document.header;
|
||||||
match name.as_str() {
|
match name.as_str() {
|
||||||
|
// The standalone LTSCALE and CELTSCALE commands
|
||||||
|
// require a positive value; reaching the same
|
||||||
|
// variable through SETVAR used to accept zero or a
|
||||||
|
// negative one, which degenerates every dash
|
||||||
|
// pattern in the drawing.
|
||||||
"LTSCALE" => match &value {
|
"LTSCALE" => match &value {
|
||||||
Some(v) => v
|
Some(v) => v
|
||||||
.parse::<f64>()
|
.parse::<f64>()
|
||||||
|
.ok()
|
||||||
|
.filter(|x| x.is_finite() && *x > 0.0)
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
h.linetype_scale = x;
|
h.linetype_scale = x;
|
||||||
(format!("LTSCALE = {x}"), true)
|
(format!("LTSCALE = {x}"), true)
|
||||||
})
|
})
|
||||||
.map_err(|_| "SETVAR: numeric value required.".into()),
|
.ok_or_else(|| {
|
||||||
|
"SETVAR: positive numeric value required.".into()
|
||||||
|
}),
|
||||||
None => Ok((format!("LTSCALE = {}", h.linetype_scale), false)),
|
None => Ok((format!("LTSCALE = {}", h.linetype_scale), false)),
|
||||||
},
|
},
|
||||||
"CELTSCALE" => match &value {
|
"CELTSCALE" => match &value {
|
||||||
Some(v) => v
|
Some(v) => v
|
||||||
.parse::<f64>()
|
.parse::<f64>()
|
||||||
|
.ok()
|
||||||
|
.filter(|x| x.is_finite() && *x > 0.0)
|
||||||
.map(|x| {
|
.map(|x| {
|
||||||
h.current_entity_linetype_scale = x;
|
h.current_entity_linetype_scale = x;
|
||||||
(format!("CELTSCALE = {x}"), true)
|
(format!("CELTSCALE = {x}"), true)
|
||||||
})
|
})
|
||||||
.map_err(|_| "SETVAR: numeric value required.".into()),
|
.ok_or_else(|| {
|
||||||
|
"SETVAR: positive numeric value required.".into()
|
||||||
|
}),
|
||||||
None => Ok((
|
None => Ok((
|
||||||
format!("CELTSCALE = {}", h.current_entity_linetype_scale),
|
format!("CELTSCALE = {}", h.current_entity_linetype_scale),
|
||||||
false,
|
false,
|
||||||
@@ -1633,15 +1651,18 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DELOBJ" => match &value {
|
"DELOBJ" => match &value {
|
||||||
Some(v) => parse_bool(v)
|
Some(v) => v
|
||||||
.map(|b| {
|
.parse::<i16>()
|
||||||
h.delete_objects = b;
|
.ok()
|
||||||
(format!("DELOBJ = {}", b as i32), true)
|
.filter(|value| (0..=3).contains(value))
|
||||||
|
.map(|value| {
|
||||||
|
(format!("DELOBJ = {value}"), true)
|
||||||
})
|
})
|
||||||
.ok_or_else(|| "SETVAR: 0 or 1 required.".into()),
|
.ok_or_else(|| "SETVAR: integer from 0 to 3 required.".into()),
|
||||||
None => {
|
None => Ok((
|
||||||
Ok((format!("DELOBJ = {}", h.delete_objects as i32), false))
|
format!("DELOBJ = {current_delete_objects}"),
|
||||||
}
|
false,
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
"PLINEGEN" => match &value {
|
"PLINEGEN" => match &value {
|
||||||
Some(v) => parse_bool(v)
|
Some(v) => parse_bool(v)
|
||||||
@@ -2119,6 +2140,9 @@ impl OpenCADStudio {
|
|||||||
};
|
};
|
||||||
match outcome {
|
match outcome {
|
||||||
Ok((msg, changed)) => {
|
Ok((msg, changed)) => {
|
||||||
|
if let Some(value) = requested_delete_objects {
|
||||||
|
self.delete_objects = value;
|
||||||
|
}
|
||||||
if changed {
|
if changed {
|
||||||
if matches!(
|
if matches!(
|
||||||
name.as_str(),
|
name.as_str(),
|
||||||
@@ -2144,6 +2168,13 @@ impl OpenCADStudio {
|
|||||||
| "GRIPHOT"
|
| "GRIPHOT"
|
||||||
| "GRIPHOVER"
|
| "GRIPHOVER"
|
||||||
| "GRIPOBJLIMIT"
|
| "GRIPOBJLIMIT"
|
||||||
|
| "DELOBJ"
|
||||||
|
// App-level, not part of the drawing:
|
||||||
|
// it lives in a process global and is
|
||||||
|
// snapshotted into the settings file.
|
||||||
|
// Without this it marked the drawing
|
||||||
|
// modified instead of persisting.
|
||||||
|
| "TEXTFILL"
|
||||||
) {
|
) {
|
||||||
self.persist_settings_if_changed();
|
self.persist_settings_if_changed();
|
||||||
} else {
|
} else {
|
||||||
@@ -2172,9 +2203,7 @@ impl OpenCADStudio {
|
|||||||
// TEXTFILL reset the glyph atlas; re-tessellate so text picks
|
// TEXTFILL reset the glyph atlas; re-tessellate so text picks
|
||||||
// up the re-baked filled / hollow tiles.
|
// up the re-baked filled / hollow tiles.
|
||||||
if name == "TEXTFILL" {
|
if name == "TEXTFILL" {
|
||||||
self.tabs[i]
|
self.invalidate_text_everywhere();
|
||||||
.scene
|
|
||||||
.invalidate_text_geometry_dependencies();
|
|
||||||
}
|
}
|
||||||
// LTSCALE scales the dash pattern baked into every wire, and
|
// LTSCALE scales the dash pattern baked into every wire, and
|
||||||
// PDMODE / PDSIZE decide the point glyph built at tessellation
|
// PDMODE / PDSIZE decide the point glyph built at tessellation
|
||||||
@@ -3120,4 +3149,50 @@ mod tests {
|
|||||||
assert!(app.tabs[app.active_tab].active_cmd.is_none());
|
assert!(app.tabs[app.active_tab].active_cmd.is_none());
|
||||||
assert_eq!(app.commandline_fade_ms, 5000);
|
assert_eq!(app.commandline_fade_ms, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delobj_accepts_values_zero_through_three() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
|
||||||
|
for value in 0..=3 {
|
||||||
|
let _ = app.run_command_line(&format!("SETVAR DELOBJ {value}"));
|
||||||
|
assert_eq!(app.delete_objects, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = app.run_command_line("SETVAR DELOBJ 4");
|
||||||
|
assert_eq!(app.delete_objects, 3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod scale_validation_tests {
|
||||||
|
use crate::app::OpenCADStudio;
|
||||||
|
|
||||||
|
/// A linetype scale of zero or less degenerates every dash pattern in the
|
||||||
|
/// drawing, which is why the standalone commands reject it. Reaching the
|
||||||
|
/// same variable through SETVAR used to accept it, so which door you came
|
||||||
|
/// through decided whether the drawing could be broken.
|
||||||
|
#[test]
|
||||||
|
fn both_doors_to_ltscale_refuse_a_non_positive_value() {
|
||||||
|
for name in ["LTSCALE", "CELTSCALE"] {
|
||||||
|
for entry in [format!("{name} -5"), format!("SETVAR {name} -5")] {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
let before = if name == "LTSCALE" {
|
||||||
|
app.tabs[i].scene.document.header.linetype_scale
|
||||||
|
} else {
|
||||||
|
app.tabs[i].scene.document.header.current_entity_linetype_scale
|
||||||
|
};
|
||||||
|
let _ = app.run_command_line(&entry);
|
||||||
|
let after = if name == "LTSCALE" {
|
||||||
|
app.tabs[i].scene.document.header.linetype_scale
|
||||||
|
} else {
|
||||||
|
app.tabs[i].scene.document.header.current_entity_linetype_scale
|
||||||
|
};
|
||||||
|
assert_eq!(before, after, "{entry} must be refused");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -475,7 +475,16 @@ impl AppConfig {
|
|||||||
|
|
||||||
/// Persist the config as JSON. Best-effort; silent on unavailable or
|
/// Persist the config as JSON. Best-effort; silent on unavailable or
|
||||||
/// read-only storage.
|
/// read-only storage.
|
||||||
|
///
|
||||||
|
/// Does nothing under `cfg(test)`. The path is the developer's own
|
||||||
|
/// settings file, and the suite builds whole applications and changes
|
||||||
|
/// preferences on them — without this, running `cargo test` rewrites the
|
||||||
|
/// settings of whoever ran it.
|
||||||
pub fn save(&self) {
|
pub fn save(&self) {
|
||||||
|
if cfg!(test) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
let Some(path) = config_path() else { return };
|
let Some(path) = config_path() else { return };
|
||||||
|
|||||||
@@ -347,6 +347,10 @@ pub(crate) enum TextEntryMode {
|
|||||||
FreeText,
|
FreeText,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn delobj_deletes_auxiliary(value: i16, creates_surface: bool) -> bool {
|
||||||
|
value == 2 || (value == 3 && !creates_surface)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) struct OpenCADStudio {
|
pub(super) struct OpenCADStudio {
|
||||||
start: Instant,
|
start: Instant,
|
||||||
control: control::State,
|
control: control::State,
|
||||||
@@ -513,6 +517,11 @@ pub(super) struct OpenCADStudio {
|
|||||||
crosshair_color: Option<[u8; 3]>,
|
crosshair_color: Option<[u8; 3]>,
|
||||||
/// Editable Options buffer for the crosshair colour.
|
/// Editable Options buffer for the crosshair colour.
|
||||||
crosshair_color_input: String,
|
crosshair_color_input: String,
|
||||||
|
/// Defer the ISOLINES mesh rebuild until the slider is released.
|
||||||
|
isolines_awaiting_regen: bool,
|
||||||
|
/// Edit buffer for the SNAPANG field on the Options Drafting page. Kept
|
||||||
|
/// separate from `snap_angle_deg` so a half-typed angle is not parsed.
|
||||||
|
snap_angle_input: String,
|
||||||
/// Model-space lineweight preview scale, in percent (25..=200).
|
/// Model-space lineweight preview scale, in percent (25..=200).
|
||||||
lineweight_display_scale: i32,
|
lineweight_display_scale: i32,
|
||||||
/// Whole-interface scale, in percent (50..=200) — drives iced's own
|
/// Whole-interface scale, in percent (50..=200) — drives iced's own
|
||||||
@@ -592,6 +601,8 @@ pub(super) struct OpenCADStudio {
|
|||||||
/// `MIRRTEXT`) — the next command-line entry is its new value, empty keeps
|
/// `MIRRTEXT`) — the next command-line entry is its new value, empty keeps
|
||||||
/// the current one.
|
/// the current one.
|
||||||
pending_setvar: Option<String>,
|
pending_setvar: Option<String>,
|
||||||
|
/// DELOBJ system variable (0–3), shared by every open drawing.
|
||||||
|
delete_objects: i16,
|
||||||
/// Cursor is hovering over the UCS icon body — drives the hover highlight.
|
/// Cursor is hovering over the UCS icon body — drives the hover highlight.
|
||||||
ucs_icon_hover: bool,
|
ucs_icon_hover: bool,
|
||||||
/// UCS icon is selected (clicked): its grips are shown and draggable.
|
/// UCS icon is selected (clicked): its grips are shown and draggable.
|
||||||
@@ -2043,6 +2054,58 @@ pub enum Message {
|
|||||||
ShiftToAddToggled(bool),
|
ShiftToAddToggled(bool),
|
||||||
/// Toggle press-and-drag drawing a rectangle instead of a lasso (PICKDRAG).
|
/// Toggle press-and-drag drawing a rectangle instead of a lasso (PICKDRAG).
|
||||||
PickDragRectToggled(bool),
|
PickDragRectToggled(bool),
|
||||||
|
/// Change the automatic-save interval in minutes; 0 disables it (SAVETIME).
|
||||||
|
SaveTimeChanged(i32),
|
||||||
|
/// Toggle keeping a `.bak` copy when overwriting a drawing (ISAVEBAK).
|
||||||
|
BackupOnSaveChanged(bool),
|
||||||
|
/// Toggle filled TrueType glyphs (TEXTFILL).
|
||||||
|
TextFillChanged(bool),
|
||||||
|
/// Change how many prompt lines sit above the command window (CLIPROMPTLINES).
|
||||||
|
ClipromptLinesChanged(i32),
|
||||||
|
/// Change how long command-line history lines stay visible (COMMANDLINEFADETIME).
|
||||||
|
CommandLineFadeChanged(i32),
|
||||||
|
/// Toggle reversing the mouse-wheel zoom direction (ZOOMWHEEL).
|
||||||
|
ZoomWheelReversedChanged(bool),
|
||||||
|
/// Change how far one wheel notch zooms (ZOOMFACTOR, 3..=100).
|
||||||
|
ZoomFactorChanged(i32),
|
||||||
|
/// Toggle TEXTEDIT ending after one object (TEXTEDITMODE).
|
||||||
|
TextEditModeChanged(bool),
|
||||||
|
/// Toggle continued dimensions inheriting the base style (DIMCONTINUEMODE).
|
||||||
|
DimContinueModeChanged(bool),
|
||||||
|
/// Change which points QDIM measures from (0 endpoints, 1 intersections).
|
||||||
|
QdimSnapPriorityChanged(u8),
|
||||||
|
/// Change which annotative objects pick up a new scale (ANNOAUTOSCALE).
|
||||||
|
AnnoAutoScaleChanged(i8),
|
||||||
|
/// Edit the drafting rotation field; parsed when it holds a valid angle (SNAPANG).
|
||||||
|
SnapAngleInputChanged(String),
|
||||||
|
/// Change the polar tracking increment in degrees.
|
||||||
|
PolarIncrementChanged(f32),
|
||||||
|
/// Toggle the navigation cube (NAVVCUBE).
|
||||||
|
ShowViewCubeChanged(bool),
|
||||||
|
/// Toggle the UCS icon (UCSICON).
|
||||||
|
ShowUcsIconChanged(bool),
|
||||||
|
/// Toggle drawing the UCS icon at the origin (UCSICON ORigin).
|
||||||
|
UcsIconAtOriginChanged(bool),
|
||||||
|
/// Toggle selection cycling from Options; the status-bar pill toggles the same flag.
|
||||||
|
SelectionCyclingChanged(bool),
|
||||||
|
/// Reveal one of the application's own folders in the system file manager.
|
||||||
|
OpenFolder(String),
|
||||||
|
/// Change isolines per surface in the current drawing (ISOLINES).
|
||||||
|
IsolinesChanged(i16),
|
||||||
|
/// The isolines slider was released; rebuild the meshes if it moved.
|
||||||
|
IsolinesReleased,
|
||||||
|
/// Toggle silhouette edges in the current drawing (DISPSILH).
|
||||||
|
DispSilhChanged(bool),
|
||||||
|
/// Change surface density U in the current drawing (SURFU).
|
||||||
|
SurfaceUChanged(i16),
|
||||||
|
/// Change surface density V in the current drawing (SURFV).
|
||||||
|
SurfaceVChanged(i16),
|
||||||
|
/// Change the surface type in the current drawing (SURFTYPE).
|
||||||
|
SurfaceTypeChanged(i16),
|
||||||
|
/// Toggle recording composite-solid history in the current drawing (SOLIDHIST).
|
||||||
|
SolidHistChanged(bool),
|
||||||
|
/// Change when solid history is shown in the current drawing (SHOWHIST).
|
||||||
|
ShowHistChanged(i16),
|
||||||
/// Restore Model Space display/canvas appearance to defaults.
|
/// Restore Model Space display/canvas appearance to defaults.
|
||||||
RestoreModelSpaceDisplayDefaults,
|
RestoreModelSpaceDisplayDefaults,
|
||||||
/// Restore Selection visual effect settings to defaults.
|
/// Restore Selection visual effect settings to defaults.
|
||||||
@@ -3500,6 +3563,8 @@ impl OpenCADStudio {
|
|||||||
cursor_type: settings::CursorType::Crosshair,
|
cursor_type: settings::CursorType::Crosshair,
|
||||||
crosshair_color: None,
|
crosshair_color: None,
|
||||||
crosshair_color_input: String::new(),
|
crosshair_color_input: String::new(),
|
||||||
|
isolines_awaiting_regen: false,
|
||||||
|
snap_angle_input: "0".to_string(),
|
||||||
lineweight_display_scale: 100,
|
lineweight_display_scale: 100,
|
||||||
ui_scale: 100,
|
ui_scale: 100,
|
||||||
isometric_drafting: false,
|
isometric_drafting: false,
|
||||||
@@ -3526,6 +3591,7 @@ impl OpenCADStudio {
|
|||||||
block_usage_last_persist: None,
|
block_usage_last_persist: None,
|
||||||
awaiting_vports: false,
|
awaiting_vports: false,
|
||||||
pending_setvar: None,
|
pending_setvar: None,
|
||||||
|
delete_objects: 1,
|
||||||
ucs_icon_hover: false,
|
ucs_icon_hover: false,
|
||||||
ucs_icon_selected: false,
|
ucs_icon_selected: false,
|
||||||
ucs_grip_drag: None,
|
ucs_grip_drag: None,
|
||||||
@@ -3898,7 +3964,16 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn new_for_test() -> Self {
|
pub(crate) fn new_for_test() -> Self {
|
||||||
Self::new()
|
let mut app = Self::new();
|
||||||
|
// `new` loads the real settings file, so without this every test runs
|
||||||
|
// against whatever the developer last set in the application — a suite
|
||||||
|
// that passes on a clean machine and fails on a used one. It surfaced
|
||||||
|
// when a persisted `GRIPOBJLIMIT` made the grip-limit test see 32767
|
||||||
|
// where it expected the default, and the number of persisted settings
|
||||||
|
// only grows.
|
||||||
|
app.apply_config(crate::app::config::AppConfig::default());
|
||||||
|
app.last_saved_config = Some(app.current_config());
|
||||||
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Install `cmd` as the active interactive command for tab `tab`.
|
/// Install `cmd` as the active interactive command for tab `tab`.
|
||||||
|
|||||||
@@ -402,6 +402,24 @@ impl super::OpenCADStudio {
|
|||||||
self.add_surface_model_inner(entity, surface, true)
|
self.add_surface_model_inner(entity, surface, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn add_surface_model_with_history(
|
||||||
|
&mut self,
|
||||||
|
entity: EntityType,
|
||||||
|
surface: Body,
|
||||||
|
history: SolidHistoryOperation,
|
||||||
|
) -> Handle {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let handle = self.add_surface_model_inner(entity, surface, true);
|
||||||
|
if handle.is_null() {
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
if !self.tabs[i].scene.create_solid_history(handle, history) {
|
||||||
|
self.tabs[i].scene.rollback_new_entities(&[handle]);
|
||||||
|
return Handle::NULL;
|
||||||
|
}
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
|
||||||
fn add_surface_model_preserving_style(
|
fn add_surface_model_preserving_style(
|
||||||
&mut self,
|
&mut self,
|
||||||
entity: EntityType,
|
entity: EntityType,
|
||||||
@@ -2293,7 +2311,9 @@ impl super::OpenCADStudio {
|
|||||||
surfaces.push(surf);
|
surfaces.push(surf);
|
||||||
}
|
}
|
||||||
self.push_undo_snapshot(i, "CONVTOSURFACE");
|
self.push_undo_snapshot(i, "CONVTOSURFACE");
|
||||||
self.tabs[i].scene.erase_entities(&handles);
|
if self.delete_objects != 0 {
|
||||||
|
self.tabs[i].scene.erase_entities(&handles);
|
||||||
|
}
|
||||||
let n = surfaces.len();
|
let n = surfaces.len();
|
||||||
for surf in surfaces {
|
for surf in surfaces {
|
||||||
self.tabs[i].scene.add_entity(EntityType::Surface(surf));
|
self.tabs[i].scene.add_entity(EntityType::Surface(surf));
|
||||||
|
|||||||
@@ -2221,6 +2221,17 @@ impl OpenCADStudio {
|
|||||||
entity_type_label(entity)
|
entity_type_label(entity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
acadrust::EntityType::Surface(_)
|
||||||
|
if matches!(
|
||||||
|
crate::scene::model::solid_history::primitive_property_operation(
|
||||||
|
&self.tabs[i].scene.document,
|
||||||
|
handle,
|
||||||
|
),
|
||||||
|
Some(acadrust::objects::SolidHistoryOperation::Extrusion(_))
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
format!("{} ({})", t!("Surface"), t!("Extrusion"))
|
||||||
|
}
|
||||||
_ => entity_type_label(entity),
|
_ => entity_type_label(entity),
|
||||||
};
|
};
|
||||||
ui::PropertiesPanel {
|
ui::PropertiesPanel {
|
||||||
@@ -2328,23 +2339,22 @@ filter={:.1} local={:.1} aggregate={:.1} entities={}",
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if local_refs.iter().all(|(handle, _)| {
|
let compact_solids = local_refs.iter().all(|(handle, _)| {
|
||||||
crate::scene::model::solid_history::has_compact_solid_properties(
|
crate::scene::model::solid_history::has_compact_solid_properties(
|
||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
*handle,
|
*handle,
|
||||||
)
|
)
|
||||||
}) {
|
});
|
||||||
sections.retain(|section| {
|
if compact_solids {
|
||||||
!section.props.iter().any(|property| {
|
retain_compact_solid_sections(&mut sections);
|
||||||
property.field.starts_with("acis_")
|
|
||||||
|| property.field.starts_with("s3d_")
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
sections.extend(aggregate_solid_history_sections(
|
sections.extend(aggregate_solid_history_sections(
|
||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
&local_refs.iter().map(|(handle, _)| *handle).collect::<Vec<_>>(),
|
&local_refs.iter().map(|(handle, _)| *handle).collect::<Vec<_>>(),
|
||||||
));
|
));
|
||||||
|
if compact_solids {
|
||||||
|
retain_compact_solid_sections(&mut sections);
|
||||||
|
}
|
||||||
ui::PropertiesPanel {
|
ui::PropertiesPanel {
|
||||||
choice_combos: sections
|
choice_combos: sections
|
||||||
.iter()
|
.iter()
|
||||||
@@ -3293,6 +3303,10 @@ fn retain_compact_solid_sections(
|
|||||||
sections: &mut Vec<crate::scene::model::object::PropSection>,
|
sections: &mut Vec<crate::scene::model::object::PropSection>,
|
||||||
) {
|
) {
|
||||||
sections.iter_mut().for_each(|section| {
|
sections.iter_mut().for_each(|section| {
|
||||||
|
let internal_surface_section = section
|
||||||
|
.props
|
||||||
|
.iter()
|
||||||
|
.any(|property| property.field == "srf_kind");
|
||||||
section.props.retain(|property| {
|
section.props.retain(|property| {
|
||||||
matches!(
|
matches!(
|
||||||
property.field,
|
property.field,
|
||||||
@@ -3305,7 +3319,8 @@ fn retain_compact_solid_sections(
|
|||||||
| "transparency"
|
| "transparency"
|
||||||
| "hyperlink"
|
| "hyperlink"
|
||||||
| "material"
|
| "material"
|
||||||
) || crate::scene::model::solid_history::is_specialized_property(property.field)
|
) || (!internal_surface_section
|
||||||
|
&& crate::scene::model::solid_history::is_specialized_property(property.field))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
sections.retain(|section| !section.props.is_empty());
|
sections.retain(|section| !section.props.is_empty());
|
||||||
@@ -4237,6 +4252,29 @@ mod grip_limit_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The ViewCube, the UCS icon and selection cycling all have commands and
|
||||||
|
/// status-bar pills, but their values lived only on the app struct: turn
|
||||||
|
/// the ViewCube off, restart, and it came straight back. Nothing failed —
|
||||||
|
/// the setting simply evaporated — which is exactly the kind of thing a
|
||||||
|
/// round-trip test catches and a person does not.
|
||||||
|
#[test]
|
||||||
|
fn the_view_toggles_survive_a_save_and_load() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.show_viewcube = false;
|
||||||
|
app.show_ucs_icon = false;
|
||||||
|
app.ucs_icon_at_origin = false;
|
||||||
|
app.selection_cycling = true;
|
||||||
|
|
||||||
|
let saved = app.current_settings();
|
||||||
|
let mut restored = OpenCADStudio::new_for_test();
|
||||||
|
restored.apply_settings(&saved);
|
||||||
|
|
||||||
|
assert!(!restored.show_viewcube, "the ViewCube must stay off");
|
||||||
|
assert!(!restored.show_ucs_icon);
|
||||||
|
assert!(!restored.ucs_icon_at_origin);
|
||||||
|
assert!(restored.selection_cycling);
|
||||||
|
}
|
||||||
|
|
||||||
/// The Options card writes these four straight onto the app and relies on
|
/// The Options card writes these four straight onto the app and relies on
|
||||||
/// the snapshot/restore pair to carry them across a restart. Three of them
|
/// the snapshot/restore pair to carry them across a restart. Three of them
|
||||||
/// had no UI until now and so no reason for anyone to notice if the pair
|
/// had no UI until now and so no reason for anyone to notice if the pair
|
||||||
|
|||||||
@@ -145,6 +145,39 @@ pub(crate) fn snaps_from_osmode(osmode: i32) -> (Vec<SnapType>, bool) {
|
|||||||
(modes, osmode & OSMODE_SUPPRESS == 0)
|
(modes, osmode & OSMODE_SUPPRESS == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize_options_tab<'de, D>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> Result<crate::ui::window::options::OptionsTab, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use crate::ui::window::options::OptionsTab;
|
||||||
|
use serde::Deserialize;
|
||||||
|
let name = String::deserialize(deserializer).unwrap_or_default();
|
||||||
|
Ok(match name.as_str() {
|
||||||
|
"files" => OptionsTab::Files,
|
||||||
|
"open-and-save" => OptionsTab::OpenAndSave,
|
||||||
|
"display" => OptionsTab::Display,
|
||||||
|
"drafting" => OptionsTab::Drafting,
|
||||||
|
"modeling" => OptionsTab::Modeling,
|
||||||
|
"selection" => OptionsTab::Selection,
|
||||||
|
"user-preferences" => OptionsTab::UserPreferences,
|
||||||
|
_ => OptionsTab::General,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a drafting angle without a trailing `.0`, so `22.5` but `30`.
|
||||||
|
///
|
||||||
|
/// The polar pop-up formats its presets the same way; both are showing the
|
||||||
|
/// same kind of number to the same person.
|
||||||
|
pub fn format_snap_angle(deg: f32) -> String {
|
||||||
|
if (deg - deg.round()).abs() < 1e-4 {
|
||||||
|
format!("{}", deg.round() as i32)
|
||||||
|
} else {
|
||||||
|
format!("{deg}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// GRIPOBJLIMIT default: past this many selected objects, no grips are drawn.
|
/// GRIPOBJLIMIT default: past this many selected objects, no grips are drawn.
|
||||||
pub const DEFAULT_GRIP_OBJECT_LIMIT: i32 = 100;
|
pub const DEFAULT_GRIP_OBJECT_LIMIT: i32 = 100;
|
||||||
|
|
||||||
@@ -170,6 +203,18 @@ pub struct UserSettings {
|
|||||||
/// GRIPOBJLIMIT: past this many selected objects, no grips are drawn at
|
/// GRIPOBJLIMIT: past this many selected objects, no grips are drawn at
|
||||||
/// all. 0 means no limit. The drawing header carries no slot for it.
|
/// all. 0 means no limit. The drawing header carries no slot for it.
|
||||||
pub grip_object_limit: i32,
|
pub grip_object_limit: i32,
|
||||||
|
/// Last Options page; unknown saved names fall back without rejecting the config.
|
||||||
|
#[serde(default, deserialize_with = "deserialize_options_tab")]
|
||||||
|
pub options_tab: crate::ui::window::options::OptionsTab,
|
||||||
|
/// Show the navigation cube (NAVVCUBE).
|
||||||
|
pub show_viewcube: bool,
|
||||||
|
/// Show the UCS icon (UCSICON).
|
||||||
|
pub show_ucs_icon: bool,
|
||||||
|
/// Draw the UCS icon at the origin rather than in the corner
|
||||||
|
/// (UCSICON ORigin / NOorigin).
|
||||||
|
pub ucs_icon_at_origin: bool,
|
||||||
|
/// Selection cycling: a click where objects overlap opens a picker.
|
||||||
|
pub selection_cycling: bool,
|
||||||
/// CURSORTYPE: crosshair or the platform pointer over the drawing.
|
/// CURSORTYPE: crosshair or the platform pointer over the drawing.
|
||||||
pub cursor_type: CursorType,
|
pub cursor_type: CursorType,
|
||||||
/// Explicit crosshair RGB. `None` keeps automatic background contrast.
|
/// Explicit crosshair RGB. `None` keeps automatic background contrast.
|
||||||
@@ -227,6 +272,10 @@ pub struct UserSettings {
|
|||||||
/// the current creation layer/style. Registry-style, app-level preference.
|
/// the current creation layer/style. Registry-style, app-level preference.
|
||||||
#[serde(default = "default_dimension_continue_mode")]
|
#[serde(default = "default_dimension_continue_mode")]
|
||||||
pub dimension_continue_mode: i16,
|
pub dimension_continue_mode: i16,
|
||||||
|
/// DELOBJ: source-geometry deletion policy (0–3). Registry-style,
|
||||||
|
/// app-level preference; first-run default is 1.
|
||||||
|
#[serde(default = "default_delete_objects")]
|
||||||
|
pub delete_objects: i16,
|
||||||
/// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false).
|
/// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false).
|
||||||
pub textfill: bool,
|
pub textfill: bool,
|
||||||
/// When true, saving over an existing file first copies it to a sibling
|
/// When true, saving over an existing file first copies it to a sibling
|
||||||
@@ -265,12 +314,6 @@ pub struct UserSettings {
|
|||||||
pub pick_drag_rect: bool,
|
pub pick_drag_rect: bool,
|
||||||
/// Show the floating Quick Properties panel when objects are selected.
|
/// Show the floating Quick Properties panel when objects are selected.
|
||||||
pub quick_properties: bool,
|
pub quick_properties: bool,
|
||||||
/// Persisted viewport background colours (0–255 RGB); `None` = app default
|
|
||||||
/// (dark grey model / off-white paper). Applied to every drawing tab on
|
|
||||||
/// launch and to tabs opened later, so a chosen background survives restarts
|
|
||||||
/// (#188).
|
|
||||||
pub bg_color: Option<[u8; 3]>,
|
|
||||||
pub paper_bg_color: Option<[u8; 3]>,
|
|
||||||
/// Interface language preference. `System` negotiates against the
|
/// Interface language preference. `System` negotiates against the
|
||||||
/// platform locale on every launch.
|
/// platform locale on every launch.
|
||||||
pub language: crate::i18n::Language,
|
pub language: crate::i18n::Language,
|
||||||
@@ -298,6 +341,10 @@ fn default_clipromptlines() -> i32 {
|
|||||||
3
|
3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_delete_objects() -> i16 {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
fn default_commandline_fade_ms() -> i32 {
|
fn default_commandline_fade_ms() -> i32 {
|
||||||
3000
|
3000
|
||||||
}
|
}
|
||||||
@@ -344,6 +391,11 @@ impl Default for UserSettings {
|
|||||||
zoom_factor: 60,
|
zoom_factor: 60,
|
||||||
cursor_size: 5,
|
cursor_size: 5,
|
||||||
pick_box: 3,
|
pick_box: 3,
|
||||||
|
options_tab: crate::ui::window::options::OptionsTab::General,
|
||||||
|
show_viewcube: true,
|
||||||
|
show_ucs_icon: true,
|
||||||
|
ucs_icon_at_origin: true,
|
||||||
|
selection_cycling: false,
|
||||||
double_click_block_refedit: false,
|
double_click_block_refedit: false,
|
||||||
double_click_block_attedit: true,
|
double_click_block_attedit: true,
|
||||||
grip_object_limit: DEFAULT_GRIP_OBJECT_LIMIT,
|
grip_object_limit: DEFAULT_GRIP_OBJECT_LIMIT,
|
||||||
@@ -367,6 +419,7 @@ impl Default for UserSettings {
|
|||||||
texteditmode: false,
|
texteditmode: false,
|
||||||
quick_dimension_snap_priority: 0,
|
quick_dimension_snap_priority: 0,
|
||||||
dimension_continue_mode: 1,
|
dimension_continue_mode: 1,
|
||||||
|
delete_objects: 1,
|
||||||
textfill: true,
|
textfill: true,
|
||||||
backup_on_save: true,
|
backup_on_save: true,
|
||||||
file_assoc_enabled: true,
|
file_assoc_enabled: true,
|
||||||
@@ -377,8 +430,6 @@ impl Default for UserSettings {
|
|||||||
pick_add: true,
|
pick_add: true,
|
||||||
pick_drag_rect: false,
|
pick_drag_rect: false,
|
||||||
quick_properties: false,
|
quick_properties: false,
|
||||||
bg_color: None,
|
|
||||||
paper_bg_color: None,
|
|
||||||
language: crate::i18n::Language::default(),
|
language: crate::i18n::Language::default(),
|
||||||
cliprompt_lines: 3,
|
cliprompt_lines: 3,
|
||||||
commandline_fade_ms: 3000,
|
commandline_fade_ms: 3000,
|
||||||
@@ -428,4 +479,23 @@ mod tests {
|
|||||||
let b: std::collections::HashSet<_> = back.into_iter().collect();
|
let b: std::collections::HashSet<_> = back.into_iter().collect();
|
||||||
assert_eq!(a, b);
|
assert_eq!(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_page_name_that_no_longer_exists_does_not_cost_the_other_settings() {
|
||||||
|
let json = r#"{
|
||||||
|
"settings": {
|
||||||
|
"options_tab": "drawing",
|
||||||
|
"pick_add": false,
|
||||||
|
"savetime_min": 42
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
let cfg: crate::app::config::AppConfig =
|
||||||
|
serde_json::from_str(json).expect("an unknown page name must not fail the parse");
|
||||||
|
assert_eq!(
|
||||||
|
cfg.settings.options_tab,
|
||||||
|
crate::ui::window::options::OptionsTab::General,
|
||||||
|
);
|
||||||
|
assert!(!cfg.settings.pick_add, "the rest of the file must survive");
|
||||||
|
assert_eq!(cfg.settings.savetime_min, 42);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
c.is_ascii_digit()
|
c.is_ascii_digit()
|
||||||
|| matches!(c, '.' | '-' | '+' | '*' | '/' | '^' | '%' | '(' | ')')
|
|| matches!(c, '.' | '-' | '+' | '*' | '/' | '^' | '%' | '(' | ')')
|
||||||
});
|
});
|
||||||
if dyn_field_char && self.dyn_input && !self.tabs[i].dyn_fields.is_empty() {
|
if dyn_field_char
|
||||||
|
&& self.command_line.input.is_empty()
|
||||||
|
&& self.dyn_input
|
||||||
|
&& !self.tabs[i].dyn_fields.is_empty()
|
||||||
|
{
|
||||||
let a = self.tabs[i]
|
let a = self.tabs[i]
|
||||||
.dyn_active
|
.dyn_active
|
||||||
.min(self.tabs[i].dyn_fields.len() - 1);
|
.min(self.tabs[i].dyn_fields.len() - 1);
|
||||||
@@ -199,6 +203,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
.buffer
|
.buffer
|
||||||
.get_or_insert_with(String::new)
|
.get_or_insert_with(String::new)
|
||||||
.push_str(&s);
|
.push_str(&s);
|
||||||
|
if self.tabs[i].active_grip.as_ref().is_some_and(|grip| {
|
||||||
|
matches!(
|
||||||
|
grip.mode,
|
||||||
|
GripEditMode::Lengthen
|
||||||
|
| GripEditMode::Radius
|
||||||
|
| GripEditMode::ArcLength
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
self.command_line.input.push_str(&s);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Command-line entry is shown uppercase — except in
|
// Command-line entry is shown uppercase — except in
|
||||||
// free-form text prompts, where the typed case is the
|
// free-form text prompts, where the typed case is the
|
||||||
@@ -242,6 +256,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
if buf.is_empty() {
|
if buf.is_empty() {
|
||||||
self.tabs[i].dyn_fields[a].buffer = None;
|
self.tabs[i].dyn_fields[a].buffer = None;
|
||||||
}
|
}
|
||||||
|
if self.tabs[i].active_grip.as_ref().is_some_and(|grip| {
|
||||||
|
matches!(
|
||||||
|
grip.mode,
|
||||||
|
GripEditMode::Lengthen
|
||||||
|
| GripEditMode::Radius
|
||||||
|
| GripEditMode::ArcLength
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
self.command_line.input.pop();
|
||||||
|
}
|
||||||
return self.focus_cmd_input();
|
return self.focus_cmd_input();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -277,13 +301,49 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
// Grip-menu value prompt — consume the typed number and
|
// Grip-menu value prompt — consume the typed number and
|
||||||
// route it through `apply_grip_menu_value`.
|
// route it through `apply_grip_menu_value`.
|
||||||
|
// Interactive grip prompts are valid only while their matching
|
||||||
|
// grip edit is alive. Do not let a stale Radius / Lengthen /
|
||||||
|
// Arc Length prompt consume Enter or coordinates from a later
|
||||||
|
// drawing command.
|
||||||
|
let stale_interactive_grip_prompt = self.grip_pending.as_ref().is_some_and(|pending| {
|
||||||
|
let expected_mode = match pending.action {
|
||||||
|
crate::scene::model::object::GripMenuAction::Lengthen => {
|
||||||
|
Some(GripEditMode::Lengthen)
|
||||||
|
}
|
||||||
|
crate::scene::model::object::GripMenuAction::Radius => {
|
||||||
|
Some(GripEditMode::Radius)
|
||||||
|
}
|
||||||
|
crate::scene::model::object::GripMenuAction::ArcLength => {
|
||||||
|
Some(GripEditMode::ArcLength)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
expected_mode.is_some_and(|mode| {
|
||||||
|
!self.tabs[self.active_tab].active_grip.as_ref().is_some_and(|grip| {
|
||||||
|
grip.mode == mode
|
||||||
|
&& grip.handle == pending.handle
|
||||||
|
&& grip.grip_id == pending.grip_id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if stale_interactive_grip_prompt {
|
||||||
|
self.grip_pending = None;
|
||||||
|
}
|
||||||
if let Some(pending) = self.grip_pending.take() {
|
if let Some(pending) = self.grip_pending.take() {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if self.reject_locked_edit(i, pending.handle) {
|
if self.reject_locked_edit(i, pending.handle) {
|
||||||
self.cancel_active_grip_edit();
|
self.cancel_active_grip_edit();
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let raw = crate::app::expr_eval::eval_to_string(self.command_line.input.trim());
|
// Dynamic Input keeps numeric typing in its focused field.
|
||||||
|
// Fall back to the command-line buffer so the established
|
||||||
|
// prompt workflow remains unchanged when DYN is disabled.
|
||||||
|
let dyn_value = self.tabs[i]
|
||||||
|
.dyn_fields
|
||||||
|
.iter()
|
||||||
|
.find_map(|field| field.buffer.as_deref());
|
||||||
|
let entered = dyn_value.unwrap_or(self.command_line.input.trim());
|
||||||
|
let raw = crate::app::expr_eval::eval_to_string(entered);
|
||||||
self.command_line.input.clear();
|
self.command_line.input.clear();
|
||||||
let Ok(v) = raw.parse::<f64>() else {
|
let Ok(v) = raw.parse::<f64>() else {
|
||||||
self.command_line.push_error(crate::tf!(
|
self.command_line.push_error(crate::tf!(
|
||||||
@@ -293,15 +353,27 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
self.grip_pending = Some(pending);
|
self.grip_pending = Some(pending);
|
||||||
return self.focus_cmd_input();
|
return self.focus_cmd_input();
|
||||||
};
|
};
|
||||||
let interactive_lengthen = self.tabs[i]
|
let interactive_value_grip = self.tabs[i]
|
||||||
.active_grip
|
.active_grip
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|grip| {
|
.is_some_and(|grip| {
|
||||||
grip.mode == GripEditMode::Lengthen
|
matches!(
|
||||||
|
(grip.mode, pending.action),
|
||||||
|
(
|
||||||
|
GripEditMode::Lengthen,
|
||||||
|
crate::scene::model::object::GripMenuAction::Lengthen,
|
||||||
|
) | (
|
||||||
|
GripEditMode::Radius,
|
||||||
|
crate::scene::model::object::GripMenuAction::Radius,
|
||||||
|
) | (
|
||||||
|
GripEditMode::ArcLength,
|
||||||
|
crate::scene::model::object::GripMenuAction::ArcLength,
|
||||||
|
)
|
||||||
|
)
|
||||||
&& grip.handle == pending.handle
|
&& grip.handle == pending.handle
|
||||||
&& grip.grip_id == pending.grip_id
|
&& grip.grip_id == pending.grip_id
|
||||||
});
|
});
|
||||||
if interactive_lengthen {
|
if interactive_value_grip {
|
||||||
self.cancel_active_grip_edit();
|
self.cancel_active_grip_edit();
|
||||||
}
|
}
|
||||||
use crate::entities::traits::EntityTypeOps;
|
use crate::entities::traits::EntityTypeOps;
|
||||||
@@ -566,6 +638,30 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
.map(|c| c.is_free_text_step())
|
.map(|c| c.is_free_text_step())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let raw = self.command_line.input.trim().to_string();
|
let raw = self.command_line.input.trim().to_string();
|
||||||
|
let is_mtp = raw.trim_start_matches('_').eq_ignore_ascii_case("MTP")
|
||||||
|
|| raw.trim_start_matches('_').eq_ignore_ascii_case("M2P");
|
||||||
|
if is_mtp {
|
||||||
|
let is_point_step = !self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.input_kind().wants_text())
|
||||||
|
.unwrap_or(true)
|
||||||
|
|| self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.point_step_accepts_keywords())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let not_entity_pick = !self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.needs_entity_pick())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if is_point_step && not_entity_pick {
|
||||||
|
self.command_line.input.clear();
|
||||||
|
self.start_mtp_modifier(i);
|
||||||
|
return self.focus_cmd_input();
|
||||||
|
}
|
||||||
|
}
|
||||||
let text = if free_text {
|
let text = if free_text {
|
||||||
raw
|
raw
|
||||||
} else {
|
} else {
|
||||||
@@ -1306,7 +1402,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
action: item.action,
|
action: item.action,
|
||||||
label,
|
label,
|
||||||
});
|
});
|
||||||
if matches!(item.action, GripMenuAction::Lengthen) {
|
if matches!(
|
||||||
|
item.action,
|
||||||
|
GripMenuAction::Lengthen
|
||||||
|
| GripMenuAction::Radius
|
||||||
|
| GripMenuAction::ArcLength
|
||||||
|
) {
|
||||||
if let Some((_, grip)) = self.tabs[i]
|
if let Some((_, grip)) = self.tabs[i]
|
||||||
.selected_grip_handles
|
.selected_grip_handles
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1315,14 +1416,52 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
**owner == popup.handle && grip.id == popup.grip_id
|
**owner == popup.handle && grip.id == popup.grip_id
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
self.tabs[i].active_grip = Some(GripEdit::lengthen(
|
if self.grip_originals.is_empty() {
|
||||||
popup.handle,
|
self.grip_originals = self.tabs[i]
|
||||||
popup.grip_id,
|
.scene
|
||||||
grip.world,
|
.document
|
||||||
));
|
.get_entity(popup.handle)
|
||||||
|
.cloned()
|
||||||
|
.map(|entity| vec![(popup.handle, entity)])
|
||||||
|
.unwrap_or_default();
|
||||||
|
}
|
||||||
|
self.tabs[i].active_grip = Some(match item.action {
|
||||||
|
GripMenuAction::Radius => GripEdit::radius(
|
||||||
|
popup.handle,
|
||||||
|
popup.grip_id,
|
||||||
|
grip.world,
|
||||||
|
),
|
||||||
|
GripMenuAction::ArcLength => GripEdit::arc_length(
|
||||||
|
popup.handle,
|
||||||
|
popup.grip_id,
|
||||||
|
grip.world,
|
||||||
|
),
|
||||||
|
_ => GripEdit::lengthen(
|
||||||
|
popup.handle,
|
||||||
|
popup.grip_id,
|
||||||
|
grip.world,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Popup actions do not pass through the normal grip
|
||||||
|
// press handler, which is where dynamic fields are
|
||||||
|
// usually seeded. Build the value field
|
||||||
|
// immediately so it is visible before the next mouse
|
||||||
|
// move (and so keyboard input has a field to target).
|
||||||
|
self.sync_dyn_fields();
|
||||||
|
if matches!(item.action, GripMenuAction::Radius) {
|
||||||
|
self.command_line.push_info(
|
||||||
|
crate::t!("Specify point or enter radius:").as_ref(),
|
||||||
|
);
|
||||||
|
} else if matches!(item.action, GripMenuAction::ArcLength) {
|
||||||
|
self.command_line.push_info(
|
||||||
|
crate::t!("Specify point or enter arc length:").as_ref(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.command_line.push_info(
|
||||||
|
crate::t!("Specify point or enter distance:").as_ref(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
self.command_line
|
|
||||||
.push_info(crate::t!("Specify point or enter distance:").as_ref());
|
|
||||||
} else {
|
} else {
|
||||||
self.command_line.push_info(crate::tf!("{label}:").as_ref());
|
self.command_line.push_info(crate::tf!("{label}:").as_ref());
|
||||||
}
|
}
|
||||||
@@ -1443,24 +1582,25 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// One-shot action — apply immediately.
|
// One-shot action — apply immediately.
|
||||||
let unchanged = self.tabs[i]
|
let unchanged = item.label.starts_with('✓')
|
||||||
.scene
|
|| self.tabs[i]
|
||||||
.document
|
.scene
|
||||||
.get_entity(popup.handle)
|
.document
|
||||||
.is_some_and(|entity| match (item.action, entity) {
|
.get_entity(popup.handle)
|
||||||
(GripMenuAction::ShowFit, acadrust::EntityType::Spline(spline)) => {
|
.is_some_and(|entity| match (item.action, entity) {
|
||||||
!spline.cv_frame_visible
|
(GripMenuAction::ShowFit, acadrust::EntityType::Spline(spline)) => {
|
||||||
&& crate::entities::spline::uses_fit_method(spline)
|
!spline.cv_frame_visible
|
||||||
}
|
&& crate::entities::spline::uses_fit_method(spline)
|
||||||
(
|
}
|
||||||
GripMenuAction::ShowControlVertices,
|
(
|
||||||
acadrust::EntityType::Spline(spline),
|
GripMenuAction::ShowControlVertices,
|
||||||
) => {
|
acadrust::EntityType::Spline(spline),
|
||||||
spline.cv_frame_visible
|
) => {
|
||||||
|| !crate::entities::spline::uses_fit_method(spline)
|
spline.cv_frame_visible
|
||||||
}
|
|| !crate::entities::spline::uses_fit_method(spline)
|
||||||
_ => false,
|
}
|
||||||
});
|
_ => false,
|
||||||
|
});
|
||||||
if unchanged {
|
if unchanged {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
@@ -2002,7 +2142,51 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
self.push_undo_snapshot(i, "CHPROP");
|
self.push_undo_snapshot(i, "CHPROP");
|
||||||
|
|
||||||
if crate::scene::model::solid_history::is_loft_geometry_choice(field) {
|
if crate::scene::model::solid_history::is_surface_property_choice(field) {
|
||||||
|
use crate::scene::model::solid_history::{
|
||||||
|
PROP_SURFACE_MAINTAIN_ASSOCIATIVITY, PROP_SURFACE_SHOW_ASSOCIATIVITY,
|
||||||
|
PROP_SURFACE_WIREFRAME_TYPE,
|
||||||
|
};
|
||||||
|
for &handle in &handles {
|
||||||
|
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(mut state) = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(handle)
|
||||||
|
.and_then(|entity| match entity {
|
||||||
|
acadrust::EntityType::Surface(surface) => Some(
|
||||||
|
crate::entities::solid3d::surface_property_state(surface),
|
||||||
|
),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match field {
|
||||||
|
PROP_SURFACE_WIREFRAME_TYPE => {
|
||||||
|
state.isolines = !value.eq_ignore_ascii_case("Isoparms")
|
||||||
|
}
|
||||||
|
PROP_SURFACE_MAINTAIN_ASSOCIATIVITY => {
|
||||||
|
state.maintain_associativity = value.eq_ignore_ascii_case("Yes")
|
||||||
|
}
|
||||||
|
PROP_SURFACE_SHOW_ASSOCIATIVITY => {
|
||||||
|
state.show_associativity = value.eq_ignore_ascii_case("Yes")
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
crate::scene::view::dispatch::set_entity_xdata(
|
||||||
|
&mut self.tabs[i].scene.document,
|
||||||
|
handle,
|
||||||
|
crate::entities::solid3d::SURFACE_PROPERTIES_APP,
|
||||||
|
Some(crate::entities::solid3d::surface_property_xdata_values(state)),
|
||||||
|
);
|
||||||
|
if field == PROP_SURFACE_WIREFRAME_TYPE {
|
||||||
|
self.tabs[i].scene.reseed_derived_caches(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if crate::scene::model::solid_history::is_loft_geometry_choice(field) {
|
||||||
// These choices change the generated body, not just history
|
// These choices change the generated body, not just history
|
||||||
// flags. Use the same transactional rebuild as numeric edits.
|
// flags. Use the same transactional rebuild as numeric edits.
|
||||||
for &handle in &handles {
|
for &handle in &handles {
|
||||||
@@ -2980,6 +3164,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if matches!(field, "srf_u_isolines" | "srf_v_isolines")
|
||||||
|
{
|
||||||
|
self.tabs[i].scene.reseed_derived_caches(handle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,34 +65,55 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
// A normal 2D grip stretch behaves like a point-placement step:
|
// A normal 2D grip stretch behaves like a point-placement step:
|
||||||
// distance and angle are measured from the grip's original position.
|
// distance and angle are measured from the grip's original position.
|
||||||
|
// Lengthen uses the same overlay machinery, but it only needs the
|
||||||
|
// distance field: the entity itself determines the endpoint direction
|
||||||
|
// (or arc sweep), so an angle entry would be misleading.
|
||||||
//
|
//
|
||||||
// Grip editing is not an `active_cmd`, so handle it before the normal
|
// Grip editing is not an `active_cmd`, so handle it before the normal
|
||||||
// command-only path below.
|
// command-only path below.
|
||||||
let grip_origin = self.tabs[i]
|
let grip_input = self.tabs[i]
|
||||||
.active_grip
|
.active_grip
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|grip| {
|
.map(|grip| match grip.mode {
|
||||||
grip.mode == crate::scene::pick::grip::GripEditMode::Stretch
|
crate::scene::pick::grip::GripEditMode::Stretch => {
|
||||||
})
|
(grip.origin_world, None)
|
||||||
.map(|grip| grip.origin_world);
|
}
|
||||||
|
crate::scene::pick::grip::GripEditMode::Lengthen => {
|
||||||
|
(grip.origin_world, Some(crate::command::DynRole::Distance))
|
||||||
|
}
|
||||||
|
crate::scene::pick::grip::GripEditMode::Radius => {
|
||||||
|
(grip.origin_world, Some(crate::command::DynRole::Radius))
|
||||||
|
}
|
||||||
|
crate::scene::pick::grip::GripEditMode::ArcLength => {
|
||||||
|
(grip.origin_world, Some(crate::command::DynRole::Distance))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if let Some(origin) = grip_origin {
|
if let Some((origin, scalar_role)) = grip_input {
|
||||||
let wanted = [DynComponent::Distance, DynComponent::Angle];
|
let wanted_roles: Vec<crate::command::DynRole> = scalar_role.map_or_else(
|
||||||
let current: Vec<DynComponent> = self.tabs[i]
|
|| vec![crate::command::DynRole::Distance, crate::command::DynRole::Angle],
|
||||||
|
|role| vec![role],
|
||||||
|
);
|
||||||
|
let current: Vec<crate::command::DynRole> = self.tabs[i]
|
||||||
.dyn_fields
|
.dyn_fields
|
||||||
.iter()
|
.iter()
|
||||||
.map(|field| field.component)
|
.map(|field| field.role)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if current.as_slice() != wanted {
|
if current != wanted_roles {
|
||||||
self.tabs[i].dyn_fields = wanted
|
self.tabs[i].dyn_fields = wanted_roles
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(DynFieldEntry::new)
|
.copied()
|
||||||
|
.map(DynFieldEntry::from_role)
|
||||||
.collect();
|
.collect();
|
||||||
self.tabs[i].dyn_active = 0;
|
self.tabs[i].dyn_active = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tabs[i].dyn_guide = crate::command::DynGuide::Polar;
|
self.tabs[i].dyn_guide = if scalar_role.is_some() {
|
||||||
|
crate::command::DynGuide::Radius
|
||||||
|
} else {
|
||||||
|
crate::command::DynGuide::Polar
|
||||||
|
};
|
||||||
self.tabs[i].dyn_anchor = Some(origin);
|
self.tabs[i].dyn_anchor = Some(origin);
|
||||||
self.tabs[i].dyn_ref = None;
|
self.tabs[i].dyn_ref = None;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -355,6 +355,37 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write a header variable on the active drawing and mark it modified.
|
||||||
|
pub(in crate::app) fn set_drawing_var(
|
||||||
|
&mut self,
|
||||||
|
write: impl FnOnce(&mut acadrust::document::HeaderVariables),
|
||||||
|
) {
|
||||||
|
let i = self.active_tab;
|
||||||
|
if let Some(tab) = self.tabs.get_mut(i) {
|
||||||
|
write(&mut tab.scene.document.header);
|
||||||
|
tab.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-tessellate active-drawing solids after an ISOLINES change.
|
||||||
|
pub(in crate::app) fn regenerate_meshes(&mut self) {
|
||||||
|
let i = self.active_tab;
|
||||||
|
if let Some(tab) = self.tabs.get_mut(i) {
|
||||||
|
tab.scene.populate_meshes_from_document();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuild text geometry in every open drawing.
|
||||||
|
///
|
||||||
|
/// `TEXTFILL` is a process-global, so a change to it invalidates the text
|
||||||
|
/// in every tab. Doing only the active one leaves the others rendering the
|
||||||
|
/// previous setting until some unrelated edit happens to rebuild them.
|
||||||
|
pub(in crate::app) fn invalidate_text_everywhere(&mut self) {
|
||||||
|
for tab in &mut self.tabs {
|
||||||
|
tab.scene.invalidate_text_geometry_dependencies();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Snapshot the persisted UI preferences from live state.
|
/// Snapshot the persisted UI preferences from live state.
|
||||||
pub(in crate::app) fn current_settings(&self) -> crate::app::settings::UserSettings {
|
pub(in crate::app) fn current_settings(&self) -> crate::app::settings::UserSettings {
|
||||||
crate::app::settings::UserSettings {
|
crate::app::settings::UserSettings {
|
||||||
@@ -365,6 +396,11 @@ impl OpenCADStudio {
|
|||||||
zoom_factor: self.zoom_factor,
|
zoom_factor: self.zoom_factor,
|
||||||
cursor_size: self.cursor_size,
|
cursor_size: self.cursor_size,
|
||||||
pick_box: self.pick_box,
|
pick_box: self.pick_box,
|
||||||
|
options_tab: self.options_tab,
|
||||||
|
show_viewcube: self.show_viewcube,
|
||||||
|
show_ucs_icon: self.show_ucs_icon,
|
||||||
|
ucs_icon_at_origin: self.ucs_icon_at_origin,
|
||||||
|
selection_cycling: self.selection_cycling,
|
||||||
double_click_block_refedit: self.double_click_block_refedit,
|
double_click_block_refedit: self.double_click_block_refedit,
|
||||||
double_click_block_attedit: self.double_click_block_attedit,
|
double_click_block_attedit: self.double_click_block_attedit,
|
||||||
grip_object_limit: self.grip_object_limit,
|
grip_object_limit: self.grip_object_limit,
|
||||||
@@ -393,6 +429,7 @@ impl OpenCADStudio {
|
|||||||
texteditmode: self.texteditmode,
|
texteditmode: self.texteditmode,
|
||||||
quick_dimension_snap_priority: self.quick_dimension_snap_priority,
|
quick_dimension_snap_priority: self.quick_dimension_snap_priority,
|
||||||
dimension_continue_mode: self.dimension_continue_mode,
|
dimension_continue_mode: self.dimension_continue_mode,
|
||||||
|
delete_objects: self.delete_objects,
|
||||||
textfill: crate::scene::text::sdf_atlas::textfill(),
|
textfill: crate::scene::text::sdf_atlas::textfill(),
|
||||||
backup_on_save: self.backup_on_save,
|
backup_on_save: self.backup_on_save,
|
||||||
file_assoc_enabled: self.file_assoc_enabled,
|
file_assoc_enabled: self.file_assoc_enabled,
|
||||||
@@ -403,8 +440,6 @@ impl OpenCADStudio {
|
|||||||
pick_add: self.pick_add,
|
pick_add: self.pick_add,
|
||||||
pick_drag_rect: self.pick_drag_rect,
|
pick_drag_rect: self.pick_drag_rect,
|
||||||
quick_properties: self.quick_properties,
|
quick_properties: self.quick_properties,
|
||||||
bg_color: None,
|
|
||||||
paper_bg_color: None,
|
|
||||||
language: self.language,
|
language: self.language,
|
||||||
cliprompt_lines: crate::app::settings::clamp_clipromptlines(self.cliprompt_lines),
|
cliprompt_lines: crate::app::settings::clamp_clipromptlines(self.cliprompt_lines),
|
||||||
commandline_fade_ms: crate::app::settings::clamp_commandline_fade_ms(
|
commandline_fade_ms: crate::app::settings::clamp_commandline_fade_ms(
|
||||||
@@ -424,6 +459,14 @@ impl OpenCADStudio {
|
|||||||
self.zoom_factor = s.zoom_factor.clamp(3, 100);
|
self.zoom_factor = s.zoom_factor.clamp(3, 100);
|
||||||
self.cursor_size = s.cursor_size.clamp(1, 100);
|
self.cursor_size = s.cursor_size.clamp(1, 100);
|
||||||
self.pick_box = s.pick_box.clamp(0, 50);
|
self.pick_box = s.pick_box.clamp(0, 50);
|
||||||
|
self.options_tab = s.options_tab;
|
||||||
|
// These four drive real features with commands and status-bar pills,
|
||||||
|
// but they lived only on the app struct: turning the ViewCube off and
|
||||||
|
// restarting brought it straight back.
|
||||||
|
self.show_viewcube = s.show_viewcube;
|
||||||
|
self.show_ucs_icon = s.show_ucs_icon;
|
||||||
|
self.ucs_icon_at_origin = s.ucs_icon_at_origin;
|
||||||
|
self.selection_cycling = s.selection_cycling;
|
||||||
self.double_click_block_refedit = s.double_click_block_refedit;
|
self.double_click_block_refedit = s.double_click_block_refedit;
|
||||||
self.double_click_block_attedit = s.double_click_block_attedit;
|
self.double_click_block_attedit = s.double_click_block_attedit;
|
||||||
self.grip_object_limit = s.grip_object_limit.clamp(0, 32767);
|
self.grip_object_limit = s.grip_object_limit.clamp(0, 32767);
|
||||||
@@ -442,6 +485,9 @@ impl OpenCADStudio {
|
|||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
// The Options field edits a buffer rather than the value, so it has to
|
||||||
|
// be reseeded whenever the value is restored from behind it.
|
||||||
|
self.snap_angle_input = crate::app::settings::format_snap_angle(self.snap_angle_deg);
|
||||||
// Ortho + running OSNAP are per-drawing (adopted from the header on
|
// Ortho + running OSNAP are per-drawing (adopted from the header on
|
||||||
// open / tab switch), not app-global, so they are not applied here.
|
// open / tab switch), not app-global, so they are not applied here.
|
||||||
self.snapper.otrack_enabled = s.otrack;
|
self.snapper.otrack_enabled = s.otrack;
|
||||||
@@ -464,6 +510,7 @@ impl OpenCADStudio {
|
|||||||
self.texteditmode = s.texteditmode;
|
self.texteditmode = s.texteditmode;
|
||||||
self.quick_dimension_snap_priority = s.quick_dimension_snap_priority.min(1);
|
self.quick_dimension_snap_priority = s.quick_dimension_snap_priority.min(1);
|
||||||
self.dimension_continue_mode = s.dimension_continue_mode.clamp(0, 1);
|
self.dimension_continue_mode = s.dimension_continue_mode.clamp(0, 1);
|
||||||
|
self.delete_objects = s.delete_objects.clamp(0, 3);
|
||||||
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
|
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
|
||||||
self.backup_on_save = s.backup_on_save;
|
self.backup_on_save = s.backup_on_save;
|
||||||
self.file_assoc_enabled = s.file_assoc_enabled;
|
self.file_assoc_enabled = s.file_assoc_enabled;
|
||||||
@@ -475,7 +522,6 @@ impl OpenCADStudio {
|
|||||||
self.pick_add = s.pick_add;
|
self.pick_add = s.pick_add;
|
||||||
self.pick_drag_rect = s.pick_drag_rect;
|
self.pick_drag_rect = s.pick_drag_rect;
|
||||||
self.quick_properties = s.quick_properties;
|
self.quick_properties = s.quick_properties;
|
||||||
// Legacy settings.bg_color / paper_bg_color are superseded by model_space config.
|
|
||||||
if crate::i18n::set_language(s.language).is_ok() {
|
if crate::i18n::set_language(s.language).is_ok() {
|
||||||
self.language = s.language;
|
self.language = s.language;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1583,6 +1583,24 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
let live_input = s.clone();
|
let live_input = s.clone();
|
||||||
self.command_line.input = live_input.clone();
|
self.command_line.input = live_input.clone();
|
||||||
|
let i = self.active_tab;
|
||||||
|
if self.dyn_input
|
||||||
|
&& self.tabs[i].active_grip.as_ref().is_some_and(|grip| {
|
||||||
|
matches!(
|
||||||
|
grip.mode,
|
||||||
|
crate::scene::pick::grip::GripEditMode::Lengthen
|
||||||
|
| crate::scene::pick::grip::GripEditMode::Radius
|
||||||
|
| crate::scene::pick::grip::GripEditMode::ArcLength
|
||||||
|
)
|
||||||
|
})
|
||||||
|
&& !self.tabs[i].dyn_fields.is_empty()
|
||||||
|
{
|
||||||
|
let a = self.tabs[i]
|
||||||
|
.dyn_active
|
||||||
|
.min(self.tabs[i].dyn_fields.len() - 1);
|
||||||
|
self.tabs[i].dyn_fields[a].buffer =
|
||||||
|
(!live_input.is_empty()).then_some(live_input.clone());
|
||||||
|
}
|
||||||
self.command_line.autocomplete_cursor = None;
|
self.command_line.autocomplete_cursor = None;
|
||||||
self.command_line.cancel_history_navigation();
|
self.command_line.cancel_history_navigation();
|
||||||
// Live incremental search for INSERT/MINSERT: update picker on each keystroke
|
// Live incremental search for INSERT/MINSERT: update picker on each keystroke
|
||||||
@@ -6269,6 +6287,188 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::SaveTimeChanged(minutes) => {
|
||||||
|
self.savetime_min = minutes.max(0);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::BackupOnSaveChanged(enabled) => {
|
||||||
|
self.backup_on_save = enabled;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::TextFillChanged(filled) => {
|
||||||
|
crate::scene::text::sdf_atlas::set_textfill(filled);
|
||||||
|
self.invalidate_text_everywhere();
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::ClipromptLinesChanged(lines) => {
|
||||||
|
let lines = crate::app::settings::clamp_clipromptlines(lines);
|
||||||
|
self.cliprompt_lines = lines;
|
||||||
|
self.command_line.set_cliprompt_lines(lines.clamp(0, 50) as u8);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::CommandLineFadeChanged(ms) => {
|
||||||
|
let ms = crate::app::settings::clamp_commandline_fade_ms(ms);
|
||||||
|
self.commandline_fade_ms = ms;
|
||||||
|
self.command_line.set_commandline_fade_ms(ms.max(0) as u32);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::ZoomWheelReversedChanged(reversed) => {
|
||||||
|
self.zoom_wheel_reversed = reversed;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::ZoomFactorChanged(factor) => {
|
||||||
|
self.zoom_factor = factor.clamp(3, 100);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::TextEditModeChanged(single) => {
|
||||||
|
self.texteditmode = single;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::DimContinueModeChanged(inherit) => {
|
||||||
|
self.dimension_continue_mode = i16::from(inherit);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::QdimSnapPriorityChanged(priority) => {
|
||||||
|
self.quick_dimension_snap_priority = priority.min(1);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
// The sign carries on/off and the magnitude the mode, so switching
|
||||||
|
// off and back on has to return to the mode that was chosen — the
|
||||||
|
// same convention the status-bar pill uses when it negates.
|
||||||
|
Message::AnnoAutoScaleChanged(mode) => {
|
||||||
|
self.annotation_auto_scale = if mode == 0 {
|
||||||
|
-self.annotation_auto_scale.abs().max(1)
|
||||||
|
} else {
|
||||||
|
mode.clamp(1, 4)
|
||||||
|
};
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Typed into freely; only committed when it parses, so clearing
|
||||||
|
// the field to retype does not snap the crosshair back to zero.
|
||||||
|
Message::SnapAngleInputChanged(value) => {
|
||||||
|
self.snap_angle_input = value;
|
||||||
|
if let Ok(angle) = self.snap_angle_input.trim().parse::<f32>() {
|
||||||
|
if angle.is_finite() {
|
||||||
|
self.snap_angle_deg = angle.rem_euclid(360.0);
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::PolarIncrementChanged(deg) => {
|
||||||
|
if deg.is_finite() && deg > 0.0 {
|
||||||
|
self.polar_increment_deg = deg;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::ShowViewCubeChanged(show) => {
|
||||||
|
self.show_viewcube = show;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::ShowUcsIconChanged(show) => {
|
||||||
|
self.show_ucs_icon = show;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::UcsIconAtOriginChanged(at_origin) => {
|
||||||
|
self.ucs_icon_at_origin = at_origin;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISOLINES is baked into solid meshes, so rebuild once on release.
|
||||||
|
Message::IsolinesChanged(value) => {
|
||||||
|
let value = value.max(0);
|
||||||
|
let changed = self
|
||||||
|
.tabs
|
||||||
|
.get(self.active_tab)
|
||||||
|
.is_some_and(|tab| tab.scene.document.header.isolines != value);
|
||||||
|
if changed {
|
||||||
|
self.set_drawing_var(|header| header.isolines = value);
|
||||||
|
self.isolines_awaiting_regen = true;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::IsolinesReleased => {
|
||||||
|
if std::mem::take(&mut self.isolines_awaiting_regen) {
|
||||||
|
self.regenerate_meshes();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::DispSilhChanged(on) => {
|
||||||
|
self.set_drawing_var(|header| header.display_silhouette = on);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SurfaceUChanged(value) => {
|
||||||
|
self.set_drawing_var(|header| header.surface_u_density = value.clamp(0, 200));
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SurfaceVChanged(value) => {
|
||||||
|
self.set_drawing_var(|header| header.surface_v_density = value.clamp(0, 200));
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SurfaceTypeChanged(value) => {
|
||||||
|
self.set_drawing_var(|header| header.surface_type = value);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SolidHistChanged(record) => {
|
||||||
|
self.set_drawing_var(|header| header.record_solid_history = record);
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::ShowHistChanged(mode) => {
|
||||||
|
self.set_drawing_var(|header| {
|
||||||
|
header.show_solid_history = mode.clamp(0, 2)
|
||||||
|
});
|
||||||
|
let i = self.active_tab;
|
||||||
|
if let Some(tab) = self.tabs.get_mut(i) {
|
||||||
|
tab.scene.bump_geometry();
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::SelectionCyclingChanged(on) => {
|
||||||
|
self.selection_cycling = on;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::OpenFolder(path) => crate::sys::open_url(&path, None),
|
||||||
|
|
||||||
Message::PickDragRectToggled(rectangle) => {
|
Message::PickDragRectToggled(rectangle) => {
|
||||||
self.pick_drag_rect = rectangle;
|
self.pick_drag_rect = rectangle;
|
||||||
self.persist_settings_if_changed();
|
self.persist_settings_if_changed();
|
||||||
|
|||||||
@@ -1535,15 +1535,30 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(axis) = grip.axis {
|
if let Some(axis) = grip.axis {
|
||||||
snapped = cursor_on_projected_axis(
|
let acquired_point = otrack_hit.is_some()
|
||||||
p,
|
|| snap_hit.is_some_and(|hit| {
|
||||||
bounds,
|
hit.snap_type != crate::snap::SnapType::Grid
|
||||||
view_rot,
|
});
|
||||||
eye,
|
if acquired_point {
|
||||||
grip.origin_world,
|
if let Some(axis) = axis.try_normalize() {
|
||||||
axis,
|
// An axis grip still moves on its construction axis, but
|
||||||
)
|
// an acquired object point supplies the exact coordinate
|
||||||
.unwrap_or(snapped);
|
// along that axis. This lets height and draft arrows seat
|
||||||
|
// back onto existing surface/solid geometry.
|
||||||
|
snapped = grip.origin_world
|
||||||
|
+ axis * (snapped - grip.origin_world).dot(axis);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
snapped = cursor_on_projected_axis(
|
||||||
|
p,
|
||||||
|
bounds,
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
grip.origin_world,
|
||||||
|
axis,
|
||||||
|
)
|
||||||
|
.unwrap_or(snapped);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0;
|
let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
@@ -1565,8 +1580,19 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
let apply_started = Instant::now();
|
let apply_started = Instant::now();
|
||||||
let delta = snapped - grip.last_world;
|
let delta = snapped - grip.last_world;
|
||||||
let lengthen = grip.mode == GripEditMode::Lengthen;
|
let menu_action = match grip.mode {
|
||||||
let actions: Vec<_> = if lengthen {
|
GripEditMode::Lengthen => {
|
||||||
|
Some(crate::scene::model::object::GripMenuAction::Lengthen)
|
||||||
|
}
|
||||||
|
GripEditMode::Radius => {
|
||||||
|
Some(crate::scene::model::object::GripMenuAction::Radius)
|
||||||
|
}
|
||||||
|
GripEditMode::ArcLength => {
|
||||||
|
Some(crate::scene::model::object::GripMenuAction::ArcLength)
|
||||||
|
}
|
||||||
|
GripEditMode::Stretch => None,
|
||||||
|
};
|
||||||
|
let actions: Vec<_> = if menu_action.is_some() {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
grip.targets
|
grip.targets
|
||||||
@@ -1581,14 +1607,13 @@ impl OpenCADStudio {
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
if lengthen {
|
if let Some(action) = menu_action {
|
||||||
let original = self
|
let original = self
|
||||||
.grip_originals
|
.grip_originals
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(handle, _)| *handle == grip.handle)
|
.find(|(handle, _)| *handle == grip.handle)
|
||||||
.map(|(_, entity)| entity.clone());
|
.map(|(_, entity)| entity.clone());
|
||||||
if let Some(original) = original {
|
if let Some(original) = original {
|
||||||
let action = crate::scene::model::object::GripMenuAction::Lengthen;
|
|
||||||
let value = crate::scene::view::dispatch::grip_menu_point_value(
|
let value = crate::scene::view::dispatch::grip_menu_point_value(
|
||||||
&original,
|
&original,
|
||||||
grip.grip_id,
|
grip.grip_id,
|
||||||
@@ -1782,6 +1807,26 @@ impl OpenCADStudio {
|
|||||||
let preview_ms = preview_started.elapsed().as_secs_f64() * 1000.0;
|
let preview_ms = preview_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
let geometry_ms = grip_started.elapsed().as_secs_f64() * 1000.0;
|
let geometry_ms = grip_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
self.refresh_selected_grips();
|
self.refresh_selected_grips();
|
||||||
|
// A history rebuild can clamp a requested value or move a derived
|
||||||
|
// construction handle non-linearly. Rebase each active target onto
|
||||||
|
// the freshly rebuilt grip so the hot arrow remains attached to the
|
||||||
|
// live surface and reverses immediately from a geometric limit.
|
||||||
|
let refreshed_targets: Vec<_> = self.tabs[i]
|
||||||
|
.selected_grip_handles
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.zip(self.tabs[i].selected_grips.iter())
|
||||||
|
.map(|(handle, grip)| ((handle, grip.id), grip.world))
|
||||||
|
.collect();
|
||||||
|
if let Some(active) = self.tabs[i].active_grip.as_mut() {
|
||||||
|
for target in &mut active.targets {
|
||||||
|
if let Some((_, world)) = refreshed_targets.iter().find(|((handle, id), _)| {
|
||||||
|
*handle == target.handle && *id == target.grip_id
|
||||||
|
}) {
|
||||||
|
target.last_world = *world;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let grips_ms = grip_started.elapsed().as_secs_f64() * 1000.0 - geometry_ms;
|
let grips_ms = grip_started.elapsed().as_secs_f64() * 1000.0 - geometry_ms;
|
||||||
// Properties are refreshed when the grip is committed or cancelled.
|
// Properties are refreshed when the grip is committed or cancelled.
|
||||||
// Rebuilding the inspector on every pointer event adds no drawing
|
// Rebuilding the inspector on every pointer event adds no drawing
|
||||||
@@ -3080,7 +3125,11 @@ impl OpenCADStudio {
|
|||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let grip_shape = self.tabs[i].selected_grips[grip_index].shape;
|
let grip_shape = self.tabs[i].selected_grips[grip_index].shape;
|
||||||
if grip_shape == crate::scene::model::object::GripShape::Dropdown {
|
if matches!(
|
||||||
|
grip_shape,
|
||||||
|
crate::scene::model::object::GripShape::Dropdown
|
||||||
|
| crate::scene::model::object::GripShape::DropdownAdjacent
|
||||||
|
) {
|
||||||
use crate::entities::traits::EntityTypeOps;
|
use crate::entities::traits::EntityTypeOps;
|
||||||
let items = self.tabs[i]
|
let items = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
@@ -3224,7 +3273,10 @@ impl OpenCADStudio {
|
|||||||
// Engaging click — stay hot, wait for the placement click.
|
// Engaging click — stay hot, wait for the placement click.
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
if grip.mode == GripEditMode::Lengthen {
|
if matches!(
|
||||||
|
grip.mode,
|
||||||
|
GripEditMode::Lengthen | GripEditMode::Radius | GripEditMode::ArcLength
|
||||||
|
) {
|
||||||
self.grip_pending = None;
|
self.grip_pending = None;
|
||||||
self.command_line.input.clear();
|
self.command_line.input.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ pub(super) struct RenderModeChoice(pub acadrust::entities::ViewportRenderMode);
|
|||||||
|
|
||||||
impl std::fmt::Display for RenderModeChoice {
|
impl std::fmt::Display for RenderModeChoice {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_str(crate::modules::view::visual_style::label_for(self.0))
|
f.write_str(crate::t!(crate::modules::view::visual_style::label_for(self.0)).as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -974,7 +974,32 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
// A command may drive a typed scalar by mouse (e.g. a
|
// A command may drive a typed scalar by mouse (e.g. a
|
||||||
// perpendicular distance to a picked object); show that live
|
// perpendicular distance to a picked object); show that live
|
||||||
// value in the box until the user types over it.
|
// value in the box until the user types over it.
|
||||||
let live = tab.active_cmd.as_ref().and_then(|c| c.dyn_live_value(w));
|
let live = tab.active_cmd.as_ref().and_then(|c| c.dyn_live_value(w)).or_else(|| {
|
||||||
|
let grip = tab.active_grip.as_ref()?;
|
||||||
|
let action = match grip.mode {
|
||||||
|
crate::scene::pick::grip::GripEditMode::Lengthen => {
|
||||||
|
crate::scene::model::object::GripMenuAction::Lengthen
|
||||||
|
}
|
||||||
|
crate::scene::pick::grip::GripEditMode::Radius => {
|
||||||
|
crate::scene::model::object::GripMenuAction::Radius
|
||||||
|
}
|
||||||
|
crate::scene::pick::grip::GripEditMode::ArcLength => {
|
||||||
|
crate::scene::model::object::GripMenuAction::ArcLength
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let original = self
|
||||||
|
.grip_originals
|
||||||
|
.iter()
|
||||||
|
.find(|(handle, _)| *handle == grip.handle)
|
||||||
|
.map(|(_, entity)| entity)?;
|
||||||
|
crate::scene::view::dispatch::grip_menu_point_value(
|
||||||
|
original,
|
||||||
|
grip.grip_id,
|
||||||
|
action,
|
||||||
|
w,
|
||||||
|
)
|
||||||
|
});
|
||||||
let boxes: Vec<crate::ui::overlay::DynBox> = tab
|
let boxes: Vec<crate::ui::overlay::DynBox> = tab
|
||||||
.dyn_fields
|
.dyn_fields
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1734,17 +1759,28 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
// viewport stack rather than as a separate row in the main
|
// viewport stack rather than as a separate row in the main
|
||||||
// column — frees up vertical space when no command is active
|
// column — frees up vertical space when no command is active
|
||||||
// and keeps the input close to where the cursor is drawing.
|
// and keeps the input close to where the cursor is drawing.
|
||||||
// Autocomplete shows only when no command is collecting its
|
// Autocomplete shows only when no command or grip-menu action is
|
||||||
// own input (otherwise typed prefixes are coordinates / values).
|
// collecting its own input. A pending Lengthen / Radius / Arc Length
|
||||||
let allow_autocomplete = tab.active_cmd.is_none();
|
// value is numeric input, not the prefix of a new command (for example,
|
||||||
|
// `3` must not open the 3DALIGN / 3DARRAY suggestions).
|
||||||
|
let allow_autocomplete = tab.active_cmd.is_none() && self.grip_pending.is_none();
|
||||||
// Dynamic input captures keystrokes when its fields are showing,
|
// Dynamic input captures keystrokes when its fields are showing,
|
||||||
// so the command-line field must release focus / its on_input.
|
// so the command-line field must release focus / its on_input.
|
||||||
// The MText preview also captures keystrokes (typing edits it), so the
|
// The MText preview also captures keystrokes (typing edits it), so the
|
||||||
// command line must likewise release its on_input there.
|
// command line must likewise release its on_input there.
|
||||||
|
let interactive_value_grip = tab.active_grip.as_ref().is_some_and(|grip| {
|
||||||
|
matches!(
|
||||||
|
grip.mode,
|
||||||
|
crate::scene::pick::grip::GripEditMode::Lengthen
|
||||||
|
| crate::scene::pick::grip::GripEditMode::Radius
|
||||||
|
| crate::scene::pick::grip::GripEditMode::ArcLength
|
||||||
|
)
|
||||||
|
});
|
||||||
let dyn_capturing =
|
let dyn_capturing =
|
||||||
(self.dyn_input
|
(self.dyn_input
|
||||||
&& (tab.active_cmd.is_some() || tab.active_grip.is_some())
|
&& (tab.active_cmd.is_some() || tab.active_grip.is_some())
|
||||||
&& !tab.dyn_fields.is_empty())
|
&& !tab.dyn_fields.is_empty()
|
||||||
|
&& !interactive_value_grip)
|
||||||
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
||||||
|| self.text_inline.is_some();
|
|| self.text_inline.is_some();
|
||||||
// The workspace row is: left edge stack, viewport, right edge stack.
|
// The workspace row is: left edge stack, viewport, right edge stack.
|
||||||
|
|||||||
@@ -191,8 +191,8 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
super::super::ModalKind::Options => sized_flow(
|
super::super::ModalKind::Options => sized_flow(
|
||||||
ex,
|
ex,
|
||||||
540,
|
880,
|
||||||
560,
|
620,
|
||||||
|flow| {
|
|flow| {
|
||||||
crate::ui::window::options::view_window(
|
crate::ui::window::options::view_window(
|
||||||
&self.default_save_format,
|
&self.default_save_format,
|
||||||
@@ -209,6 +209,55 @@ impl OpenCADStudio {
|
|||||||
pick_add: self.pick_add,
|
pick_add: self.pick_add,
|
||||||
pick_drag_rect: self.pick_drag_rect,
|
pick_drag_rect: self.pick_drag_rect,
|
||||||
grip_object_limit: self.grip_object_limit,
|
grip_object_limit: self.grip_object_limit,
|
||||||
|
selection_cycling: self.selection_cycling,
|
||||||
|
},
|
||||||
|
crate::ui::window::options::AppPrefs {
|
||||||
|
savetime_min: self.savetime_min,
|
||||||
|
backup_on_save: self.backup_on_save,
|
||||||
|
textfill: crate::scene::text::sdf_atlas::textfill(),
|
||||||
|
cliprompt_lines: self.cliprompt_lines,
|
||||||
|
commandline_fade_ms: self.commandline_fade_ms,
|
||||||
|
zoom_wheel_reversed: self.zoom_wheel_reversed,
|
||||||
|
zoom_factor: self.zoom_factor,
|
||||||
|
texteditmode: self.texteditmode,
|
||||||
|
dimension_continue_mode: self.dimension_continue_mode,
|
||||||
|
qdim_snap_priority: self.quick_dimension_snap_priority,
|
||||||
|
annotation_auto_scale: self.annotation_auto_scale,
|
||||||
|
polar_increment_deg: self.polar_increment_deg,
|
||||||
|
show_viewcube: self.show_viewcube,
|
||||||
|
show_ucs_icon: self.show_ucs_icon,
|
||||||
|
ucs_icon_at_origin: self.ucs_icon_at_origin,
|
||||||
|
},
|
||||||
|
&self.snap_angle_input,
|
||||||
|
{
|
||||||
|
let header = self
|
||||||
|
.tabs
|
||||||
|
.get(self.active_tab)
|
||||||
|
.map(|tab| &tab.scene.document.header);
|
||||||
|
crate::ui::window::options::DrawingPrefs {
|
||||||
|
available: header.is_some(),
|
||||||
|
isolines: header.map_or(4, |h| h.isolines),
|
||||||
|
display_silhouette: header
|
||||||
|
.is_some_and(|h| h.display_silhouette),
|
||||||
|
surface_u: header.map_or(6, |h| h.surface_u_density),
|
||||||
|
surface_v: header.map_or(6, |h| h.surface_v_density),
|
||||||
|
surface_type: header.map_or(6, |h| h.surface_type),
|
||||||
|
record_solid_history: header
|
||||||
|
.is_some_and(|h| h.record_solid_history),
|
||||||
|
show_solid_history: header
|
||||||
|
.map_or(1, |h| h.show_solid_history),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crate::ui::window::options::Folders {
|
||||||
|
config: crate::config::config_dir()
|
||||||
|
.map(|p| p.display().to_string()),
|
||||||
|
plot_styles: crate::io::plot_style::plot_styles_dir()
|
||||||
|
.ok()
|
||||||
|
.map(|p| p.display().to_string()),
|
||||||
|
plugins: crate::plugin::external::plugins_dir()
|
||||||
|
.map(|p| p.display().to_string()),
|
||||||
|
autosave: crate::config::config_dir()
|
||||||
|
.map(|_| std::env::temp_dir().display().to_string()),
|
||||||
},
|
},
|
||||||
self.double_click_block_refedit,
|
self.double_click_block_refedit,
|
||||||
self.double_click_block_attedit,
|
self.double_click_block_attedit,
|
||||||
|
|||||||
@@ -744,6 +744,88 @@ impl CadCommand for TwoValuePromptCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Mid between 2 points (MTP / M2P) ────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Point-entry modifier that prompts for two points, draws a preview connecting line
|
||||||
|
/// with a midpoint marker, and returns the midpoint to the caller.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Mid2PointCommand {
|
||||||
|
pub first_point: Option<DVec3>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mid2PointCommand {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { first_point: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for Mid2PointCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"MTP"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
if self.first_point.is_none() {
|
||||||
|
crate::t!("_mtp Specify first point of mid:").into_owned()
|
||||||
|
} else {
|
||||||
|
crate::t!("_mtp Specify second point of mid:").into_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
|
if let Some(first) = self.first_point {
|
||||||
|
let mid = (first + pt) * 0.5;
|
||||||
|
CmdResult::ReturnPoint(mid)
|
||||||
|
} else {
|
||||||
|
self.first_point = Some(pt);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
|
||||||
|
let mut wires = Vec::new();
|
||||||
|
if let Some(first) = self.first_point {
|
||||||
|
// Rubber-band line connecting the first point to the cursor
|
||||||
|
wires.push(WireModel::solid_f64(
|
||||||
|
"mtp_rubber_band".to_string(),
|
||||||
|
vec![[first.x, first.y, first.z], [pt.x, pt.y, pt.z]],
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
// Midpoint marker (AutoCAD style triangle glyph)
|
||||||
|
let mid = (first + pt) * 0.5;
|
||||||
|
let dist = (pt - first).length();
|
||||||
|
let s = (dist * 0.02).clamp(0.5, 10.0);
|
||||||
|
let h = s * 1.5;
|
||||||
|
let w = s * 1.0;
|
||||||
|
wires.push(WireModel::solid_f64(
|
||||||
|
"mtp_mid_triangle".to_string(),
|
||||||
|
vec![
|
||||||
|
[mid.x, mid.y + h * (2.0 / 3.0), mid.z],
|
||||||
|
[mid.x - w, mid.y - h * (1.0 / 3.0), mid.z],
|
||||||
|
[mid.x + w, mid.y - h * (1.0 / 3.0), mid.z],
|
||||||
|
],
|
||||||
|
WireModel::CYAN,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
wires
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory::submit!(CommandRegistration {
|
||||||
|
names: &["MTP", "M2P"],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/// Generic interactive front-end for a keyword command that operates on the
|
/// Generic interactive front-end for a keyword command that operates on the
|
||||||
/// current selection (CHPROP, ADJUST, XDATA, UNDERLAY, DRAWORDER…). If nothing
|
/// current selection (CHPROP, ADJUST, XDATA, UNDERLAY, DRAWORDER…). If nothing
|
||||||
/// is selected when it starts it first gathers a selection (Enter confirms),
|
/// is selected when it starts it first gathers a selection (Enter confirms),
|
||||||
@@ -1689,6 +1771,8 @@ pub enum CmdResult {
|
|||||||
UndoDocument,
|
UndoDocument,
|
||||||
/// Sets the TEXTEDITMODE system variable and ends the command.
|
/// Sets the TEXTEDITMODE system variable and ends the command.
|
||||||
SetTexteditMode(bool),
|
SetTexteditMode(bool),
|
||||||
|
/// Return a resolved point back to a suspended parent command (e.g. MTP / M2P).
|
||||||
|
ReturnPoint(DVec3),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What kind of value the active command is currently asking for. Drives
|
/// What kind of value the active command is currently asking for. Drives
|
||||||
@@ -2485,3 +2569,42 @@ mod constraint_registry_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mid2point_command() {
|
||||||
|
let mut cmd = Mid2PointCommand::new();
|
||||||
|
assert_eq!(cmd.name(), "MTP");
|
||||||
|
assert_eq!(cmd.prompt(), "_mtp Specify first point of mid:");
|
||||||
|
|
||||||
|
// First point
|
||||||
|
let res = cmd.on_point(DVec3::new(10.0, 20.0, 0.0));
|
||||||
|
assert!(matches!(res, CmdResult::NeedPoint));
|
||||||
|
assert_eq!(cmd.prompt(), "_mtp Specify second point of mid:");
|
||||||
|
|
||||||
|
// Preview wires
|
||||||
|
let wires = cmd.on_preview_wires(DVec3::new(30.0, 40.0, 0.0));
|
||||||
|
assert!(!wires.is_empty());
|
||||||
|
|
||||||
|
// Second point
|
||||||
|
let res = cmd.on_point(DVec3::new(30.0, 40.0, 0.0));
|
||||||
|
assert!(matches!(res, CmdResult::ReturnPoint(mid) if mid == DVec3::new(20.0, 30.0, 0.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mid2point_cancel() {
|
||||||
|
let mut cmd = Mid2PointCommand::new();
|
||||||
|
assert!(matches!(cmd.on_escape(), CmdResult::Cancel));
|
||||||
|
assert!(matches!(cmd.on_enter(), CmdResult::Cancel));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mtp_registered() {
|
||||||
|
let names = all_registered_command_names();
|
||||||
|
assert!(names.contains(&"MTP"));
|
||||||
|
assert!(names.contains(&"M2P"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -364,19 +364,44 @@ impl crate::entities::traits::Grippable for Arc {
|
|||||||
action: crate::scene::model::object::GripMenuAction,
|
action: crate::scene::model::object::GripMenuAction,
|
||||||
point: glam::DVec3,
|
point: glam::DVec3,
|
||||||
) -> Option<f64> {
|
) -> Option<f64> {
|
||||||
|
use cadkernel::geom2d::{Circle as KernelCircle, Curve as KernelCurve, Vec2};
|
||||||
use crate::scene::model::object::GripMenuAction as A;
|
use crate::scene::model::object::GripMenuAction as A;
|
||||||
if !matches!(action, A::Lengthen) || self.radius <= 1.0e-9 {
|
if self.radius <= 1.0e-9 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let (x, y, _) = crate::scene::view::transform::wcs_point_to_ocs(
|
let curve = crate::entities::curve::arc_curve(self);
|
||||||
(point.x, point.y, point.z),
|
let point = curve.plane.project(point.to_array())?;
|
||||||
(self.normal.x, self.normal.y, self.normal.z),
|
let KernelCurve::Arc(arc) = &curve.curve else {
|
||||||
);
|
unreachable!("arc entity must produce an arc curve")
|
||||||
let cursor_angle = (y - self.center.y).atan2(x - self.center.x);
|
};
|
||||||
let current_sweep = (self.end_angle - self.start_angle).rem_euclid(TAU);
|
if matches!(action, A::Radius) && grip_id == 3 {
|
||||||
|
let radius = Vec2::from(point).distance(Vec2::from(arc.centre));
|
||||||
|
return (radius > 1.0e-9).then_some(radius);
|
||||||
|
}
|
||||||
|
if matches!(action, A::ArcLength) && grip_id == 3 {
|
||||||
|
let middle = Vec2::from(curve.curve.point_at(0.5));
|
||||||
|
let tangent = Vec2::from(curve.curve.tangent_at(0.5)).normalize()?;
|
||||||
|
let length = curve.length() + (Vec2::from(point) - middle).dot(tangent);
|
||||||
|
let circumference = KernelCircle {
|
||||||
|
centre: arc.centre,
|
||||||
|
radius: arc.radius,
|
||||||
|
}
|
||||||
|
.length();
|
||||||
|
return (length > 1.0e-9 && length < circumference - 1.0e-9).then_some(length);
|
||||||
|
}
|
||||||
|
if !matches!(action, A::Lengthen) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let cursor_angle = KernelCurve::Circle(KernelCircle {
|
||||||
|
centre: arc.centre,
|
||||||
|
radius: arc.radius,
|
||||||
|
})
|
||||||
|
.parameter_at(point)
|
||||||
|
* TAU;
|
||||||
|
let current_sweep = arc.sweep();
|
||||||
let desired_sweep = match grip_id {
|
let desired_sweep = match grip_id {
|
||||||
1 => (self.end_angle - cursor_angle).rem_euclid(TAU),
|
1 => (arc.end_angle - cursor_angle).rem_euclid(TAU),
|
||||||
2 => (cursor_angle - self.start_angle).rem_euclid(TAU),
|
2 => (cursor_angle - arc.start_angle).rem_euclid(TAU),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
if desired_sweep <= 1.0e-9 {
|
if desired_sweep <= 1.0e-9 {
|
||||||
@@ -458,3 +483,48 @@ impl crate::entities::traits::MassPropsCalc for acadrust::entities::Arc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::entities::traits::Grippable;
|
||||||
|
use crate::scene::model::object::GripMenuAction;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn midpoint_grip_drives_radius_and_arc_length_in_entity_plane() {
|
||||||
|
let mut arc = Arc::default();
|
||||||
|
arc.center = acadrust::types::Vector3::new(1.0, 2.0, 3.0);
|
||||||
|
arc.normal = acadrust::types::Vector3::new(0.0, 1.0, 0.0);
|
||||||
|
arc.radius = 2.0;
|
||||||
|
arc.start_angle = 0.0;
|
||||||
|
arc.end_angle = std::f64::consts::FRAC_PI_2;
|
||||||
|
|
||||||
|
let curve = crate::entities::curve::arc_curve(&arc);
|
||||||
|
let middle_angle = std::f64::consts::FRAC_PI_4;
|
||||||
|
let radius_point = curve.plane.point_at([
|
||||||
|
arc.center.x + 5.0 * middle_angle.cos(),
|
||||||
|
arc.center.y + 5.0 * middle_angle.sin(),
|
||||||
|
]);
|
||||||
|
let radius = arc
|
||||||
|
.grip_menu_point_value(3, GripMenuAction::Radius, radius_point.into())
|
||||||
|
.expect("radius value");
|
||||||
|
assert!((radius - 5.0).abs() < 1.0e-9);
|
||||||
|
|
||||||
|
let middle = glam::DVec3::from_array(curve.point_at(0.5));
|
||||||
|
let tangent = glam::DVec3::from_array(curve.tangent_at(0.5)).normalize();
|
||||||
|
let wanted_length = curve.length() + 1.25;
|
||||||
|
let arc_length = arc
|
||||||
|
.grip_menu_point_value(
|
||||||
|
3,
|
||||||
|
GripMenuAction::ArcLength,
|
||||||
|
middle + tangent * 1.25,
|
||||||
|
)
|
||||||
|
.expect("arc length value");
|
||||||
|
assert!((arc_length - wanted_length).abs() < 1.0e-9);
|
||||||
|
|
||||||
|
arc.apply_grip_menu_value(3, GripMenuAction::ArcLength, arc_length);
|
||||||
|
assert!(
|
||||||
|
(crate::entities::curve::arc_curve(&arc).length() - wanted_length).abs() < 1.0e-9
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -256,7 +256,89 @@ impl RenderConvertible for Circle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::impl_entity_basics!(Circle);
|
impl crate::entities::traits::Grippable for Circle {
|
||||||
|
fn grips(&self) -> Vec<GripDef> {
|
||||||
|
grips(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||||
|
apply_grip(self, grip_id, apply);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grip_menu(
|
||||||
|
&self,
|
||||||
|
grip_id: usize,
|
||||||
|
) -> Vec<crate::scene::model::object::GripMenuItem> {
|
||||||
|
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
||||||
|
let mut items = vec![GripMenuItem {
|
||||||
|
label: "Stretch",
|
||||||
|
action: GripMenuAction::Stretch,
|
||||||
|
}];
|
||||||
|
if (1..=4).contains(&grip_id) {
|
||||||
|
items.push(GripMenuItem {
|
||||||
|
label: "Radius",
|
||||||
|
action: GripMenuAction::Radius,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
items
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grip_menu_value_prompt(
|
||||||
|
&self,
|
||||||
|
_grip_id: usize,
|
||||||
|
action: crate::scene::model::object::GripMenuAction,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
matches!(action, crate::scene::model::object::GripMenuAction::Radius)
|
||||||
|
.then_some("New radius")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grip_menu_point_value(
|
||||||
|
&self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: crate::scene::model::object::GripMenuAction,
|
||||||
|
point: glam::DVec3,
|
||||||
|
) -> Option<f64> {
|
||||||
|
if !matches!(action, crate::scene::model::object::GripMenuAction::Radius)
|
||||||
|
|| !(1..=4).contains(&grip_id)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let plane = crate::entities::curve::circle_curve(self).plane;
|
||||||
|
let point = plane.project(point.to_array())?;
|
||||||
|
let radius = (point[0] - self.center.x).hypot(point[1] - self.center.y);
|
||||||
|
(radius > 1.0e-9).then_some(radius)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu_value(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: crate::scene::model::object::GripMenuAction,
|
||||||
|
value: f64,
|
||||||
|
) {
|
||||||
|
if matches!(action, crate::scene::model::object::GripMenuAction::Radius)
|
||||||
|
&& (1..=4).contains(&grip_id)
|
||||||
|
&& value > 1.0e-9
|
||||||
|
{
|
||||||
|
self.radius = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::entities::traits::PropertyEditable for Circle {
|
||||||
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||||
|
properties(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||||
|
apply_geom_prop(self, field, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::entities::traits::Transformable for Circle {
|
||||||
|
fn apply_transform(&mut self, transform: &EntityTransform) {
|
||||||
|
apply_transform(self, transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl crate::entities::traits::MassPropsCalc for Circle {
|
impl crate::entities::traits::MassPropsCalc for Circle {
|
||||||
fn mass_props(&self) -> crate::entities::traits::MassProps {
|
fn mass_props(&self) -> crate::entities::traits::MassProps {
|
||||||
@@ -276,6 +358,28 @@ impl crate::entities::traits::MassPropsCalc for Circle {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quadrant_grips_offer_interactive_radius() {
|
||||||
|
use crate::entities::traits::Grippable;
|
||||||
|
use crate::scene::model::object::GripMenuAction;
|
||||||
|
|
||||||
|
let mut circle = Circle::default();
|
||||||
|
circle.radius = 2.0;
|
||||||
|
|
||||||
|
assert_eq!(circle.grip_menu(0).len(), 1);
|
||||||
|
assert!(circle
|
||||||
|
.grip_menu(1)
|
||||||
|
.iter()
|
||||||
|
.any(|item| item.action == GripMenuAction::Radius));
|
||||||
|
assert_eq!(
|
||||||
|
circle.grip_menu_value_prompt(1, GripMenuAction::Radius),
|
||||||
|
Some("New radius")
|
||||||
|
);
|
||||||
|
|
||||||
|
circle.apply_grip_menu_value(1, GripMenuAction::Radius, 5.0);
|
||||||
|
assert_eq!(circle.radius, 5.0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_circle_segments_scales_with_zoom() {
|
fn test_circle_segments_scales_with_zoom() {
|
||||||
let r = 10.0;
|
let r = 10.0;
|
||||||
@@ -361,4 +465,3 @@ mod tests {
|
|||||||
assert!(close_count > far_count, "{close_count} <= {far_count}");
|
assert!(close_count > far_count, "{close_count} <= {far_count}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4,6 +4,8 @@ use std::collections::HashSet;
|
|||||||
use crate::scene::model::object::{GripDef, GripShape, PropValue, Property};
|
use crate::scene::model::object::{GripDef, GripShape, PropValue, Property};
|
||||||
use crate::scene::model::wire_model::PatternStationPiece;
|
use crate::scene::model::wire_model::PatternStationPiece;
|
||||||
|
|
||||||
|
pub const VARIES_LABEL: &str = "*VARIES*";
|
||||||
|
|
||||||
/// Linear / angular unit format pulled from the document header so the
|
/// Linear / angular unit format pulled from the document header so the
|
||||||
/// per-thread properties pipeline can format values consistently without
|
/// per-thread properties pipeline can format values consistently without
|
||||||
/// passing the document through every callsite.
|
/// passing the document through every callsite.
|
||||||
|
|||||||
@@ -13,13 +13,19 @@ use crate::entities::common::{
|
|||||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
||||||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
||||||
use crate::scene::model::object::{
|
use crate::scene::model::object::{
|
||||||
GripApply, GripDef, PropSection, PropValue, Property,
|
GripApply, GripDef, GripMenuAction, GripMenuItem, PropSection, PropValue, Property,
|
||||||
};
|
};
|
||||||
use crate::scene::model::wire_model::SnapHint;
|
use crate::scene::model::wire_model::SnapHint;
|
||||||
|
|
||||||
const NAN: [f64; 3] = [f64::NAN; 3];
|
const NAN: [f64; 3] = [f64::NAN; 3];
|
||||||
const SECTION_SLICE_APP: &str = "IsSlice";
|
const SECTION_SLICE_APP: &str = "IsSlice";
|
||||||
const SECTION_THICKNESS_APP: &str = "ThicknessDepth";
|
const SECTION_THICKNESS_APP: &str = "ThicknessDepth";
|
||||||
|
const SECTION_GRIP_LEFT: usize = 100_000;
|
||||||
|
const SECTION_GRIP_RIGHT: usize = 100_001;
|
||||||
|
const SECTION_GRIP_TOP: usize = 100_002;
|
||||||
|
const SECTION_GRIP_BOTTOM: usize = 100_003;
|
||||||
|
const SECTION_GRIP_NORMAL: usize = 100_004;
|
||||||
|
const SECTION_GRIP_STATE: usize = 100_005;
|
||||||
|
|
||||||
pub(crate) fn section_is_slice(entity: &ExtendedEntity) -> bool {
|
pub(crate) fn section_is_slice(entity: &ExtendedEntity) -> bool {
|
||||||
entity
|
entity
|
||||||
@@ -1563,15 +1569,56 @@ fn move_extents(
|
|||||||
|
|
||||||
fn grips(entity: &ExtendedEntity) -> Vec<GripDef> {
|
fn grips(entity: &ExtendedEntity) -> Vec<GripDef> {
|
||||||
match &entity.data {
|
match &entity.data {
|
||||||
ExtendedEntityData::SectionObject(data) => data
|
ExtendedEntityData::SectionObject(data) => {
|
||||||
.vertices
|
let mut grips: Vec<GripDef> = if section_kind(entity, data) == "Plane"
|
||||||
.iter()
|
&& data.vertices.len() == 2
|
||||||
.chain(data.back_line_vertices.iter())
|
&& data.back_line_vertices.is_empty()
|
||||||
.enumerate()
|
{
|
||||||
.map(|(index, point)| {
|
section_plane_edge_grips(data)
|
||||||
square_grip(index, glam::DVec3::new(point.x, point.y, point.z))
|
} else {
|
||||||
})
|
data.vertices
|
||||||
.collect(),
|
.iter()
|
||||||
|
.chain(data.back_line_vertices.iter())
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, point)| {
|
||||||
|
square_grip(index, glam::DVec3::new(point.x, point.y, point.z))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
if let Some(right) = data.vertices.last() {
|
||||||
|
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
||||||
|
let edge_center_offset = (data.top_height - data.bottom_height) * 0.5;
|
||||||
|
let anchor = *right + vertical * edge_center_offset;
|
||||||
|
grips.push(GripDef {
|
||||||
|
id: SECTION_GRIP_STATE,
|
||||||
|
world: glam::DVec3::new(anchor.x, anchor.y, anchor.z),
|
||||||
|
is_midpoint: false,
|
||||||
|
shape: crate::scene::model::object::GripShape::DropdownAdjacent,
|
||||||
|
dir: None,
|
||||||
|
axis: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(center) = section_center(data) {
|
||||||
|
let normal = section_viewing_direction(data);
|
||||||
|
let span = data
|
||||||
|
.vertices
|
||||||
|
.first()
|
||||||
|
.zip(data.vertices.last())
|
||||||
|
.map_or(1.0, |(first, last)| (*last - *first).length());
|
||||||
|
let offset = (span * 0.08).max(1.0e-6);
|
||||||
|
let normal_world = glam::DVec3::new(normal.x, normal.y, normal.z);
|
||||||
|
grips.push(GripDef {
|
||||||
|
id: SECTION_GRIP_NORMAL,
|
||||||
|
world: glam::DVec3::new(center.x, center.y, center.z)
|
||||||
|
- normal_world * offset,
|
||||||
|
is_midpoint: false,
|
||||||
|
shape: crate::scene::model::object::GripShape::Triangle,
|
||||||
|
dir: Some(-normal_world),
|
||||||
|
axis: Some(normal_world),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
grips
|
||||||
|
}
|
||||||
ExtendedEntityData::ArcAlignedText(data) => vec![center_grip(
|
ExtendedEntityData::ArcAlignedText(data) => vec![center_grip(
|
||||||
0,
|
0,
|
||||||
glam::DVec3::new(data.center.x, data.center.y, data.center.z),
|
glam::DVec3::new(data.center.x, data.center.y, data.center.z),
|
||||||
@@ -1643,7 +1690,41 @@ fn grips(entity: &ExtendedEntity) -> Vec<GripDef> {
|
|||||||
fn apply_grip(entity: &mut ExtendedEntity, grip_id: usize, apply: GripApply) {
|
fn apply_grip(entity: &mut ExtendedEntity, grip_id: usize, apply: GripApply) {
|
||||||
match &mut entity.data {
|
match &mut entity.data {
|
||||||
ExtendedEntityData::SectionObject(data) => {
|
ExtendedEntityData::SectionObject(data) => {
|
||||||
if grip_id < data.vertices.len() {
|
if matches!(
|
||||||
|
grip_id,
|
||||||
|
SECTION_GRIP_LEFT
|
||||||
|
| SECTION_GRIP_RIGHT
|
||||||
|
| SECTION_GRIP_TOP
|
||||||
|
| SECTION_GRIP_BOTTOM
|
||||||
|
) {
|
||||||
|
apply_section_plane_edge_grip(data, grip_id, apply);
|
||||||
|
} else if grip_id == SECTION_GRIP_NORMAL {
|
||||||
|
let Some(center) = section_center(data) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let normal = section_viewing_direction(data);
|
||||||
|
let span = data
|
||||||
|
.vertices
|
||||||
|
.first()
|
||||||
|
.zip(data.vertices.last())
|
||||||
|
.map_or(1.0, |(first, last)| (*last - *first).length());
|
||||||
|
let offset = (span * 0.08).max(1.0e-6);
|
||||||
|
let current = center - normal * offset;
|
||||||
|
let delta = match apply {
|
||||||
|
GripApply::Translate(delta) => Vector3::new(delta.x, delta.y, delta.z),
|
||||||
|
GripApply::Absolute(position) => {
|
||||||
|
Vector3::new(position.x, position.y, position.z) - current
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let constrained = normal * delta.dot(&normal);
|
||||||
|
for point in data
|
||||||
|
.vertices
|
||||||
|
.iter_mut()
|
||||||
|
.chain(data.back_line_vertices.iter_mut())
|
||||||
|
{
|
||||||
|
*point = *point + constrained;
|
||||||
|
}
|
||||||
|
} else if grip_id < data.vertices.len() {
|
||||||
apply_point(&mut data.vertices[grip_id], apply);
|
apply_point(&mut data.vertices[grip_id], apply);
|
||||||
} else if let Some(point) = data
|
} else if let Some(point) = data
|
||||||
.back_line_vertices
|
.back_line_vertices
|
||||||
@@ -1704,6 +1785,115 @@ fn apply_grip(entity: &mut ExtendedEntity, grip_id: usize, apply: GripApply) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn section_center(data: &SectionObjectData) -> Option<Vector3> {
|
||||||
|
let (first, last) = data.vertices.first().zip(data.vertices.last())?;
|
||||||
|
Some((*first + *last) * 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_plane_edge_grips(data: &SectionObjectData) -> Vec<GripDef> {
|
||||||
|
let Some((first, last)) = data.vertices.first().zip(data.vertices.last()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let center = (*first + *last) * 0.5;
|
||||||
|
let tangent = section_tangent(data);
|
||||||
|
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
||||||
|
let edge_center_offset = (data.top_height - data.bottom_height) * 0.5;
|
||||||
|
let definitions = [
|
||||||
|
(
|
||||||
|
SECTION_GRIP_LEFT,
|
||||||
|
*first + vertical * edge_center_offset,
|
||||||
|
vertical,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SECTION_GRIP_RIGHT,
|
||||||
|
*last + vertical * edge_center_offset,
|
||||||
|
vertical,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SECTION_GRIP_TOP,
|
||||||
|
center + vertical * data.top_height,
|
||||||
|
tangent,
|
||||||
|
Some(vertical),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SECTION_GRIP_BOTTOM,
|
||||||
|
center - vertical * data.bottom_height,
|
||||||
|
tangent,
|
||||||
|
Some(vertical),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
definitions
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, world, edge, axis)| GripDef {
|
||||||
|
id,
|
||||||
|
world: glam::DVec3::new(world.x, world.y, world.z),
|
||||||
|
is_midpoint: false,
|
||||||
|
shape: crate::scene::model::object::GripShape::Rectangle,
|
||||||
|
dir: Some(glam::DVec3::new(edge.x, edge.y, edge.z)),
|
||||||
|
axis: axis.map(|axis| glam::DVec3::new(axis.x, axis.y, axis.z)),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_section_plane_edge_grip(
|
||||||
|
data: &mut SectionObjectData,
|
||||||
|
grip_id: usize,
|
||||||
|
apply: GripApply,
|
||||||
|
) {
|
||||||
|
let Some((first, last)) = data.vertices.first().zip(data.vertices.last()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let center = (*first + *last) * 0.5;
|
||||||
|
let tangent = section_tangent(data);
|
||||||
|
let vertical = normalized(data.vertical_direction, Vector3::UNIT_Z);
|
||||||
|
let edge_center_offset = (data.top_height - data.bottom_height) * 0.5;
|
||||||
|
let current = match grip_id {
|
||||||
|
SECTION_GRIP_LEFT => *first + vertical * edge_center_offset,
|
||||||
|
SECTION_GRIP_RIGHT => *last + vertical * edge_center_offset,
|
||||||
|
SECTION_GRIP_TOP => center + vertical * data.top_height,
|
||||||
|
SECTION_GRIP_BOTTOM => center - vertical * data.bottom_height,
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let delta = match apply {
|
||||||
|
GripApply::Translate(delta) => Vector3::new(delta.x, delta.y, delta.z),
|
||||||
|
GripApply::Absolute(position) => {
|
||||||
|
Vector3::new(position.x, position.y, position.z) - current
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let span = (*last - *first).length();
|
||||||
|
let minimum_span = 1.0e-6;
|
||||||
|
let in_plane_vertical = normalized(
|
||||||
|
vertical - tangent * vertical.dot(&tangent),
|
||||||
|
vertical,
|
||||||
|
);
|
||||||
|
let vertical_change = in_plane_vertical * delta.dot(&in_plane_vertical);
|
||||||
|
match grip_id {
|
||||||
|
SECTION_GRIP_LEFT => {
|
||||||
|
let tangent_change = delta
|
||||||
|
.dot(&tangent)
|
||||||
|
.min((span - minimum_span).max(0.0));
|
||||||
|
data.vertices[0] = data.vertices[0] + tangent * tangent_change + vertical_change;
|
||||||
|
}
|
||||||
|
SECTION_GRIP_RIGHT => {
|
||||||
|
let last = data.vertices.len() - 1;
|
||||||
|
let tangent_change = delta
|
||||||
|
.dot(&tangent)
|
||||||
|
.max((-span + minimum_span).min(0.0));
|
||||||
|
data.vertices[last] =
|
||||||
|
data.vertices[last] + tangent * tangent_change + vertical_change;
|
||||||
|
}
|
||||||
|
SECTION_GRIP_TOP => {
|
||||||
|
data.top_height = (data.top_height + delta.dot(&vertical)).max(0.0);
|
||||||
|
}
|
||||||
|
SECTION_GRIP_BOTTOM => {
|
||||||
|
data.bottom_height = (data.bottom_height - delta.dot(&vertical)).max(0.0);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn entity_transform(transform: &EntityTransform) -> Transform {
|
fn entity_transform(transform: &EntityTransform) -> Transform {
|
||||||
match transform {
|
match transform {
|
||||||
EntityTransform::Translate(delta) => {
|
EntityTransform::Translate(delta) => {
|
||||||
@@ -1943,6 +2133,60 @@ impl Grippable for ExtendedEntity {
|
|||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||||||
apply_grip(self, grip_id, apply);
|
apply_grip(self, grip_id, apply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grip_menu(&self, grip_id: usize) -> Vec<GripMenuItem> {
|
||||||
|
if grip_id != SECTION_GRIP_STATE {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let ExtendedEntityData::SectionObject(data) = &self.data else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let current = section_kind(self, data);
|
||||||
|
vec![
|
||||||
|
GripMenuItem {
|
||||||
|
label: if current == "Plane" { "✓ Plane" } else { "Plane" },
|
||||||
|
action: GripMenuAction::SectionPlane,
|
||||||
|
},
|
||||||
|
GripMenuItem {
|
||||||
|
label: if current == "Slice" { "✓ Slice" } else { "Slice" },
|
||||||
|
action: GripMenuAction::SectionSlice,
|
||||||
|
},
|
||||||
|
GripMenuItem {
|
||||||
|
label: if current == "Boundary" {
|
||||||
|
"✓ Boundary"
|
||||||
|
} else {
|
||||||
|
"Boundary"
|
||||||
|
},
|
||||||
|
action: GripMenuAction::SectionBoundary,
|
||||||
|
},
|
||||||
|
GripMenuItem {
|
||||||
|
label: if current == "Volume" { "✓ Volume" } else { "Volume" },
|
||||||
|
action: GripMenuAction::SectionVolume,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu(&mut self, grip_id: usize, action: GripMenuAction) {
|
||||||
|
if grip_id != SECTION_GRIP_STATE
|
||||||
|
|| !matches!(self.data, ExtendedEntityData::SectionObject(_))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let value = match action {
|
||||||
|
GripMenuAction::SectionPlane => "Plane",
|
||||||
|
GripMenuAction::SectionSlice => "Slice",
|
||||||
|
GripMenuAction::SectionBoundary => "Boundary",
|
||||||
|
GripMenuAction::SectionVolume => "Volume",
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let ExtendedEntityData::SectionObject(data) = &self.data else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if section_kind(self, data) == value {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
apply_section_prop(self, "ext_section_state", value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyEditable for ExtendedEntity {
|
impl PropertyEditable for ExtendedEntity {
|
||||||
@@ -1960,3 +2204,35 @@ impl Transformable for ExtendedEntity {
|
|||||||
apply_transform(self, transform);
|
apply_transform(self, transform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use acadrust::entities::EntityCommon;
|
||||||
|
use acadrust::types::Color;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reselecting_slice_keeps_its_depth() {
|
||||||
|
let mut entity = ExtendedEntity {
|
||||||
|
common: EntityCommon::new(),
|
||||||
|
data: ExtendedEntityData::SectionObject(SectionObjectData {
|
||||||
|
state: 1,
|
||||||
|
flags: 5,
|
||||||
|
name: String::new(),
|
||||||
|
vertical_direction: Vector3::UNIT_Z,
|
||||||
|
top_height: 1.0,
|
||||||
|
bottom_height: 1.0,
|
||||||
|
indicator_alpha: 70,
|
||||||
|
indicator_color: Color::from_index(9),
|
||||||
|
back_line_vertices: Vec::new(),
|
||||||
|
vertices: vec![Vector3::ZERO, Vector3::new(60.0, 0.0, 0.0)],
|
||||||
|
settings_handle: Handle::NULL,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
set_section_slice_metadata(&mut entity, true, 0.25);
|
||||||
|
|
||||||
|
entity.apply_grip_menu(SECTION_GRIP_STATE, GripMenuAction::SectionSlice);
|
||||||
|
|
||||||
|
assert_eq!(section_slice_depth(&entity), Some(0.25));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ use crate::t;
|
|||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::{
|
use crate::entities::common::{
|
||||||
edit_prop as edit, edit_scalar_prop as edit_scalar, format_area, format_length, parse_f64,
|
edit_prop as edit, edit_scalar_prop as edit_scalar, format_area, format_length, parse_f64,
|
||||||
rectangle_grip, ro_prop as ro, square_grip, stepper_prop as stepper,
|
rectangle_grip, ro_prop as ro, square_grip, stepper_prop as stepper, VARIES_LABEL,
|
||||||
};
|
};
|
||||||
use crate::entities::traits::RenderConvertible;
|
use crate::entities::traits::RenderConvertible;
|
||||||
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
||||||
@@ -18,14 +18,38 @@ const REVCLOUD_BULGE: f64 = 0.5;
|
|||||||
const MAX_REVCLOUD_VERTICES: usize = 100_000;
|
const MAX_REVCLOUD_VERTICES: usize = 100_000;
|
||||||
const WIDTH_EPSILON: f64 = 1.0e-9;
|
const WIDTH_EPSILON: f64 = 1.0e-9;
|
||||||
|
|
||||||
fn effective_width(width: f64, constant_width: f64) -> f64 {
|
pub(crate) fn vertex_segment_widths(vertex: &LwVertex, constant_width: f64) -> (f64, f64) {
|
||||||
if width > WIDTH_EPSILON {
|
if vertex.start_width > WIDTH_EPSILON || vertex.end_width > WIDTH_EPSILON {
|
||||||
width
|
(vertex.start_width, vertex.end_width)
|
||||||
} else {
|
} else {
|
||||||
constant_width
|
(constant_width, constant_width)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lwpolyline_global_width(pline: &LwPolyline) -> Option<f64> {
|
||||||
|
let count = pline.vertices.len();
|
||||||
|
let seg_count = if pline.is_closed {
|
||||||
|
count
|
||||||
|
} else {
|
||||||
|
count.saturating_sub(1)
|
||||||
|
};
|
||||||
|
if seg_count == 0 {
|
||||||
|
return Some(pline.constant_width);
|
||||||
|
}
|
||||||
|
let (w0_start, w0_end) = vertex_segment_widths(&pline.vertices[0], pline.constant_width);
|
||||||
|
if (w0_start - w0_end).abs() > 1e-6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let w0 = w0_start;
|
||||||
|
for i in 1..seg_count {
|
||||||
|
let (sw, ew) = vertex_segment_widths(&pline.vertices[i], pline.constant_width);
|
||||||
|
if (sw - ew).abs() > 1e-6 || (sw - w0).abs() > 1e-6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(w0)
|
||||||
|
}
|
||||||
|
|
||||||
/// Midpoint position on an arc segment defined by its bulge.
|
/// Midpoint position on an arc segment defined by its bulge.
|
||||||
fn arc_midpoint(p0: [f64; 2], p1: [f64; 2], bulge: f64) -> [f64; 2] {
|
fn arc_midpoint(p0: [f64; 2], p1: [f64; 2], bulge: f64) -> [f64; 2] {
|
||||||
match crate::entities::common::BulgeArc::from_bulge(p0, p1, bulge) {
|
match crate::entities::common::BulgeArc::from_bulge(p0, p1, bulge) {
|
||||||
@@ -504,8 +528,7 @@ fn band_verts(pline: &LwPolyline) -> Vec<([f64; 2], f64, f64, f64)> {
|
|||||||
.vertices
|
.vertices
|
||||||
.iter()
|
.iter()
|
||||||
.map(|v| {
|
.map(|v| {
|
||||||
let sw = effective_width(v.start_width, c);
|
let (sw, ew) = vertex_segment_widths(v, c);
|
||||||
let ew = effective_width(v.end_width, c);
|
|
||||||
([v.location.x, v.location.y], v.bulge, sw, ew)
|
([v.location.x, v.location.y], v.bulge, sw, ew)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -899,11 +922,8 @@ fn properties(pline: &LwPolyline) -> Vec<PropSection> {
|
|||||||
let v = pline.vertices.get(vi);
|
let v = pline.vertices.get(vi);
|
||||||
let vx = v.map_or(0.0, |v| v.location.x);
|
let vx = v.map_or(0.0, |v| v.location.x);
|
||||||
let vy = v.map_or(0.0, |v| v.location.y);
|
let vy = v.map_or(0.0, |v| v.location.y);
|
||||||
let start_w = v.map_or(0.0, |v| {
|
let (start_w, end_w) = v.map_or((0.0, 0.0), |v| {
|
||||||
effective_width(v.start_width, pline.constant_width)
|
vertex_segment_widths(v, pline.constant_width)
|
||||||
});
|
|
||||||
let end_w = v.map_or(0.0, |v| {
|
|
||||||
effective_width(v.end_width, pline.constant_width)
|
|
||||||
});
|
});
|
||||||
let mp = <LwPolyline as crate::entities::traits::MassPropsCalc>::mass_props(pline);
|
let mp = <LwPolyline as crate::entities::traits::MassPropsCalc>::mass_props(pline);
|
||||||
let cloud_arc_length = revision_cloud_arc_length(pline);
|
let cloud_arc_length = revision_cloud_arc_length(pline);
|
||||||
@@ -937,6 +957,14 @@ fn properties(pline: &LwPolyline) -> Vec<PropSection> {
|
|||||||
arc_length,
|
arc_length,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let global_width_prop = match lwpolyline_global_width(pline) {
|
||||||
|
Some(gw) => edit(t!("Global width").as_ref(), "global_width", gw),
|
||||||
|
None => Property {
|
||||||
|
label: t!("Global width").into_owned(),
|
||||||
|
field: "global_width",
|
||||||
|
value: PropValue::EditText(VARIES_LABEL.to_string()),
|
||||||
|
},
|
||||||
|
};
|
||||||
let mut geometry_props = vec![
|
let mut geometry_props = vec![
|
||||||
stepper(t!("Current Vertex").as_ref(), "current_vertex", vertex_label),
|
stepper(t!("Current Vertex").as_ref(), "current_vertex", vertex_label),
|
||||||
edit(t!("Vertex X").as_ref(), "vertex_x", vx),
|
edit(t!("Vertex X").as_ref(), "vertex_x", vx),
|
||||||
@@ -948,7 +976,7 @@ fn properties(pline: &LwPolyline) -> Vec<PropSection> {
|
|||||||
geometry_props.push(edit(t!("End segment width").as_ref(), "end_width", end_w));
|
geometry_props.push(edit(t!("End segment width").as_ref(), "end_width", end_w));
|
||||||
}
|
}
|
||||||
geometry_props.extend([
|
geometry_props.extend([
|
||||||
edit(t!("Global width").as_ref(), "global_width", pline.constant_width),
|
global_width_prop,
|
||||||
edit(t!("Elevation").as_ref(), "elevation", pline.elevation),
|
edit(t!("Elevation").as_ref(), "elevation", pline.elevation),
|
||||||
ro(t!("Area").as_ref(), "area", format_area(mp.area)),
|
ro(t!("Area").as_ref(), "area", format_area(mp.area)),
|
||||||
ro(t!("Length").as_ref(), "length", format_length(mp.perimeter)),
|
ro(t!("Length").as_ref(), "length", format_length(mp.perimeter)),
|
||||||
@@ -1015,7 +1043,13 @@ fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) {
|
|||||||
};
|
};
|
||||||
match field {
|
match field {
|
||||||
"elevation" => pline.elevation = v,
|
"elevation" => pline.elevation = v,
|
||||||
"global_width" if v.is_finite() && v >= 0.0 => pline.constant_width = v,
|
"global_width" if v.is_finite() && v >= 0.0 => {
|
||||||
|
pline.constant_width = v;
|
||||||
|
for vtx in &mut pline.vertices {
|
||||||
|
vtx.start_width = v;
|
||||||
|
vtx.end_width = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
"vertex_x" => {
|
"vertex_x" => {
|
||||||
if let Some(vtx) = pline.vertices.get_mut(vi) {
|
if let Some(vtx) = pline.vertices.get_mut(vi) {
|
||||||
vtx.location.x = v;
|
vtx.location.x = v;
|
||||||
@@ -1027,11 +1061,31 @@ fn apply_geom_prop(pline: &mut LwPolyline, field: &str, value: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"start_width" if v.is_finite() && v >= 0.0 => {
|
"start_width" if v.is_finite() && v >= 0.0 => {
|
||||||
|
if pline.constant_width > WIDTH_EPSILON {
|
||||||
|
let c = pline.constant_width;
|
||||||
|
for vtx in &mut pline.vertices {
|
||||||
|
if vtx.start_width <= WIDTH_EPSILON && vtx.end_width <= WIDTH_EPSILON {
|
||||||
|
vtx.start_width = c;
|
||||||
|
vtx.end_width = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pline.constant_width = 0.0;
|
||||||
|
}
|
||||||
if let Some(vtx) = pline.vertices.get_mut(vi) {
|
if let Some(vtx) = pline.vertices.get_mut(vi) {
|
||||||
vtx.start_width = v;
|
vtx.start_width = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"end_width" if v.is_finite() && v >= 0.0 => {
|
"end_width" if v.is_finite() && v >= 0.0 => {
|
||||||
|
if pline.constant_width > WIDTH_EPSILON {
|
||||||
|
let c = pline.constant_width;
|
||||||
|
for vtx in &mut pline.vertices {
|
||||||
|
if vtx.start_width <= WIDTH_EPSILON && vtx.end_width <= WIDTH_EPSILON {
|
||||||
|
vtx.start_width = c;
|
||||||
|
vtx.end_width = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pline.constant_width = 0.0;
|
||||||
|
}
|
||||||
if let Some(vtx) = pline.vertices.get_mut(vi) {
|
if let Some(vtx) = pline.vertices.get_mut(vi) {
|
||||||
vtx.end_width = v;
|
vtx.end_width = v;
|
||||||
}
|
}
|
||||||
@@ -1252,8 +1306,8 @@ impl crate::entities::traits::Grippable for LwPolyline {
|
|||||||
new_v.location.x = midpoint[0];
|
new_v.location.x = midpoint[0];
|
||||||
new_v.location.y = midpoint[1];
|
new_v.location.y = midpoint[1];
|
||||||
new_v.vertex_id = 0;
|
new_v.vertex_id = 0;
|
||||||
let effective_start = effective_width(v0.start_width, self.constant_width);
|
let (effective_start, effective_end) =
|
||||||
let effective_end = effective_width(v0.end_width, self.constant_width);
|
vertex_segment_widths(&v0, self.constant_width);
|
||||||
let middle_width = (effective_start + effective_end) * 0.5;
|
let middle_width = (effective_start + effective_end) * 0.5;
|
||||||
self.vertices[i0].end_width = middle_width;
|
self.vertices[i0].end_width = middle_width;
|
||||||
new_v.start_width = middle_width;
|
new_v.start_width = middle_width;
|
||||||
@@ -1325,8 +1379,9 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> ([f64; 2], Vec<
|
|||||||
for i in 0..seg_count {
|
for i in 0..seg_count {
|
||||||
let v0 = &verts[i];
|
let v0 = &verts[i];
|
||||||
let v1 = &verts[(i + 1) % n];
|
let v1 = &verts[(i + 1) % n];
|
||||||
let hw0 = effective_width(v0.start_width, pl.constant_width) as f32 * 0.5;
|
let (sw, ew) = vertex_segment_widths(v0, pl.constant_width);
|
||||||
let hw1 = effective_width(v0.end_width, pl.constant_width) as f32 * 0.5;
|
let hw0 = sw as f32 * 0.5;
|
||||||
|
let hw1 = ew as f32 * 0.5;
|
||||||
if hw0 < 1e-6 && hw1 < 1e-6 {
|
if hw0 < 1e-6 && hw1 < 1e-6 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1381,3 +1436,88 @@ impl crate::entities::traits::MassPropsCalc for acadrust::entities::LwPolyline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::entities::traits::PropertyEditable;
|
||||||
|
use acadrust::entities::{LwPolyline, LwVertex};
|
||||||
|
use acadrust::Vector2;
|
||||||
|
|
||||||
|
fn make_test_lwpolyline(count: usize, constant_width: f64) -> LwPolyline {
|
||||||
|
let mut pl = LwPolyline::default();
|
||||||
|
pl.constant_width = constant_width;
|
||||||
|
for i in 0..count {
|
||||||
|
pl.vertices.push(LwVertex::new(Vector2::new(i as f64 * 10.0, 0.0)));
|
||||||
|
}
|
||||||
|
pl
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lwpolyline_uniform_width_shows_global_width() {
|
||||||
|
let pl = make_test_lwpolyline(3, 5.0);
|
||||||
|
assert_eq!(lwpolyline_global_width(&pl), Some(5.0));
|
||||||
|
|
||||||
|
let props = pl.geometry_properties(&[]);
|
||||||
|
let geom_props = &props[0].props;
|
||||||
|
let gw = geom_props.iter().find(|p| p.field == "global_width").unwrap();
|
||||||
|
match &gw.value {
|
||||||
|
PropValue::EditText(val) => assert_eq!(parse_f64(val), Some(5.0)),
|
||||||
|
_ => panic!("expected EditText"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lwpolyline_varying_vertex_width_shows_varies() {
|
||||||
|
let mut pl = make_test_lwpolyline(3, 5.0);
|
||||||
|
pl.vertices[0].start_width = 2.0;
|
||||||
|
pl.vertices[0].end_width = 3.0;
|
||||||
|
|
||||||
|
assert_eq!(lwpolyline_global_width(&pl), None);
|
||||||
|
|
||||||
|
let props = pl.geometry_properties(&[]);
|
||||||
|
let geom_props = &props[0].props;
|
||||||
|
let gw = geom_props.iter().find(|p| p.field == "global_width").unwrap();
|
||||||
|
match &gw.value {
|
||||||
|
PropValue::EditText(val) => assert_eq!(val, VARIES_LABEL),
|
||||||
|
_ => panic!("expected EditText with VARIES_LABEL"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lwpolyline_modifying_global_width_sets_all_vertex_widths() {
|
||||||
|
let mut pl = make_test_lwpolyline(3, 5.0);
|
||||||
|
pl.vertices[0].start_width = 2.0;
|
||||||
|
pl.vertices[0].end_width = 3.0;
|
||||||
|
|
||||||
|
pl.apply_geom_prop("global_width", "10.0");
|
||||||
|
|
||||||
|
assert_eq!(pl.constant_width, 10.0);
|
||||||
|
for v in &pl.vertices {
|
||||||
|
assert_eq!(v.start_width, 10.0);
|
||||||
|
assert_eq!(v.end_width, 10.0);
|
||||||
|
}
|
||||||
|
assert_eq!(lwpolyline_global_width(&pl), Some(10.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lwpolyline_modifying_segment_width_materializes_constant_width() {
|
||||||
|
let mut pl = make_test_lwpolyline(3, 5.0);
|
||||||
|
// Initially constant_width is 5.0, vertices start/end are 0.0
|
||||||
|
assert_eq!(pl.vertices[0].start_width, 0.0);
|
||||||
|
|
||||||
|
// Edit start width of current vertex (vertex 0) to 2.0
|
||||||
|
pl.apply_geom_prop("start_width", "2.0");
|
||||||
|
|
||||||
|
// Vertex 0 start width should be 2.0, end width materialized to 5.0
|
||||||
|
assert_eq!(pl.vertices[0].start_width, 2.0);
|
||||||
|
assert_eq!(pl.vertices[0].end_width, 5.0);
|
||||||
|
|
||||||
|
// Vertex 1 and 2 should have materialized 5.0 instead of collapsing to 0.0
|
||||||
|
assert_eq!(pl.vertices[1].start_width, 5.0);
|
||||||
|
assert_eq!(pl.vertices[1].end_width, 5.0);
|
||||||
|
|
||||||
|
// Global width must now be VARIES
|
||||||
|
assert_eq!(lwpolyline_global_width(&pl), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ use crate::t;
|
|||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::entities::common::{
|
use crate::entities::common::{
|
||||||
edit_prop as edit, format_length, parse_f64, ro_prop as ro, square_grip,
|
edit_prop as edit, format_length, parse_f64, ro_prop as ro, square_grip,
|
||||||
stepper_prop as stepper,
|
stepper_prop as stepper, VARIES_LABEL,
|
||||||
};
|
};
|
||||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
||||||
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
|
||||||
@@ -609,6 +609,51 @@ impl Grippable for Polyline2D {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn polyline2d_vertex_segment_widths(
|
||||||
|
vertex: &acadrust::entities::Vertex2D,
|
||||||
|
default_start: f64,
|
||||||
|
default_end: f64,
|
||||||
|
) -> (f64, f64) {
|
||||||
|
if vertex.start_width > 1e-9 || vertex.end_width > 1e-9 {
|
||||||
|
(vertex.start_width, vertex.end_width)
|
||||||
|
} else {
|
||||||
|
(default_start, default_end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn polyline2d_global_width(pline: &Polyline2D) -> Option<f64> {
|
||||||
|
let filtered = drawn_vertices2d(pline);
|
||||||
|
let verts: &[acadrust::entities::Vertex2D] =
|
||||||
|
filtered.as_deref().unwrap_or(&pline.vertices);
|
||||||
|
let count = verts.len();
|
||||||
|
let seg_count = if pline.is_closed() {
|
||||||
|
count
|
||||||
|
} else {
|
||||||
|
count.saturating_sub(1)
|
||||||
|
};
|
||||||
|
if seg_count == 0 {
|
||||||
|
if (pline.start_width - pline.end_width).abs() < 1e-6 {
|
||||||
|
return Some(pline.start_width);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (w0_start, w0_end) =
|
||||||
|
polyline2d_vertex_segment_widths(&verts[0], pline.start_width, pline.end_width);
|
||||||
|
if (w0_start - w0_end).abs() > 1e-6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let w0 = w0_start;
|
||||||
|
for i in 1..seg_count {
|
||||||
|
let (sw, ew) =
|
||||||
|
polyline2d_vertex_segment_widths(&verts[i], pline.start_width, pline.end_width);
|
||||||
|
if (sw - ew).abs() > 1e-6 || (sw - w0).abs() > 1e-6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(w0)
|
||||||
|
}
|
||||||
|
|
||||||
impl PropertyEditable for Polyline2D {
|
impl PropertyEditable for Polyline2D {
|
||||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||||
let n = self.vertices.len();
|
let n = self.vertices.len();
|
||||||
@@ -631,14 +676,24 @@ impl PropertyEditable for Polyline2D {
|
|||||||
let v = self.vertices.get(vi);
|
let v = self.vertices.get(vi);
|
||||||
let vertex_x = v.map(|v| v.location.x).unwrap_or_default();
|
let vertex_x = v.map(|v| v.location.x).unwrap_or_default();
|
||||||
let vertex_y = v.map(|v| v.location.y).unwrap_or_default();
|
let vertex_y = v.map(|v| v.location.y).unwrap_or_default();
|
||||||
let seg_start_w = v.map(|v| v.start_width).unwrap_or_default();
|
let (seg_start_w, seg_end_w) = v.map_or((0.0, 0.0), |v| {
|
||||||
let seg_end_w = v.map(|v| v.end_width).unwrap_or_default();
|
polyline2d_vertex_segment_widths(v, self.start_width, self.end_width)
|
||||||
|
});
|
||||||
let vertex_label = if n == 0 {
|
let vertex_label = if n == 0 {
|
||||||
"—".to_string()
|
"—".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("{} / {}", vi + 1, n)
|
format!("{} / {}", vi + 1, n)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let global_width_prop = match polyline2d_global_width(self) {
|
||||||
|
Some(gw) => edit(t!("Global width").as_ref(), "pl2_start_w", gw),
|
||||||
|
None => Property {
|
||||||
|
label: t!("Global width").into_owned(),
|
||||||
|
field: "pl2_start_w",
|
||||||
|
value: PropValue::EditText(VARIES_LABEL.to_string()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
vec![
|
vec![
|
||||||
PropSection {
|
PropSection {
|
||||||
title: t!("Geometry").into_owned(),
|
title: t!("Geometry").into_owned(),
|
||||||
@@ -648,7 +703,7 @@ impl PropertyEditable for Polyline2D {
|
|||||||
edit(t!("Vertex Y").as_ref(), "pl2_vertex_y", vertex_y),
|
edit(t!("Vertex Y").as_ref(), "pl2_vertex_y", vertex_y),
|
||||||
edit(t!("Start segment width").as_ref(), "pl2_seg_start_w", seg_start_w),
|
edit(t!("Start segment width").as_ref(), "pl2_seg_start_w", seg_start_w),
|
||||||
edit(t!("End segment width").as_ref(), "pl2_seg_end_w", seg_end_w),
|
edit(t!("End segment width").as_ref(), "pl2_seg_end_w", seg_end_w),
|
||||||
edit(t!("Global width").as_ref(), "pl2_start_w", self.start_width),
|
global_width_prop,
|
||||||
edit(t!("Elevation").as_ref(), "pl2_elevation", self.elevation),
|
edit(t!("Elevation").as_ref(), "pl2_elevation", self.elevation),
|
||||||
ro(t!("Area").as_ref(), "pl2_area", format!("{area:.4}")),
|
ro(t!("Area").as_ref(), "pl2_area", format!("{area:.4}")),
|
||||||
ro(t!("Length").as_ref(), "pl2_length", format!("{length:.4}")),
|
ro(t!("Length").as_ref(), "pl2_length", format!("{length:.4}")),
|
||||||
@@ -706,9 +761,13 @@ impl PropertyEditable for Polyline2D {
|
|||||||
}
|
}
|
||||||
"pl2_start_w" => {
|
"pl2_start_w" => {
|
||||||
if let Some(v) = parse_f64(value) {
|
if let Some(v) = parse_f64(value) {
|
||||||
if v >= 0.0 {
|
if v.is_finite() && v >= 0.0 {
|
||||||
self.start_width = v;
|
self.start_width = v;
|
||||||
self.end_width = v;
|
self.end_width = v;
|
||||||
|
for vtx in &mut self.vertices {
|
||||||
|
vtx.start_width = v;
|
||||||
|
vtx.end_width = v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -723,16 +782,44 @@ impl PropertyEditable for Polyline2D {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"pl2_seg_start_w" => {
|
"pl2_seg_start_w" => {
|
||||||
if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.get_mut(vi)) {
|
if let Some(v) = parse_f64(value) {
|
||||||
if v >= 0.0 {
|
if v.is_finite() && v >= 0.0 {
|
||||||
vert.start_width = v;
|
if self.start_width > 1e-9 || self.end_width > 1e-9 {
|
||||||
|
let def_s = self.start_width;
|
||||||
|
let def_e = self.end_width;
|
||||||
|
for vtx in &mut self.vertices {
|
||||||
|
if vtx.start_width <= 1e-9 && vtx.end_width <= 1e-9 {
|
||||||
|
vtx.start_width = def_s;
|
||||||
|
vtx.end_width = def_e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.start_width = 0.0;
|
||||||
|
self.end_width = 0.0;
|
||||||
|
}
|
||||||
|
if let Some(vert) = self.vertices.get_mut(vi) {
|
||||||
|
vert.start_width = v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"pl2_seg_end_w" => {
|
"pl2_seg_end_w" => {
|
||||||
if let (Some(v), Some(vert)) = (parse_f64(value), self.vertices.get_mut(vi)) {
|
if let Some(v) = parse_f64(value) {
|
||||||
if v >= 0.0 {
|
if v.is_finite() && v >= 0.0 {
|
||||||
vert.end_width = v;
|
if self.start_width > 1e-9 || self.end_width > 1e-9 {
|
||||||
|
let def_s = self.start_width;
|
||||||
|
let def_e = self.end_width;
|
||||||
|
for vtx in &mut self.vertices {
|
||||||
|
if vtx.start_width <= 1e-9 && vtx.end_width <= 1e-9 {
|
||||||
|
vtx.start_width = def_s;
|
||||||
|
vtx.end_width = def_e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.start_width = 0.0;
|
||||||
|
self.end_width = 0.0;
|
||||||
|
}
|
||||||
|
if let Some(vert) = self.vertices.get_mut(vi) {
|
||||||
|
vert.end_width = v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1331,3 +1418,84 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::Polyline2D) -> ([f64; 2], Vec<
|
|||||||
}
|
}
|
||||||
(origin, out)
|
(origin, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::entities::traits::PropertyEditable;
|
||||||
|
use acadrust::entities::{Polyline2D, Vertex2D};
|
||||||
|
use acadrust::Vector3;
|
||||||
|
|
||||||
|
fn make_test_polyline2d(count: usize, start_w: f64, end_w: f64) -> Polyline2D {
|
||||||
|
let mut pl = Polyline2D::default();
|
||||||
|
pl.start_width = start_w;
|
||||||
|
pl.end_width = end_w;
|
||||||
|
for i in 0..count {
|
||||||
|
pl.vertices.push(Vertex2D::new(Vector3::new(i as f64 * 10.0, 0.0, 0.0)));
|
||||||
|
}
|
||||||
|
pl
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_polyline2d_uniform_width_shows_global_width() {
|
||||||
|
let pl = make_test_polyline2d(3, 4.0, 4.0);
|
||||||
|
assert_eq!(polyline2d_global_width(&pl), Some(4.0));
|
||||||
|
|
||||||
|
let props = pl.geometry_properties(&[]);
|
||||||
|
let geom_props = &props[0].props;
|
||||||
|
let gw = geom_props.iter().find(|p| p.field == "pl2_start_w").unwrap();
|
||||||
|
match &gw.value {
|
||||||
|
PropValue::EditText(val) => assert_eq!(parse_f64(val), Some(4.0)),
|
||||||
|
_ => panic!("expected EditText"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_polyline2d_varying_vertex_width_shows_varies() {
|
||||||
|
let mut pl = make_test_polyline2d(3, 4.0, 4.0);
|
||||||
|
pl.vertices[0].start_width = 1.0;
|
||||||
|
pl.vertices[0].end_width = 2.0;
|
||||||
|
|
||||||
|
assert_eq!(polyline2d_global_width(&pl), None);
|
||||||
|
|
||||||
|
let props = pl.geometry_properties(&[]);
|
||||||
|
let geom_props = &props[0].props;
|
||||||
|
let gw = geom_props.iter().find(|p| p.field == "pl2_start_w").unwrap();
|
||||||
|
match &gw.value {
|
||||||
|
PropValue::EditText(val) => assert_eq!(val, VARIES_LABEL),
|
||||||
|
_ => panic!("expected EditText with VARIES_LABEL"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_polyline2d_modifying_global_width_sets_all_vertex_widths() {
|
||||||
|
let mut pl = make_test_polyline2d(3, 4.0, 4.0);
|
||||||
|
pl.vertices[0].start_width = 1.0;
|
||||||
|
pl.vertices[0].end_width = 2.0;
|
||||||
|
|
||||||
|
pl.apply_geom_prop("pl2_start_w", "8.0");
|
||||||
|
|
||||||
|
assert_eq!(pl.start_width, 8.0);
|
||||||
|
assert_eq!(pl.end_width, 8.0);
|
||||||
|
for v in &pl.vertices {
|
||||||
|
assert_eq!(v.start_width, 8.0);
|
||||||
|
assert_eq!(v.end_width, 8.0);
|
||||||
|
}
|
||||||
|
assert_eq!(polyline2d_global_width(&pl), Some(8.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_polyline2d_modifying_segment_width_materializes_default_width() {
|
||||||
|
let mut pl = make_test_polyline2d(3, 4.0, 4.0);
|
||||||
|
assert_eq!(pl.vertices[0].start_width, 0.0);
|
||||||
|
|
||||||
|
pl.apply_geom_prop("pl2_seg_start_w", "1.5");
|
||||||
|
|
||||||
|
assert_eq!(pl.vertices[0].start_width, 1.5);
|
||||||
|
assert_eq!(pl.vertices[0].end_width, 4.0);
|
||||||
|
assert_eq!(pl.vertices[1].start_width, 4.0);
|
||||||
|
assert_eq!(pl.vertices[1].end_width, 4.0);
|
||||||
|
|
||||||
|
assert_eq!(polyline2d_global_width(&pl), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
// Shared grips and properties for modeler entities.
|
// Shared grips and properties for modeler entities.
|
||||||
|
|
||||||
use acadrust::entities::{Body, Region, Solid3D, Surface};
|
use acadrust::entities::{Body, Region, Solid3D, Surface};
|
||||||
|
use acadrust::xdata::{ExtendedDataRecord, XDataValue};
|
||||||
use cadkernel::space::polygon;
|
use cadkernel::space::polygon;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
@@ -49,6 +50,71 @@ fn yes_no(value: bool) -> &'static str {
|
|||||||
if value { "Yes" } else { "No" }
|
if value { "Yes" } else { "No" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) const SURFACE_PROPERTIES_APP: &str = "OCS_SURFACE_PROPERTIES";
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) struct SurfacePropertyState {
|
||||||
|
pub isolines: bool,
|
||||||
|
pub maintain_associativity: bool,
|
||||||
|
pub show_associativity: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn surface_property_state(surface: &Surface) -> SurfacePropertyState {
|
||||||
|
let defaults = SurfacePropertyState {
|
||||||
|
isolines: true,
|
||||||
|
maintain_associativity: surface.history_handle.is_some(),
|
||||||
|
show_associativity: false,
|
||||||
|
};
|
||||||
|
let Some(record) = surface
|
||||||
|
.common
|
||||||
|
.extended_data
|
||||||
|
.get_record(SURFACE_PROPERTIES_APP)
|
||||||
|
else {
|
||||||
|
return defaults;
|
||||||
|
};
|
||||||
|
let mut values = record.values.iter().filter_map(|value| match value {
|
||||||
|
XDataValue::Integer16(value) => Some(*value),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
let Some(version) = values.next() else {
|
||||||
|
return defaults;
|
||||||
|
};
|
||||||
|
if version != 1 {
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
SurfacePropertyState {
|
||||||
|
isolines: values.next().map_or(defaults.isolines, |value| value == 0),
|
||||||
|
maintain_associativity: values
|
||||||
|
.next()
|
||||||
|
.map_or(defaults.maintain_associativity, |value| value != 0),
|
||||||
|
show_associativity: values
|
||||||
|
.next()
|
||||||
|
.map_or(defaults.show_associativity, |value| value != 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn surface_property_xdata_values(state: SurfacePropertyState) -> Vec<XDataValue> {
|
||||||
|
vec![
|
||||||
|
XDataValue::Integer16(1),
|
||||||
|
XDataValue::Integer16(if state.isolines { 0 } else { 1 }),
|
||||||
|
XDataValue::Integer16(state.maintain_associativity as i16),
|
||||||
|
XDataValue::Integer16(state.show_associativity as i16),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_surface_property_state(surface: &mut Surface, state: SurfacePropertyState) {
|
||||||
|
let mut record = ExtendedDataRecord::new(SURFACE_PROPERTIES_APP);
|
||||||
|
record.values = surface_property_xdata_values(state);
|
||||||
|
surface.common.extended_data.upsert_record(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn surface_isoline_counts(surface: &Surface) -> [usize; 2] {
|
||||||
|
[
|
||||||
|
surface.u_isolines.max(0) as usize,
|
||||||
|
surface.v_isolines.max(0) as usize,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_text(handle: Option<acadrust::Handle>) -> String {
|
fn handle_text(handle: Option<acadrust::Handle>) -> String {
|
||||||
handle
|
handle
|
||||||
.filter(|handle| handle.is_valid())
|
.filter(|handle| handle.is_valid())
|
||||||
@@ -700,6 +766,21 @@ impl PropertyEditable for Surface {
|
|||||||
|
|
||||||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||||
match field {
|
match field {
|
||||||
|
"srf_wireframe_type" => {
|
||||||
|
let mut state = surface_property_state(self);
|
||||||
|
state.isolines = !value.eq_ignore_ascii_case("Isoparms");
|
||||||
|
write_surface_property_state(self, state);
|
||||||
|
}
|
||||||
|
"srf_maintain_associativity" => {
|
||||||
|
let mut state = surface_property_state(self);
|
||||||
|
state.maintain_associativity = value.eq_ignore_ascii_case("Yes");
|
||||||
|
write_surface_property_state(self, state);
|
||||||
|
}
|
||||||
|
"srf_show_associativity" => {
|
||||||
|
let mut state = surface_property_state(self);
|
||||||
|
state.show_associativity = value.eq_ignore_ascii_case("Yes");
|
||||||
|
write_surface_property_state(self, state);
|
||||||
|
}
|
||||||
"srf_u_isolines" => {
|
"srf_u_isolines" => {
|
||||||
if let Some(value) = parse_f64(value) {
|
if let Some(value) = parse_f64(value) {
|
||||||
self.u_isolines = (value.round() as i16).max(0);
|
self.u_isolines = (value.round() as i16).max(0);
|
||||||
@@ -822,7 +903,8 @@ pub fn tessellate_volume(
|
|||||||
color,
|
color,
|
||||||
facet_res,
|
facet_res,
|
||||||
chordal_deflection,
|
chordal_deflection,
|
||||||
isolines,
|
surface_isoline_counts(s),
|
||||||
|
surface_property_state(s).isolines,
|
||||||
),
|
),
|
||||||
EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) => {
|
EntityType::Mesh(_) | EntityType::PolygonMesh(_) | EntityType::PolyfaceMesh(_) => {
|
||||||
crate::entities::mesh::tessellate_shaded_mesh(e, color)
|
crate::entities::mesh::tessellate_shaded_mesh(e, color)
|
||||||
|
|||||||
@@ -4120,6 +4120,71 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
|||||||
"Pick box size" => Some(("common", "pick-box-size")),
|
"Pick box size" => Some(("common", "pick-box-size")),
|
||||||
"Controls both the visible selection box and the click aperture." => Some(("common", "controls-both-the-visible-selection-box-and-the-click-aperture")),
|
"Controls both the visible selection box and the click aperture." => Some(("common", "controls-both-the-visible-selection-box-and-the-click-aperture")),
|
||||||
"Visual Effect Settings" => Some(("common", "visual-effect-settings")),
|
"Visual Effect Settings" => Some(("common", "visual-effect-settings")),
|
||||||
|
"Applications" => Some(("common", "applications")),
|
||||||
|
"Installed plugins and their sources" => Some(("common", "installed-plugins-and-their-sources")),
|
||||||
|
"Plugins…" => Some(("common", "plugins-2")),
|
||||||
|
"Keyboard shortcuts" => Some(("common", "keyboard-shortcuts-2")),
|
||||||
|
"Keyboard Shortcuts…" => Some(("common", "keyboard-shortcuts-3")),
|
||||||
|
"Command aliases" => Some(("common", "command-aliases-2")),
|
||||||
|
"Command Aliases…" => Some(("common", "command-aliases-3")),
|
||||||
|
"File Safety" => Some(("common", "file-safety")),
|
||||||
|
"Automatic save" => Some(("common", "automatic-save")),
|
||||||
|
"Minutes between recovery saves to a .sv$ file; 0 turns it off (SAVETIME)." => Some(("common", "minutes-between-recovery-saves-to-a-sv-file-0-turns-it-off-savetime")),
|
||||||
|
"Keep a .bak copy when overwriting a drawing (ISAVEBAK)" => Some(("common", "keep-a-bak-copy-when-overwriting-a-drawing-isavebak")),
|
||||||
|
"Plotting" => Some(("common", "plotting")),
|
||||||
|
"Plot device, paper, scale and plot styles" => Some(("common", "plot-device-paper-scale-and-plot-styles")),
|
||||||
|
"Plot and Page Setup…" => Some(("common", "plot-and-page-setup")),
|
||||||
|
"Fill TrueType glyphs (TEXTFILL)" => Some(("common", "fill-truetype-glyphs-textfill")),
|
||||||
|
"Command Line" => Some(("common", "command-line")),
|
||||||
|
"Prompt lines" => Some(("common", "prompt-lines")),
|
||||||
|
"Temporary prompt lines shown above the command window (CLIPROMPTLINES)." => Some(("common", "temporary-prompt-lines-shown-above-the-command-window-clipromptlines")),
|
||||||
|
"History fade time" => Some(("common", "history-fade-time")),
|
||||||
|
"How long overlay history lines stay visible; 0 skips them (COMMANDLINEFADETIME)." => Some(("common", "how-long-overlay-history-lines-stay-visible-0-skips-them-commandlinefadetime")),
|
||||||
|
"Clicking overlapping objects opens a picker" => Some(("common", "clicking-overlapping-objects-opens-a-picker")),
|
||||||
|
"Block Edit" => Some(("common", "block-edit")),
|
||||||
|
"Double-click to edit block in-place (REFEDIT)" => Some(("common", "double-click-to-edit-block-in-place-refedit")),
|
||||||
|
"Double-click attributed blocks to edit attributes (ATTEDIT)" => Some(("common", "double-click-attributed-blocks-to-edit-attributes-attedit")),
|
||||||
|
"User Preferences" => Some(("common", "user-preferences")),
|
||||||
|
"Zoom" => Some(("common", "zoom")),
|
||||||
|
"Reverse mouse wheel zoom (ZOOMWHEEL)" => Some(("common", "reverse-mouse-wheel-zoom-zoomwheel")),
|
||||||
|
"Zoom factor" => Some(("common", "zoom-factor")),
|
||||||
|
"How far one wheel notch zooms (ZOOMFACTOR)." => Some(("common", "how-far-one-wheel-notch-zooms-zoomfactor")),
|
||||||
|
"Text and Dimensions" => Some(("common", "text-and-dimensions")),
|
||||||
|
"TEXTEDIT edits one object and ends (TEXTEDITMODE)" => Some(("common", "textedit-edits-one-object-and-ends-texteditmode")),
|
||||||
|
"Continued dimensions inherit the base dimension's layer and style (DIMCONTINUEMODE)" => Some(("common", "continued-dimensions-inherit-the-base-dimension-s-layer-and-style-dimcontinuemode")),
|
||||||
|
"QDIM origin priority" => Some(("common", "qdim-origin-priority")),
|
||||||
|
"Which points QDIM measures from. Also settable inside the command." => Some(("common", "which-points-qdim-measures-from-also-settable-inside-the-command")),
|
||||||
|
"Add scales automatically" => Some(("common", "add-scales-automatically")),
|
||||||
|
"Which annotative objects pick up a newly set annotation scale (ANNOAUTOSCALE)." => Some(("common", "which-annotative-objects-pick-up-a-newly-set-annotation-scale-annoautoscale")),
|
||||||
|
"Length, angle and insertion units" => Some(("common", "length-angle-and-insertion-units")),
|
||||||
|
"Drawing Units…" => Some(("common", "drawing-units-2")),
|
||||||
|
"Drafting" => Some(("common", "drafting")),
|
||||||
|
"Drafting rotation" => Some(("common", "drafting-rotation")),
|
||||||
|
"Rotates the crosshair and the snap grid in the active UCS (SNAPANG)." => Some(("common", "rotates-the-crosshair-and-the-snap-grid-in-the-active-ucs-snapang")),
|
||||||
|
"Polar tracking increment" => Some(("common", "polar-tracking-increment")),
|
||||||
|
"Snap and Grid" => Some(("common", "snap-and-grid")),
|
||||||
|
"Snap modes, grid spacing and object snap" => Some(("common", "snap-modes-grid-spacing-and-object-snap")),
|
||||||
|
"Drafting Settings…" => Some(("common", "drafting-settings-2")),
|
||||||
|
"3D Modeling" => Some(("common", "modeling-3d")),
|
||||||
|
"Display Tools" => Some(("common", "display-tools")),
|
||||||
|
"Show the navigation cube (NAVVCUBE)" => Some(("common", "show-the-navigation-cube-navvcube")),
|
||||||
|
"Show the UCS icon (UCSICON)" => Some(("common", "show-the-ucs-icon-ucsicon")),
|
||||||
|
"Place the UCS icon at the origin (UCSICON ORigin)" => Some(("common", "place-the-ucs-icon-at-the-origin-ucsicon-origin")),
|
||||||
|
"Display Resolution" => Some(("common", "display-resolution")),
|
||||||
|
"applies to the current drawing" => Some(("common", "applies-to-the-current-drawing")),
|
||||||
|
"Isolines per surface" => Some(("common", "isolines-per-surface")),
|
||||||
|
"Show silhouette edges on solids (DISPSILH)" => Some(("common", "show-silhouette-edges-on-solids-dispsilh")),
|
||||||
|
"Surface density U" => Some(("common", "surface-density-u")),
|
||||||
|
"Surface density V" => Some(("common", "surface-density-v")),
|
||||||
|
"Surface type" => Some(("common", "surface-type")),
|
||||||
|
"Record the history of composite solids (SOLIDHIST)" => Some(("common", "record-the-history-of-composite-solids-solidhist")),
|
||||||
|
"Show solid history" => Some(("common", "show-solid-history")),
|
||||||
|
"Not available" => Some(("common", "not-available")),
|
||||||
|
"Open folder" => Some(("common", "open-folder")),
|
||||||
|
"Where the application keeps its own files. These locations are fixed." => Some(("common", "where-the-application-keeps-its-own-files-these-locations-are-fixed")),
|
||||||
|
"Configuration" => Some(("common", "configuration")),
|
||||||
|
"Plot styles" => Some(("common", "plot-styles")),
|
||||||
|
"Autosave files" => Some(("common", "autosave-files")),
|
||||||
"Selection Modes" => Some(("common", "selection-modes")),
|
"Selection Modes" => Some(("common", "selection-modes")),
|
||||||
"Use Shift to add to selection (PICKADD)" => Some(("common", "use-shift-to-add-to-selection-pickadd")),
|
"Use Shift to add to selection (PICKADD)" => Some(("common", "use-shift-to-add-to-selection-pickadd")),
|
||||||
"Press and drag draws a rectangle instead of a lasso (PICKDRAG)" => Some(("common", "press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag")),
|
"Press and drag draws a rectangle instead of a lasso (PICKDRAG)" => Some(("common", "press-and-drag-draws-a-rectangle-instead-of-a-lasso-pickdrag")),
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
// Produces a Ray entity; repeats until Enter/Esc.
|
// Produces a Ray entity; repeats until Enter/Esc.
|
||||||
// XLINE — infinite construction line: same two-click pattern, yields XLine.
|
// XLINE — infinite construction line: same two-click pattern, yields XLine.
|
||||||
|
|
||||||
|
use crate::t;
|
||||||
use acadrust::entities::{Ray as RayEnt, XLine as XLineEnt};
|
use acadrust::entities::{Ray as RayEnt, XLine as XLineEnt};
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::EntityType;
|
use acadrust::EntityType;
|
||||||
use crate::t;
|
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdResult};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
@@ -61,9 +61,7 @@ impl CadCommand for RayCommand {
|
|||||||
Vector3::new(base.x, base.y, base.z),
|
Vector3::new(base.x, base.y, base.z),
|
||||||
Vector3::new(dir_n.x, dir_n.y, dir_n.z),
|
Vector3::new(dir_n.x, dir_n.y, dir_n.z),
|
||||||
);
|
);
|
||||||
// Stay active: new base = same base (can keep clicking through points)
|
// Repeated through points share the original start point.
|
||||||
// Actually AutoCAD prompts for new start after each ray — reset base.
|
|
||||||
self.base = None;
|
|
||||||
CmdResult::CommitEntity(EntityType::Ray(ray))
|
CmdResult::CommitEntity(EntityType::Ray(ray))
|
||||||
} else {
|
} else {
|
||||||
self.base = Some(pt);
|
self.base = Some(pt);
|
||||||
@@ -167,7 +165,6 @@ impl CadCommand for XLineCommand {
|
|||||||
Vector3::new(base.x, base.y, base.z),
|
Vector3::new(base.x, base.y, base.z),
|
||||||
Vector3::new(dir_n.x, dir_n.y, dir_n.z),
|
Vector3::new(dir_n.x, dir_n.y, dir_n.z),
|
||||||
);
|
);
|
||||||
self.base = None;
|
|
||||||
CmdResult::CommitEntity(EntityType::XLine(xline))
|
CmdResult::CommitEntity(EntityType::XLine(xline))
|
||||||
} else {
|
} else {
|
||||||
self.base = Some(pt);
|
self.base = Some(pt);
|
||||||
@@ -229,7 +226,34 @@ impl CadCommand for XLineCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repeated_rays_and_xlines_keep_their_first_point() {
|
||||||
|
let base = DVec3::new(2.0, 3.0, 4.0);
|
||||||
|
let mut ray = RayCommand::new();
|
||||||
|
let mut xline = XLineCommand::new();
|
||||||
|
assert!(matches!(ray.on_point(base), CmdResult::NeedPoint));
|
||||||
|
assert!(matches!(xline.on_point(base), CmdResult::NeedPoint));
|
||||||
|
|
||||||
|
for through in [DVec3::new(3.0, 3.0, 4.0), DVec3::new(2.0, 5.0, 4.0)] {
|
||||||
|
let CmdResult::CommitEntity(EntityType::Ray(entity)) = ray.on_point(through) else {
|
||||||
|
panic!("ray was not committed");
|
||||||
|
};
|
||||||
|
assert_eq!(entity.base_point, Vector3::new(base.x, base.y, base.z));
|
||||||
|
|
||||||
|
let CmdResult::CommitEntity(EntityType::XLine(entity)) = xline.on_point(through) else {
|
||||||
|
panic!("construction line was not committed");
|
||||||
|
};
|
||||||
|
assert_eq!(entity.base_point, Vector3::new(base.x, base.y, base.z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
// ── Autocomplete registry ─────────────────────────────────
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["RAY"] }); // RayCommand
|
inventory::submit!(crate::command::CommandRegistration { names: &["RAY"] }); // RayCommand
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["CONSTRUCTIONLINE", "XLINE"] }); // XLineCommand
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["CONSTRUCTIONLINE", "XLINE"]
|
||||||
|
}); // XLineCommand
|
||||||
@@ -933,4 +933,6 @@ fn style_from_entity(entity: &EntityType) -> Option<CloudStyle> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["REVCLOUD"] });
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["REVCLOUD", "REVCLOUD_RECTANGULAR", "REVCLOUD_POLYGONAL", "REVCLOUD_FREEHAND"]
|
||||||
|
});
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
// Command: SPLINE (SPL)
|
// Command: SPLINE (SPL)
|
||||||
// Click to add fit points. Enter (≥2 pts) → commits EntityType::Spline.
|
// Click to add fit points. Enter (≥2 pts) → commits EntityType::Spline.
|
||||||
|
|
||||||
|
use crate::t;
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::{EntityType, Spline};
|
use acadrust::{EntityType, Spline};
|
||||||
use crate::t;
|
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use crate::command::{CadCommand, CmdResult};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
@@ -24,49 +24,87 @@ pub fn tool() -> ToolDef {
|
|||||||
|
|
||||||
pub struct SplineCommand {
|
pub struct SplineCommand {
|
||||||
pts: Vec<DVec3>,
|
pts: Vec<DVec3>,
|
||||||
|
control_vertices: bool,
|
||||||
|
choosing_method: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SplineCommand {
|
impl SplineCommand {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { pts: Vec::new() }
|
Self {
|
||||||
|
pts: Vec::new(),
|
||||||
|
control_vertices: false,
|
||||||
|
choosing_method: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn control_vertices() -> Self {
|
||||||
|
Self {
|
||||||
|
control_vertices: true,
|
||||||
|
..Self::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build(&self, closed: bool) -> Option<EntityType> {
|
fn build(&self, closed: bool) -> Option<EntityType> {
|
||||||
if self.pts.len() < 2 {
|
if self.pts.len() < 2 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let fit_points = self
|
Some(EntityType::Spline(make_spline(
|
||||||
.pts
|
&self.pts,
|
||||||
.iter()
|
closed,
|
||||||
.map(|p| Vector3::new(p.x, p.y, p.z))
|
self.control_vertices,
|
||||||
.collect();
|
)))
|
||||||
let mut spline = Spline {
|
|
||||||
degree: 3,
|
|
||||||
fit_points,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
spline.flags.closed = closed;
|
|
||||||
Some(EntityType::Spline(spline))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Preview through the same fit-point representation that `build()` commits.
|
/// Store the chosen construction method directly in the persistent spline.
|
||||||
fn sample_curve(pts: &[DVec3], closed: bool) -> Vec<[f32; 3]> {
|
fn make_spline(pts: &[DVec3], closed: bool, control_vertices: bool) -> Spline {
|
||||||
|
let mut points: Vec<Vector3> = pts.iter().map(|p| Vector3::new(p.x, p.y, p.z)).collect();
|
||||||
|
let mut spline = if control_vertices {
|
||||||
|
if closed && pts.first() != pts.last() {
|
||||||
|
points.push(points[0]);
|
||||||
|
}
|
||||||
|
let count = points.len();
|
||||||
|
let degree = 3.min(count.saturating_sub(1));
|
||||||
|
let spans = count - degree;
|
||||||
|
// Open uniform knot vector: degree + 1 equal knots at each endpoint.
|
||||||
|
let knots = (0..count + degree + 1)
|
||||||
|
.map(|index| {
|
||||||
|
if index <= degree {
|
||||||
|
0.0
|
||||||
|
} else if index >= count {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
(index - degree) as f64 / spans as f64
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Spline {
|
||||||
|
degree: degree as i32,
|
||||||
|
control_points: points,
|
||||||
|
knots,
|
||||||
|
weights: vec![1.0; count],
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Spline {
|
||||||
|
degree: 3,
|
||||||
|
fit_points: points,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
spline.flags.closed = closed;
|
||||||
|
spline
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preview the exact representation that `build()` commits.
|
||||||
|
fn sample_curve(pts: &[DVec3], closed: bool, control_vertices: bool) -> Vec<[f32; 3]> {
|
||||||
if pts.len() < 2 {
|
if pts.len() < 2 {
|
||||||
return pts
|
return pts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
|
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
let mut spline = Spline {
|
let spline = make_spline(pts, closed, control_vertices);
|
||||||
degree: 3,
|
|
||||||
fit_points: pts
|
|
||||||
.iter()
|
|
||||||
.map(|p| Vector3::new(p.x, p.y, p.z))
|
|
||||||
.collect(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
spline.flags.closed = closed;
|
|
||||||
crate::entities::curve::spline_curve(&spline)
|
crate::entities::curve::spline_curve(&spline)
|
||||||
.map(|curve| crate::entities::curve::curve_points(&curve))
|
.map(|curve| crate::entities::curve::curve_points(&curve))
|
||||||
.unwrap_or_else(|| crate::entities::spline::measurement_polyline(&spline))
|
.unwrap_or_else(|| crate::entities::spline::measurement_polyline(&spline))
|
||||||
@@ -77,11 +115,19 @@ fn sample_curve(pts: &[DVec3], closed: bool) -> Vec<[f32; 3]> {
|
|||||||
|
|
||||||
impl CadCommand for SplineCommand {
|
impl CadCommand for SplineCommand {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"SPLINE"
|
if self.control_vertices {
|
||||||
|
"SPLINECV"
|
||||||
|
} else {
|
||||||
|
"SPLINE"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
if self.pts.is_empty() {
|
if self.choosing_method {
|
||||||
|
"SPLINE Choose creation method [Fit/Control vertices]:".into()
|
||||||
|
} else if self.pts.is_empty() && self.control_vertices {
|
||||||
|
t!("SPLINE Specify first control point:").into_owned()
|
||||||
|
} else if self.pts.is_empty() {
|
||||||
t!("SPLINE Specify first point:").into_owned()
|
t!("SPLINE Specify first point:").into_owned()
|
||||||
} else {
|
} else {
|
||||||
let n = self.pts.len();
|
let n = self.pts.len();
|
||||||
@@ -91,8 +137,14 @@ impl CadCommand for SplineCommand {
|
|||||||
|
|
||||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||||
use crate::command::CmdOption;
|
use crate::command::CmdOption;
|
||||||
|
if self.choosing_method {
|
||||||
|
return vec![
|
||||||
|
CmdOption::new("Fit", "FIT"),
|
||||||
|
CmdOption::new("Control vertices", "CV"),
|
||||||
|
];
|
||||||
|
}
|
||||||
if self.pts.is_empty() {
|
if self.pts.is_empty() {
|
||||||
return Vec::new();
|
return vec![CmdOption::new(t!("Method").as_ref(), "M")];
|
||||||
}
|
}
|
||||||
let mut opts = vec![CmdOption::new(t!("Close").as_ref(), "C")];
|
let mut opts = vec![CmdOption::new(t!("Close").as_ref(), "C")];
|
||||||
// Undo only makes sense once a control point exists.
|
// Undo only makes sense once a control point exists.
|
||||||
@@ -102,11 +154,18 @@ impl CadCommand for SplineCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
|
if self.choosing_method || !pt.is_finite() {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
self.pts.push(pt);
|
self.pts.push(pt);
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
if self.choosing_method {
|
||||||
|
self.choosing_method = false;
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
match self.build(false) {
|
match self.build(false) {
|
||||||
Some(e) => CmdResult::CommitAndExit(e),
|
Some(e) => CmdResult::CommitAndExit(e),
|
||||||
None => CmdResult::Cancel,
|
None => CmdResult::Cancel,
|
||||||
@@ -114,26 +173,37 @@ impl CadCommand for SplineCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn enter_accepts_default_start(&self) -> bool {
|
fn enter_accepts_default_start(&self) -> bool {
|
||||||
self.pts.is_empty()
|
self.pts.is_empty() && !self.choosing_method
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_escape(&mut self) -> CmdResult {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
match self.build(false) {
|
CmdResult::Cancel
|
||||||
Some(e) => CmdResult::CommitAndExit(e),
|
|
||||||
None => CmdResult::Cancel,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn wants_text_input(&self) -> bool {
|
||||||
!self.pts.is_empty()
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
!self.pts.is_empty()
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
match text.trim().to_uppercase().as_str() {
|
match text.trim().to_uppercase().as_str() {
|
||||||
|
"M" | "METHOD" if self.pts.is_empty() => {
|
||||||
|
self.choosing_method = true;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
"F" | "FIT" if self.pts.is_empty() => {
|
||||||
|
self.control_vertices = false;
|
||||||
|
self.choosing_method = false;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
"CV" | "CONTROL" | "CONTROLVERTICES" if self.pts.is_empty() => {
|
||||||
|
self.control_vertices = true;
|
||||||
|
self.choosing_method = false;
|
||||||
|
Some(CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
"C" | "CLOSE" => match self.build(true) {
|
"C" | "CLOSE" => match self.build(true) {
|
||||||
Some(e) => Some(CmdResult::CommitAndExit(e)),
|
Some(e) => Some(CmdResult::CommitAndExit(e)),
|
||||||
None => Some(CmdResult::NeedPoint),
|
None => Some(CmdResult::NeedPoint),
|
||||||
@@ -150,18 +220,44 @@ impl CadCommand for SplineCommand {
|
|||||||
if self.pts.is_empty() {
|
if self.pts.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// Preview the committed fit-point representation.
|
// Preview the committed construction method.
|
||||||
let mut ctrl = self.pts.clone();
|
let mut ctrl = self.pts.clone();
|
||||||
ctrl.push(pt);
|
ctrl.push(pt);
|
||||||
Some(WireModel::solid(
|
Some(WireModel::solid(
|
||||||
"rubber_band".into(),
|
"rubber_band".into(),
|
||||||
sample_curve(&ctrl, false),
|
sample_curve(&ctrl, false, self.control_vertices),
|
||||||
WireModel::CYAN,
|
WireModel::CYAN,
|
||||||
false,
|
false,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn control_vertices_make_a_clamped_finite_spline() {
|
||||||
|
let points = [
|
||||||
|
DVec3::new(0.0, 0.0, 0.0),
|
||||||
|
DVec3::new(1.0, 2.0, 0.0),
|
||||||
|
DVec3::new(2.0, 2.0, 0.0),
|
||||||
|
DVec3::new(3.0, 0.0, 0.0),
|
||||||
|
];
|
||||||
|
let spline = make_spline(&points, false, true);
|
||||||
|
|
||||||
|
assert_eq!(spline.degree, 3);
|
||||||
|
assert_eq!(spline.control_points.len(), 4);
|
||||||
|
assert_eq!(spline.knots, vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]);
|
||||||
|
assert_eq!(spline.weights, vec![1.0; 4]);
|
||||||
|
assert!(sample_curve(&points, false, true)
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.all(|value| value.is_finite()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
// ── Autocomplete registry ─────────────────────────────────
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["SPLINE"] }); // SplineCommand
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["SPLINE", "SPLINECV"]
|
||||||
|
}); // SplineCommand
|
||||||
@@ -2778,6 +2778,8 @@ pub fn empty_extruded_surface(direction: DVec3, taper_angle: f64) -> EntityType
|
|||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
|
|
||||||
let mut surface = Surface::new(SurfaceKind::Extruded);
|
let mut surface = Surface::new(SurfaceKind::Extruded);
|
||||||
|
surface.u_isolines = 6;
|
||||||
|
surface.v_isolines = 6;
|
||||||
if let SurfaceData::Extruded {
|
if let SurfaceData::Extruded {
|
||||||
options,
|
options,
|
||||||
sweep_vector,
|
sweep_vector,
|
||||||
|
|||||||