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
This commit is contained in:
Felix Riestra committed 2026-09-10 23:34:50 +02:00
commit ad718def3f
124 files changed
+9866 -692

No files matched your search

Generated
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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"] }
+3
View File
@@ -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

+59 -3
View File
@@ -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

+3
View File
@@ -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

+3
View File
@@ -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

+57 -3
View File
@@ -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

+11 -11
View File
@@ -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

+86 -12
View File
@@ -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

+3
View File
@@ -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

+3
View File
@@ -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

+3
View File
@@ -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

+3
View File
@@ -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

+3
View File
@@ -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

+3
View File
@@ -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

+11 -11
View File
@@ -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

+18 -18
View File
@@ -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

+3
View File
@@ -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

+18 -8
View File
@@ -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.
+3 -1
View File
@@ -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"
+98
View File
@@ -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
+34 -3
View File
@@ -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
+25 -2
View File
@@ -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
+277 -7
View File
@@ -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(())
} }
+65
View File
@@ -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"
+84 -1
View File
@@ -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>;
+9 -6
View File
@@ -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)))
} }
}; };
+136 -7
View File
@@ -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}"))
} }
+368 -21
View File
@@ -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 {
+61 -2
View File
@@ -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`)
+139 -3
View File
@@ -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"
}, },
+14 -2
View File
@@ -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 }"
] ]
} }
} }
+26 -1
View File
@@ -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,
},
} }
+31
View File
@@ -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,
},
} }
+42
View File
@@ -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(" "));
}
}
+11 -2
View File
@@ -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`).
+193
View File
@@ -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",
} }
} }
} }
+23
View File
@@ -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",
} }
} }
} }
+142 -1
View File
@@ -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());
}
}
+122 -5
View File
@@ -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))
} }
+495 -24
View File
@@ -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]
+1
View File
@@ -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>
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+65
View File
@@ -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)
+71 -6
View File
@@ -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ı…
+65
View File
@@ -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)
+65
View File
@@ -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)
+16 -5
View File
@@ -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"
+11 -7
View File
@@ -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')
+97
View File
@@ -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"));
}
} }
+223 -22
View File
@@ -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(&sections, &guides, path, &available, mode, options) loft_command_model::record(&sections, &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::*;
+84 -6
View File
@@ -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,
}
}
} }
+71 -7
View File
@@ -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);
}
}
}
+44 -1
View File
@@ -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")
);
}
} }
+88 -13
View File
@@ -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");
}
}
}
}
+9
View File
@@ -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 };
+1122 -97
View File
File diff suppressed because it is too large. Load diff
+76 -1
View File
@@ -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`.
+21 -1
View File
@@ -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));
+47 -9
View File
@@ -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
+78 -8
View File
@@ -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);
}
} }
+220 -32
View File
@@ -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);
}
} }
} }
} }
+35 -14
View File
@@ -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;
+49 -3
View File
@@ -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;
} }
+200
View File
@@ -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();
+67 -15
View File
@@ -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();
} }
+42 -6
View File
@@ -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.
+51 -2
View File
@@ -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,
+123
View File
@@ -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"));
}
}
+79 -9
View File
@@ -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
);
}
}
+105 -2
View File
@@ -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}");
} }
} }
+2
View File
@@ -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.
+287 -11
View File
@@ -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));
}
}
+158 -18
View File
@@ -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);
}
}
+179 -11
View File
@@ -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);
}
}
+83 -1
View File
@@ -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)
+65
View File
@@ -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")),
+31 -7
View File
@@ -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
+3 -1
View File
@@ -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"]
});
+134 -38
View File
@@ -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
+2
View File
@@ -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,
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff. Show more