brep-io-kernel 1.0.184 → 1.0.185
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/about.html +1 -1
- package/dist/help/developer-index.html +1 -1
- package/dist/help/expressions.html +730 -0
- package/dist/help/index.html +2 -1
- package/dist/help/input-params-schema.html +54 -2
- package/dist/help/modes__modeling.html +1 -1
- package/dist/help/part-history.html +81 -2
- package/dist/help/search-index.json +13 -7
- package/dist/help/table-of-contents.html +1 -0
- package/dist-kernel/help/developer-index.html +1 -1
- package/dist-kernel/help/expressions.html +730 -0
- package/dist-kernel/help/index.html +2 -1
- package/dist-kernel/help/input-params-schema.html +54 -2
- package/dist-kernel/help/modes__modeling.html +1 -1
- package/dist-kernel/help/part-history.html +81 -2
- package/dist-kernel/help/search-index.json +13 -7
- package/dist-kernel/help/table-of-contents.html +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,730 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
6
|
+
<title>Expressions and Configurator - BREP</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{
|
|
9
|
+
--bg:#0b0f14; --panel:#0f141b; --text:#d7dde6; --muted:#9aa7b2;
|
|
10
|
+
--border:#1b2430; --accent:#5cc8ff; --chip:#121823;
|
|
11
|
+
}
|
|
12
|
+
*{box-sizing:border-box}
|
|
13
|
+
html,body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono','Courier New',monospace}
|
|
14
|
+
main{max-width:1100px;margin:0 auto;padding:28px}
|
|
15
|
+
h1{margin:0 0 18px;font-size:22px;color:var(--accent);font-weight:700}
|
|
16
|
+
.summary{color:var(--muted);margin-bottom:22px}
|
|
17
|
+
.card{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:18px;margin:0 0 18px}
|
|
18
|
+
.readme{padding:0}
|
|
19
|
+
.readme .header{padding:16px 18px;border-bottom:1px solid var(--border)}
|
|
20
|
+
.readme .content{padding:18px}
|
|
21
|
+
.doc-card{padding:18px}
|
|
22
|
+
.doc-nav{margin:0 0 18px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;color:var(--muted)}
|
|
23
|
+
.doc-nav-links{display:flex;gap:12px;flex-wrap:wrap;align-items:center}
|
|
24
|
+
.doc-nav a{color:var(--accent);font-weight:600}
|
|
25
|
+
.doc-list{list-style:none;margin:18px 0;padding:0}
|
|
26
|
+
.doc-list li{margin:6px 0}
|
|
27
|
+
.doc-list a{color:var(--accent)}
|
|
28
|
+
.nav-search{margin-left:auto;position:relative;display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;min-width:240px;max-width:520px}
|
|
29
|
+
.search-title{font-weight:700;font-size:14px;margin-bottom:4px}
|
|
30
|
+
.search-hint{color:var(--muted);font-size:12px;margin:0}
|
|
31
|
+
.search-input-row{display:flex;gap:8px;align-items:center;width:100%}
|
|
32
|
+
.search-input{flex:1;border:1px solid var(--border);border-radius:10px;background:var(--chip);color:var(--text);padding:10px 12px}
|
|
33
|
+
.search-input:focus{outline:1px solid var(--accent);box-shadow:0 0 0 3px rgba(92,200,255,0.15)}
|
|
34
|
+
.search-clear{border:1px solid var(--border);background:var(--panel);color:var(--muted);border-radius:10px;padding:9px 12px;cursor:pointer;font-weight:600}
|
|
35
|
+
.search-clear:hover{border-color:var(--accent);color:var(--accent)}
|
|
36
|
+
.search-status{color:var(--muted);font-size:12px;margin-top:4px;width:100%;text-align:right}
|
|
37
|
+
.search-results{position:absolute;top:100%;right:0;margin-top:10px;border:1px solid var(--border);border-radius:12px;background:var(--panel);max-height:360px;overflow:auto;min-width:320px;max-width:520px;box-shadow:0 12px 30px rgba(0,0,0,0.35);z-index:25}
|
|
38
|
+
.search-results ul{list-style:none;margin:0;padding:0}
|
|
39
|
+
.search-item{padding:10px 12px;border-bottom:1px solid var(--border)}
|
|
40
|
+
.search-item:last-child{border-bottom:none}
|
|
41
|
+
.search-item a{display:block;color:var(--accent);font-weight:600}
|
|
42
|
+
.search-snippet{color:var(--muted);font-size:13px;margin-top:4px}
|
|
43
|
+
.search-results mark{background:rgba(92,200,255,0.2);color:var(--text);border-radius:4px;padding:0 2px}
|
|
44
|
+
.prose h1{font-size:24px;margin:0 0 12px}
|
|
45
|
+
.prose h2{font-size:18px;margin:18px 0 8px}
|
|
46
|
+
.prose h3{font-size:16px;margin:14px 0 6px}
|
|
47
|
+
.prose p{margin:0 0 10px}
|
|
48
|
+
.prose ul,.prose ol{margin:0 0 10px 18px}
|
|
49
|
+
.prose li{margin:4px 0}
|
|
50
|
+
.prose :not(pre)>code{background:#0d1520;border:1px solid var(--border);padding:1px 5px;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono','Courier New',monospace;font-size:12px}
|
|
51
|
+
.prose pre{margin:0;overflow:auto}
|
|
52
|
+
.prose pre code{display:block;border:0;background:transparent;padding:0;white-space:pre;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono','Courier New',monospace;font-size:13px;line-height:1.55;color:#eef4ff}
|
|
53
|
+
.prose .doc-codeblock{margin:14px 0;background:#1a2131;border:1px solid #3a4b74;border-radius:12px;overflow:hidden}
|
|
54
|
+
.prose .doc-codeblock-header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px;background:#2a3247;border-bottom:1px solid #3a4b74}
|
|
55
|
+
.prose .doc-codeblock-lang{font-size:20px;line-height:1;color:#e7efff;text-transform:lowercase}
|
|
56
|
+
.prose .doc-codeblock-body{padding:14px}
|
|
57
|
+
.prose .doc-codeblock-copy{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border:1px solid transparent;border-radius:8px;background:transparent;color:#9fc5ff;cursor:pointer}
|
|
58
|
+
.prose .doc-codeblock-copy:hover,.prose .doc-codeblock-copy:focus-visible{border-color:#4f6494;background:rgba(159,197,255,0.08);outline:none}
|
|
59
|
+
.prose .doc-codeblock-copy-icon{display:block;width:18px;height:18px}
|
|
60
|
+
.prose .doc-codeblock-copy-icon svg{display:block;width:100%;height:100%}
|
|
61
|
+
.prose .doc-codeblock-copy.is-copied{color:#a8ffd4}
|
|
62
|
+
.prose .doc-codeblock-copy.is-copied .doc-codeblock-copy-icon svg rect{stroke:currentColor}
|
|
63
|
+
.prose .doc-codeblock-js .tok-keyword{color:#d68dff;font-weight:600}
|
|
64
|
+
.prose .doc-codeblock-js .tok-string{color:#7be3a3}
|
|
65
|
+
.prose .doc-codeblock-js .tok-comment{color:#76839f}
|
|
66
|
+
.prose a{color:var(--accent)}
|
|
67
|
+
.prose img{max-width:100%;height:auto;}
|
|
68
|
+
.prose img.doc-gallery-trigger{cursor:zoom-in}
|
|
69
|
+
.prose img.doc-gallery-source-active{outline:2px solid var(--accent);outline-offset:2px;border-radius:6px}
|
|
70
|
+
.prose table{border-collapse:collapse;width:100%;margin:16px 0;background:var(--panel);border:1px solid var(--border);border-radius:8px;overflow:hidden}
|
|
71
|
+
.prose th,.prose td{padding:8px 12px;text-align:left;border-bottom:1px solid var(--border)}
|
|
72
|
+
.prose th{background:var(--chip);color:var(--text);font-weight:600;font-size:13px}
|
|
73
|
+
.prose tr:last-child td{border-bottom:none}
|
|
74
|
+
.prose tbody tr:hover{background:rgba(92,200,255,0.05)}
|
|
75
|
+
.doc-image-modal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;z-index:1200}
|
|
76
|
+
.doc-image-modal[hidden]{display:none!important}
|
|
77
|
+
.doc-image-backdrop{position:absolute;inset:0;border:0;margin:0;padding:0;background:rgba(6,10,14,0.86);cursor:pointer}
|
|
78
|
+
.doc-image-stage{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;max-width:min(96vw,1400px);max-height:90vh;width:100%}
|
|
79
|
+
.doc-image-view{max-width:100%;max-height:66vh;width:auto;height:auto;object-fit:contain;display:block;background:#0a1119;border:1px solid var(--border);border-radius:10px;box-shadow:0 16px 40px rgba(0,0,0,0.45)}
|
|
80
|
+
.doc-image-meta{display:flex;align-items:center;justify-content:space-between;gap:10px;width:min(100%,1000px);padding:8px 12px;border:1px solid var(--border);border-radius:10px;background:rgba(12,18,26,0.78);color:var(--muted);font-size:13px}
|
|
81
|
+
.doc-image-caption{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
82
|
+
.doc-image-counter{white-space:nowrap;color:var(--accent);font-variant-numeric:tabular-nums}
|
|
83
|
+
.doc-image-thumbs{width:min(100%,1000px);padding:8px;border:1px solid var(--border);border-radius:10px;background:rgba(12,18,26,0.78);overflow-x:auto;overflow-y:hidden}
|
|
84
|
+
.doc-image-thumbs-list{display:flex;gap:8px;align-items:center;min-width:max-content}
|
|
85
|
+
.doc-image-thumb{width:94px;height:62px;flex:0 0 auto;padding:0;border:1px solid var(--border);border-radius:8px;background:#0a1119;cursor:pointer;overflow:hidden}
|
|
86
|
+
.doc-image-thumb:hover,.doc-image-thumb:focus-visible{border-color:var(--accent)}
|
|
87
|
+
.doc-image-thumb.is-active{border-color:var(--accent);box-shadow:0 0 0 2px rgba(92,200,255,0.2)}
|
|
88
|
+
.doc-image-thumb-img{display:block;width:100%;height:100%;object-fit:cover;background:#070d14}
|
|
89
|
+
.doc-image-nav,.doc-image-close{position:absolute;z-index:2;width:42px;height:42px;border-radius:999px;border:1px solid var(--border);background:rgba(15,20,27,0.9);color:var(--text);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:24px;line-height:1}
|
|
90
|
+
.doc-image-nav:hover,.doc-image-close:hover{border-color:var(--accent);color:var(--accent)}
|
|
91
|
+
.doc-image-nav:disabled{opacity:0.4;cursor:default}
|
|
92
|
+
.doc-image-prev{left:12px;top:50%;transform:translateY(-50%)}
|
|
93
|
+
.doc-image-next{right:12px;top:50%;transform:translateY(-50%)}
|
|
94
|
+
.doc-image-close{top:12px;right:12px;font-size:28px}
|
|
95
|
+
@media (max-width:720px){
|
|
96
|
+
.doc-image-modal{padding:10px}
|
|
97
|
+
.doc-image-view{max-height:52vh}
|
|
98
|
+
.doc-image-meta{width:100%;font-size:12px}
|
|
99
|
+
.doc-image-thumbs{width:100%;padding:6px}
|
|
100
|
+
.doc-image-thumb{width:76px;height:50px}
|
|
101
|
+
.doc-image-prev{left:4px}
|
|
102
|
+
.doc-image-next{right:4px}
|
|
103
|
+
.doc-image-close{top:4px;right:4px}
|
|
104
|
+
}
|
|
105
|
+
.license{
|
|
106
|
+
background:var(--panel);border:1px solid var(--border);border-radius:14px;
|
|
107
|
+
padding:16px 16px 8px;margin:0 0 18px;
|
|
108
|
+
}
|
|
109
|
+
.license > h2{margin:0 0 10px;font-size:16px;font-weight:700}
|
|
110
|
+
.pkg{
|
|
111
|
+
border-top:1px solid var(--border);padding:10px 0;
|
|
112
|
+
display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center;
|
|
113
|
+
}
|
|
114
|
+
.pkg:first-of-type{border-top:none}
|
|
115
|
+
.pkg .meta{display:flex;gap:10px;flex-wrap:wrap}
|
|
116
|
+
.pkg .name{font-weight:600}
|
|
117
|
+
.pkg .desc{color:var(--muted);margin-top:2px}
|
|
118
|
+
.desc{color:var(--muted);margin-top:6px}
|
|
119
|
+
a{color:var(--accent);text-decoration:none} a:hover{text-decoration:underline}
|
|
120
|
+
.chip{background:var(--chip);border:1px solid var(--border);border-radius:999px;padding:2px 8px;font-size:12px;color:var(--muted)}
|
|
121
|
+
.footer{margin-top:26px;color:var(--muted);font-size:12px}
|
|
122
|
+
</style>
|
|
123
|
+
</head>
|
|
124
|
+
<body>
|
|
125
|
+
<main>
|
|
126
|
+
<nav class="doc-nav">
|
|
127
|
+
<div class="doc-nav-links">
|
|
128
|
+
<a href="index.html">Help Home</a><span>·</span><a href="/help/table-of-contents.html">Table of Contents</a><span>·</span><a href="https://github.com/mmiscool/BREP" target="_blank" rel="noopener noreferrer">GitHub</a>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="nav-search">
|
|
131
|
+
<div class="search-input-row">
|
|
132
|
+
<input type="search" id="doc-search" class="search-input" placeholder="Search docs... Type at least 2 characters to search" autocomplete="off" spellcheck="false" />
|
|
133
|
+
<button type="button" class="search-clear" id="doc-search-clear">Clear</button>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="search-status" id="doc-search-status"></div>
|
|
136
|
+
<div class="search-results" id="doc-search-results" hidden>
|
|
137
|
+
<ul id="doc-search-list"></ul>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</nav>
|
|
141
|
+
<section class="card doc-card">
|
|
142
|
+
|
|
143
|
+
<div class="prose">
|
|
144
|
+
<h1>Expressions and Configurator</h1><p>This page is the dedicated guide for the Expressions panel in Modeling Mode.</p><h2>Live Demos</h2><ul><li>Examples hub: <a href="https://BREP.io/apiExamples/index.html">https://BREP.io/apiExamples/index.html</a></li><li>Embeded CAD: <a href="https://BREP.io/apiExamples/Embeded_CAD.html">https://BREP.io/apiExamples/Embeded_CAD.html</a></li></ul><h2>Overview</h2><p>The Expressions panel has two related parts:</p><ul><li><code>expressions</code>: a shared script where you define variables and formulas</li><li><code>configurator</code>: a set of UI widgets whose values are exposed to expressions as <code>configurator.fieldName</code></li></ul><p>Together, they let you drive feature dialogs from reusable parameters instead of hard-coded values.</p><h2>Expression syntax</h2><p>Expressions use JavaScript-style syntax. Typical usage is to assign variables in the script, then reference them in feature fields.</p><p>Example script:</p><div class="doc-codeblock doc-codeblock-js" data-code-language="javascript">
|
|
145
|
+
<div class="doc-codeblock-header">
|
|
146
|
+
<span class="doc-codeblock-lang">javascript</span>
|
|
147
|
+
<button type="button" class="doc-codeblock-copy" data-copy-code aria-label="Copy code" title="Copy code">
|
|
148
|
+
<span class="doc-codeblock-copy-icon" aria-hidden="true">
|
|
149
|
+
<svg viewBox="0 0 24 24" fill="none">
|
|
150
|
+
<rect x="9" y="3" width="12" height="16" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
151
|
+
<rect x="3" y="9" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
152
|
+
</svg>
|
|
153
|
+
</span>
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="doc-codeblock-body"><pre><code class="language-javascript">wall = 2;
|
|
157
|
+
width = 80;
|
|
158
|
+
height = width * 1.5;
|
|
159
|
+
holeOffset = width * 0.25;</code></pre></div>
|
|
160
|
+
</div><p>Then in a feature dialog field:</p><div class="doc-codeblock doc-codeblock-js" data-code-language="javascript">
|
|
161
|
+
<div class="doc-codeblock-header">
|
|
162
|
+
<span class="doc-codeblock-lang">javascript</span>
|
|
163
|
+
<button type="button" class="doc-codeblock-copy" data-copy-code aria-label="Copy code" title="Copy code">
|
|
164
|
+
<span class="doc-codeblock-copy-icon" aria-hidden="true">
|
|
165
|
+
<svg viewBox="0 0 24 24" fill="none">
|
|
166
|
+
<rect x="9" y="3" width="12" height="16" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
167
|
+
<rect x="3" y="9" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
168
|
+
</svg>
|
|
169
|
+
</span>
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
<div class="doc-codeblock-body"><pre><code class="language-javascript">height</code></pre></div>
|
|
173
|
+
</div><p>or:</p><div class="doc-codeblock doc-codeblock-js" data-code-language="javascript">
|
|
174
|
+
<div class="doc-codeblock-header">
|
|
175
|
+
<span class="doc-codeblock-lang">javascript</span>
|
|
176
|
+
<button type="button" class="doc-codeblock-copy" data-copy-code aria-label="Copy code" title="Copy code">
|
|
177
|
+
<span class="doc-codeblock-copy-icon" aria-hidden="true">
|
|
178
|
+
<svg viewBox="0 0 24 24" fill="none">
|
|
179
|
+
<rect x="9" y="3" width="12" height="16" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
180
|
+
<rect x="3" y="9" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
181
|
+
</svg>
|
|
182
|
+
</span>
|
|
183
|
+
</button>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="doc-codeblock-body"><pre><code class="language-javascript">width - wall * 2</code></pre></div>
|
|
186
|
+
</div><h2>Runtime context</h2><p>When an expression is evaluated, the runtime includes:</p><ul><li><code>resolution = 32</code> by default</li><li>the current configurator values as <code>configurator</code></li><li>the contents of the Expressions script</li></ul><p>That means these are valid:</p><div class="doc-codeblock doc-codeblock-js" data-code-language="javascript">
|
|
187
|
+
<div class="doc-codeblock-header">
|
|
188
|
+
<span class="doc-codeblock-lang">javascript</span>
|
|
189
|
+
<button type="button" class="doc-codeblock-copy" data-copy-code aria-label="Copy code" title="Copy code">
|
|
190
|
+
<span class="doc-codeblock-copy-icon" aria-hidden="true">
|
|
191
|
+
<svg viewBox="0 0 24 24" fill="none">
|
|
192
|
+
<rect x="9" y="3" width="12" height="16" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
193
|
+
<rect x="3" y="9" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
194
|
+
</svg>
|
|
195
|
+
</span>
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="doc-codeblock-body"><pre><code class="language-javascript">resolution
|
|
199
|
+
configurator.panelWidth
|
|
200
|
+
configurator.materialName</code></pre></div>
|
|
201
|
+
</div><h2>Configurator</h2><p>The configurator is for values that should be edited through UI controls instead of typing directly into the script.</p><p>Supported widget types:</p><ul><li><code>slider</code></li><li><code>number</code></li><li><code>select</code></li><li><code>string</code></li></ul><p>Example configurator usage:</p><div class="doc-codeblock doc-codeblock-js" data-code-language="javascript">
|
|
202
|
+
<div class="doc-codeblock-header">
|
|
203
|
+
<span class="doc-codeblock-lang">javascript</span>
|
|
204
|
+
<button type="button" class="doc-codeblock-copy" data-copy-code aria-label="Copy code" title="Copy code">
|
|
205
|
+
<span class="doc-codeblock-copy-icon" aria-hidden="true">
|
|
206
|
+
<svg viewBox="0 0 24 24" fill="none">
|
|
207
|
+
<rect x="9" y="3" width="12" height="16" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
208
|
+
<rect x="3" y="9" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
209
|
+
</svg>
|
|
210
|
+
</span>
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="doc-codeblock-body"><pre><code class="language-javascript">width = configurator.panelWidth;
|
|
214
|
+
height = width * 2;
|
|
215
|
+
labelText = configurator.partLabel;</code></pre></div>
|
|
216
|
+
</div><p>Then feature inputs can use:</p><div class="doc-codeblock doc-codeblock-js" data-code-language="javascript">
|
|
217
|
+
<div class="doc-codeblock-header">
|
|
218
|
+
<span class="doc-codeblock-lang">javascript</span>
|
|
219
|
+
<button type="button" class="doc-codeblock-copy" data-copy-code aria-label="Copy code" title="Copy code">
|
|
220
|
+
<span class="doc-codeblock-copy-icon" aria-hidden="true">
|
|
221
|
+
<svg viewBox="0 0 24 24" fill="none">
|
|
222
|
+
<rect x="9" y="3" width="12" height="16" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
223
|
+
<rect x="3" y="9" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
224
|
+
</svg>
|
|
225
|
+
</span>
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="doc-codeblock-body"><pre><code class="language-javascript">width
|
|
229
|
+
height
|
|
230
|
+
labelText
|
|
231
|
+
configurator.panelWidth * 0.5</code></pre></div>
|
|
232
|
+
</div><h2>Expressions panel behavior</h2><ul><li>If no configurator fields exist, the configurator form is hidden.</li><li>If configurator fields exist, the live configurator form appears above the expression editor.</li><li>Editing a configurator value in that live form re-runs the model.</li><li>Editing the configurator layout with <code>Edit Configurator</code> shows a live preview of the widget set above the expression editor.</li><li>While the configurator editor is open, that preview does not re-run the model.</li><li>The model is re-evaluated only when the configurator edit session is committed with <code>Save Configurator</code> or by closing the editor.</li></ul><h2>Using expressions in feature dialogs</h2><p>Feature dialogs evaluate against the shared expression source.</p><p>Field support:</p><ul><li><code>number</code> fields support expressions by default</li><li><code>string</code> fields can support expressions when the schema enables <code>allowExpression: true</code></li><li><code>transform</code> and <code>vec3</code> numeric entries also evaluate expressions</li></ul><p>Examples:</p><div class="doc-codeblock doc-codeblock-js" data-code-language="javascript">
|
|
233
|
+
<div class="doc-codeblock-header">
|
|
234
|
+
<span class="doc-codeblock-lang">javascript</span>
|
|
235
|
+
<button type="button" class="doc-codeblock-copy" data-copy-code aria-label="Copy code" title="Copy code">
|
|
236
|
+
<span class="doc-codeblock-copy-icon" aria-hidden="true">
|
|
237
|
+
<svg viewBox="0 0 24 24" fill="none">
|
|
238
|
+
<rect x="9" y="3" width="12" height="16" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
239
|
+
<rect x="3" y="9" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
240
|
+
</svg>
|
|
241
|
+
</span>
|
|
242
|
+
</button>
|
|
243
|
+
</div>
|
|
244
|
+
<div class="doc-codeblock-body"><pre><code class="language-javascript">distance = configurator.panelWidth * 0.5</code></pre></div>
|
|
245
|
+
</div><div class="doc-codeblock doc-codeblock-js" data-code-language="javascript">
|
|
246
|
+
<div class="doc-codeblock-header">
|
|
247
|
+
<span class="doc-codeblock-lang">javascript</span>
|
|
248
|
+
<button type="button" class="doc-codeblock-copy" data-copy-code aria-label="Copy code" title="Copy code">
|
|
249
|
+
<span class="doc-codeblock-copy-icon" aria-hidden="true">
|
|
250
|
+
<svg viewBox="0 0 24 24" fill="none">
|
|
251
|
+
<rect x="9" y="3" width="12" height="16" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
252
|
+
<rect x="3" y="9" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
253
|
+
</svg>
|
|
254
|
+
</span>
|
|
255
|
+
</button>
|
|
256
|
+
</div>
|
|
257
|
+
<div class="doc-codeblock-body"><pre><code class="language-javascript">text = configurator.partLabel</code></pre></div>
|
|
258
|
+
</div><h2>Persistence</h2><p>Both the script and the configurator are stored in part history.</p><p>Saved part history includes:</p><ul><li><code>expressions</code></li><li><code>configurator.fields</code></li><li><code>configurator.values</code></li></ul><p>That means they survive:</p><ul><li>save/load</li><li>JSON export/import</li><li>embedded feature history in 3MF</li><li>undo/redo snapshots</li></ul><h2>Practical example</h2><p>Expressions script:</p><div class="doc-codeblock doc-codeblock-js" data-code-language="javascript">
|
|
259
|
+
<div class="doc-codeblock-header">
|
|
260
|
+
<span class="doc-codeblock-lang">javascript</span>
|
|
261
|
+
<button type="button" class="doc-codeblock-copy" data-copy-code aria-label="Copy code" title="Copy code">
|
|
262
|
+
<span class="doc-codeblock-copy-icon" aria-hidden="true">
|
|
263
|
+
<svg viewBox="0 0 24 24" fill="none">
|
|
264
|
+
<rect x="9" y="3" width="12" height="16" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
265
|
+
<rect x="3" y="9" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
266
|
+
</svg>
|
|
267
|
+
</span>
|
|
268
|
+
</button>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="doc-codeblock-body"><pre><code class="language-javascript">wall = 2;
|
|
271
|
+
outerWidth = configurator.panelWidth;
|
|
272
|
+
outerHeight = configurator.panelHeight;
|
|
273
|
+
innerWidth = outerWidth - wall * 2;
|
|
274
|
+
innerHeight = outerHeight - wall * 2;
|
|
275
|
+
titleText = configurator.label;</code></pre></div>
|
|
276
|
+
</div><p>Possible configurator fields:</p><ul><li><code>panelWidth</code> as a slider</li><li><code>panelHeight</code> as a slider</li><li><code>label</code> as a string</li></ul><p>Then feature inputs can use:</p><div class="doc-codeblock doc-codeblock-js" data-code-language="javascript">
|
|
277
|
+
<div class="doc-codeblock-header">
|
|
278
|
+
<span class="doc-codeblock-lang">javascript</span>
|
|
279
|
+
<button type="button" class="doc-codeblock-copy" data-copy-code aria-label="Copy code" title="Copy code">
|
|
280
|
+
<span class="doc-codeblock-copy-icon" aria-hidden="true">
|
|
281
|
+
<svg viewBox="0 0 24 24" fill="none">
|
|
282
|
+
<rect x="9" y="3" width="12" height="16" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
283
|
+
<rect x="3" y="9" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"></rect>
|
|
284
|
+
</svg>
|
|
285
|
+
</span>
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
<div class="doc-codeblock-body"><pre><code class="language-javascript">outerWidth
|
|
289
|
+
outerHeight
|
|
290
|
+
innerWidth
|
|
291
|
+
innerHeight
|
|
292
|
+
titleText</code></pre></div>
|
|
293
|
+
</div><h2>Developer notes</h2><p>Relevant API and integration points:</p><ul><li><code>partHistory.expressions</code></li><li><code>partHistory.configurator</code></li><li><code>partHistory.getExpressionsSource()</code></li><li><code>partHistory.buildExpressionSource()</code></li><li><code>partHistory.evaluateExpression()</code></li></ul><p>If you are authoring a feature dialog schema:</p><ul><li>use <code>type: 'number'</code> for expression-capable numeric fields</li><li>use <code>type: 'string'</code> with <code>allowExpression: true</code> for expression-capable string fields</li></ul><h2>Safety note</h2><p>Expressions are executed as code using <code>Function()</code>. Do not evaluate untrusted user input.</p><h2>Related docs</h2><ul><li><a href="modes__modeling.html">Modeling Mode</a></li><li><a href="input-params-schema.html">Input Params Schema</a></li><li><a href="part-history.html">PartHistory Reference</a></li></ul>
|
|
294
|
+
</div>
|
|
295
|
+
</section>
|
|
296
|
+
</main>
|
|
297
|
+
<script>
|
|
298
|
+
(() => {
|
|
299
|
+
const searchInput = document.getElementById("doc-search");
|
|
300
|
+
const resultsBox = document.getElementById("doc-search-results");
|
|
301
|
+
const resultsList = document.getElementById("doc-search-list");
|
|
302
|
+
const statusEl = document.getElementById("doc-search-status");
|
|
303
|
+
const clearBtn = document.getElementById("doc-search-clear");
|
|
304
|
+
if (!searchInput || !resultsBox || !resultsList || !statusEl || !clearBtn) return;
|
|
305
|
+
const indexUrl = "./search-index.json";
|
|
306
|
+
let indexPromise = null;
|
|
307
|
+
let cachedEntries = null;
|
|
308
|
+
|
|
309
|
+
const escapeHtml = (text = "") =>
|
|
310
|
+
text
|
|
311
|
+
.replace(/&/g, "&")
|
|
312
|
+
.replace(/</g, "<")
|
|
313
|
+
.replace(/>/g, ">");
|
|
314
|
+
|
|
315
|
+
const escapeRegExp = (text = "") => text.replace(/[.*+?^{}()|[\]$]/g, "\$&");
|
|
316
|
+
|
|
317
|
+
const highlight = (text, terms) => {
|
|
318
|
+
if (!terms.length) return escapeHtml(text);
|
|
319
|
+
const pattern = terms.map(escapeRegExp).join("|");
|
|
320
|
+
const regex = new RegExp(`(${pattern})`, "gi");
|
|
321
|
+
return escapeHtml(text).replace(regex, "<mark>$1</mark>");
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const buildSnippet = (entry, terms) => {
|
|
325
|
+
const source = entry.content || entry.summary || "";
|
|
326
|
+
if (!source) return { plain: "", highlighted: "" };
|
|
327
|
+
let hit = source.length;
|
|
328
|
+
for (const term of terms) {
|
|
329
|
+
const idx = entry.contentLower.indexOf(term);
|
|
330
|
+
if (idx !== -1 && idx < hit) hit = idx;
|
|
331
|
+
}
|
|
332
|
+
if (!Number.isFinite(hit) || hit === source.length) hit = 0;
|
|
333
|
+
const start = Math.max(0, hit - 60);
|
|
334
|
+
const end = Math.min(source.length, hit + 140);
|
|
335
|
+
const snippet = source.slice(start, end);
|
|
336
|
+
return { plain: snippet, highlighted: highlight(snippet, terms) };
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const updateStatus = (message) => {
|
|
340
|
+
statusEl.textContent = message;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const loadIndex = async () => {
|
|
344
|
+
if (cachedEntries) return cachedEntries;
|
|
345
|
+
if (!indexPromise) {
|
|
346
|
+
indexPromise = fetch(indexUrl, { cache: "no-store" })
|
|
347
|
+
.then((res) => {
|
|
348
|
+
if (!res.ok) throw new Error("Failed to load search index");
|
|
349
|
+
return res.json();
|
|
350
|
+
})
|
|
351
|
+
.then((json) => {
|
|
352
|
+
cachedEntries = Array.isArray(json)
|
|
353
|
+
? json.map((entry) => ({
|
|
354
|
+
...entry,
|
|
355
|
+
contentLower: (entry.content || "").toLowerCase(),
|
|
356
|
+
titleLower: (entry.title || "").toLowerCase(),
|
|
357
|
+
}))
|
|
358
|
+
: [];
|
|
359
|
+
return cachedEntries;
|
|
360
|
+
})
|
|
361
|
+
.catch((err) => {
|
|
362
|
+
console.error("Search index load failed", err);
|
|
363
|
+
cachedEntries = [];
|
|
364
|
+
throw err;
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return indexPromise;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const renderResults = (entries, terms) => {
|
|
371
|
+
if (!terms.length) {
|
|
372
|
+
resultsBox.hidden = true;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (!entries.length) {
|
|
376
|
+
resultsList.innerHTML = '<li class="search-item"><div class="search-snippet">No matches found.</div></li>';
|
|
377
|
+
resultsBox.hidden = false;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const rendered = entries
|
|
381
|
+
.slice(0, 30)
|
|
382
|
+
.map((entry) => {
|
|
383
|
+
const snippet = buildSnippet(entry, terms);
|
|
384
|
+
const title = highlight(entry.title || entry.href, terms);
|
|
385
|
+
const hasFragment = snippet.plain && snippet.plain.trim().length > 0;
|
|
386
|
+
const fragment = hasFragment ? `#:~:text=${encodeURIComponent(snippet.plain.trim())}` : "";
|
|
387
|
+
const hrefWithFragment = hasFragment
|
|
388
|
+
? (entry.href.includes("#") ? `${entry.href}&:~:text=${encodeURIComponent(snippet.plain.trim())}` : `${entry.href}${fragment}`)
|
|
389
|
+
: entry.href;
|
|
390
|
+
const snippetHtml = snippet.highlighted ? `<div class="search-snippet">${snippet.highlighted}</div>` : "";
|
|
391
|
+
return `<li class="search-item"><a href="${hrefWithFragment}">${title}</a>${snippetHtml}</li>`;
|
|
392
|
+
})
|
|
393
|
+
.join("");
|
|
394
|
+
resultsList.innerHTML = rendered;
|
|
395
|
+
resultsBox.hidden = false;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const performSearch = async () => {
|
|
399
|
+
const rawQuery = searchInput.value.trim();
|
|
400
|
+
if (rawQuery.length < 2) {
|
|
401
|
+
updateStatus("Type at least 2 characters to search.");
|
|
402
|
+
resultsBox.hidden = true;
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const terms = rawQuery.toLowerCase().split(/\s+/).filter(Boolean);
|
|
406
|
+
updateStatus("Searching...");
|
|
407
|
+
try {
|
|
408
|
+
const entries = await loadIndex();
|
|
409
|
+
const matches = entries.filter((entry) =>
|
|
410
|
+
terms.every((term) => entry.contentLower.includes(term) || entry.titleLower.includes(term))
|
|
411
|
+
);
|
|
412
|
+
renderResults(matches, terms);
|
|
413
|
+
updateStatus(matches.length ? `${matches.length} result${matches.length === 1 ? "" : "s"} for "${rawQuery}"` : "No matches found.");
|
|
414
|
+
} catch (err) {
|
|
415
|
+
updateStatus("Search unavailable (could not load index).");
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
searchInput.addEventListener("input", performSearch);
|
|
420
|
+
|
|
421
|
+
searchInput.addEventListener("focus", () => {
|
|
422
|
+
if (!cachedEntries) {
|
|
423
|
+
loadIndex().catch(() => updateStatus("Search unavailable (could not load index)."));
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
clearBtn.addEventListener("click", () => {
|
|
428
|
+
searchInput.value = "";
|
|
429
|
+
resultsBox.hidden = true;
|
|
430
|
+
updateStatus("Type at least 2 characters to search.");
|
|
431
|
+
searchInput.focus();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
document.addEventListener("keydown", (evt) => {
|
|
435
|
+
if (evt.key === "Escape" && document.activeElement === searchInput) {
|
|
436
|
+
searchInput.value = "";
|
|
437
|
+
resultsBox.hidden = true;
|
|
438
|
+
updateStatus("Type at least 2 characters to search.");
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const initCodeCopyButtons = () => {
|
|
443
|
+
const copyButtons = Array.from(document.querySelectorAll("[data-copy-code]"));
|
|
444
|
+
if (!copyButtons.length) return;
|
|
445
|
+
|
|
446
|
+
const copyText = async (value) => {
|
|
447
|
+
if (navigator.clipboard?.writeText) {
|
|
448
|
+
await navigator.clipboard.writeText(value);
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
const ta = document.createElement("textarea");
|
|
452
|
+
ta.value = value;
|
|
453
|
+
ta.setAttribute("readonly", "readonly");
|
|
454
|
+
ta.style.position = "fixed";
|
|
455
|
+
ta.style.opacity = "0";
|
|
456
|
+
ta.style.pointerEvents = "none";
|
|
457
|
+
document.body.appendChild(ta);
|
|
458
|
+
ta.select();
|
|
459
|
+
let copied = false;
|
|
460
|
+
try {
|
|
461
|
+
copied = document.execCommand("copy");
|
|
462
|
+
} catch {
|
|
463
|
+
copied = false;
|
|
464
|
+
}
|
|
465
|
+
document.body.removeChild(ta);
|
|
466
|
+
return copied;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
copyButtons.forEach((button) => {
|
|
470
|
+
button.addEventListener("click", async () => {
|
|
471
|
+
const wrapper = button.closest(".doc-codeblock");
|
|
472
|
+
const codeEl = wrapper?.querySelector("code");
|
|
473
|
+
const text = codeEl?.textContent || "";
|
|
474
|
+
if (!text.trim()) return;
|
|
475
|
+
let didCopy = false;
|
|
476
|
+
try {
|
|
477
|
+
didCopy = await copyText(text);
|
|
478
|
+
} catch {
|
|
479
|
+
didCopy = false;
|
|
480
|
+
}
|
|
481
|
+
if (!didCopy) return;
|
|
482
|
+
button.classList.add("is-copied");
|
|
483
|
+
button.setAttribute("aria-label", "Copied");
|
|
484
|
+
button.setAttribute("title", "Copied");
|
|
485
|
+
window.setTimeout(() => {
|
|
486
|
+
button.classList.remove("is-copied");
|
|
487
|
+
button.setAttribute("aria-label", "Copy code");
|
|
488
|
+
button.setAttribute("title", "Copy code");
|
|
489
|
+
}, 1200);
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const initImageGallery = () => {
|
|
495
|
+
const proseRoot = document.querySelector(".prose");
|
|
496
|
+
if (!proseRoot) return;
|
|
497
|
+
|
|
498
|
+
const galleryImages = Array.from(proseRoot.querySelectorAll("img"))
|
|
499
|
+
.filter((img) => {
|
|
500
|
+
const src = img.getAttribute("src") || img.src;
|
|
501
|
+
return !!src;
|
|
502
|
+
});
|
|
503
|
+
if (!galleryImages.length) return;
|
|
504
|
+
|
|
505
|
+
const triggerImages = galleryImages.filter((img) => !img.closest("a"));
|
|
506
|
+
if (!triggerImages.length) return;
|
|
507
|
+
|
|
508
|
+
const imageIndexMap = new Map();
|
|
509
|
+
galleryImages.forEach((img, index) => imageIndexMap.set(img, index));
|
|
510
|
+
|
|
511
|
+
const modal = document.createElement("div");
|
|
512
|
+
modal.className = "doc-image-modal";
|
|
513
|
+
modal.setAttribute("role", "dialog");
|
|
514
|
+
modal.setAttribute("aria-modal", "true");
|
|
515
|
+
modal.setAttribute("aria-label", "Image gallery");
|
|
516
|
+
modal.setAttribute("aria-hidden", "true");
|
|
517
|
+
modal.hidden = true;
|
|
518
|
+
|
|
519
|
+
const backdrop = document.createElement("button");
|
|
520
|
+
backdrop.type = "button";
|
|
521
|
+
backdrop.className = "doc-image-backdrop";
|
|
522
|
+
backdrop.setAttribute("aria-label", "Close image gallery");
|
|
523
|
+
|
|
524
|
+
const stage = document.createElement("div");
|
|
525
|
+
stage.className = "doc-image-stage";
|
|
526
|
+
|
|
527
|
+
const closeBtn = document.createElement("button");
|
|
528
|
+
closeBtn.type = "button";
|
|
529
|
+
closeBtn.className = "doc-image-close";
|
|
530
|
+
closeBtn.setAttribute("aria-label", "Close gallery");
|
|
531
|
+
closeBtn.textContent = "x";
|
|
532
|
+
|
|
533
|
+
const prevBtn = document.createElement("button");
|
|
534
|
+
prevBtn.type = "button";
|
|
535
|
+
prevBtn.className = "doc-image-nav doc-image-prev";
|
|
536
|
+
prevBtn.setAttribute("aria-label", "Previous image");
|
|
537
|
+
prevBtn.textContent = "<";
|
|
538
|
+
|
|
539
|
+
const nextBtn = document.createElement("button");
|
|
540
|
+
nextBtn.type = "button";
|
|
541
|
+
nextBtn.className = "doc-image-nav doc-image-next";
|
|
542
|
+
nextBtn.setAttribute("aria-label", "Next image");
|
|
543
|
+
nextBtn.textContent = ">";
|
|
544
|
+
|
|
545
|
+
const viewer = document.createElement("img");
|
|
546
|
+
viewer.className = "doc-image-view";
|
|
547
|
+
viewer.alt = "";
|
|
548
|
+
|
|
549
|
+
const meta = document.createElement("div");
|
|
550
|
+
meta.className = "doc-image-meta";
|
|
551
|
+
|
|
552
|
+
const caption = document.createElement("div");
|
|
553
|
+
caption.className = "doc-image-caption";
|
|
554
|
+
|
|
555
|
+
const counter = document.createElement("div");
|
|
556
|
+
counter.className = "doc-image-counter";
|
|
557
|
+
|
|
558
|
+
const thumbsBar = document.createElement("div");
|
|
559
|
+
thumbsBar.className = "doc-image-thumbs";
|
|
560
|
+
|
|
561
|
+
const thumbsList = document.createElement("div");
|
|
562
|
+
thumbsList.className = "doc-image-thumbs-list";
|
|
563
|
+
thumbsBar.appendChild(thumbsList);
|
|
564
|
+
|
|
565
|
+
meta.append(caption, counter);
|
|
566
|
+
stage.append(closeBtn, prevBtn, nextBtn, viewer, meta, thumbsBar);
|
|
567
|
+
modal.append(backdrop, stage);
|
|
568
|
+
document.body.appendChild(modal);
|
|
569
|
+
|
|
570
|
+
let activeIndex = -1;
|
|
571
|
+
let lastFocusedElement = null;
|
|
572
|
+
|
|
573
|
+
const getImageUrl = (img) => img?.currentSrc || img?.src || img?.getAttribute("src") || "";
|
|
574
|
+
|
|
575
|
+
const thumbButtons = galleryImages.map((sourceImg, index) => {
|
|
576
|
+
const thumbButton = document.createElement("button");
|
|
577
|
+
thumbButton.type = "button";
|
|
578
|
+
thumbButton.className = "doc-image-thumb";
|
|
579
|
+
thumbButton.setAttribute("aria-label", "Show image " + String(index + 1));
|
|
580
|
+
|
|
581
|
+
const thumbImage = document.createElement("img");
|
|
582
|
+
thumbImage.className = "doc-image-thumb-img";
|
|
583
|
+
thumbImage.alt = sourceImg.getAttribute("alt") || ("Image " + String(index + 1));
|
|
584
|
+
thumbImage.loading = "lazy";
|
|
585
|
+
thumbImage.src = getImageUrl(sourceImg);
|
|
586
|
+
|
|
587
|
+
thumbButton.appendChild(thumbImage);
|
|
588
|
+
thumbsList.appendChild(thumbButton);
|
|
589
|
+
return thumbButton;
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const clearActiveSource = () => {
|
|
593
|
+
galleryImages.forEach((img) => img.classList.remove("doc-gallery-source-active"));
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const markActiveSource = () => {
|
|
597
|
+
clearActiveSource();
|
|
598
|
+
const source = galleryImages[activeIndex];
|
|
599
|
+
if (source) source.classList.add("doc-gallery-source-active");
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const scrollSourceIntoView = () => {
|
|
603
|
+
const source = galleryImages[activeIndex];
|
|
604
|
+
if (!source) return;
|
|
605
|
+
source.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" });
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const renderActiveImage = () => {
|
|
609
|
+
const source = galleryImages[activeIndex];
|
|
610
|
+
if (!source) return;
|
|
611
|
+
const sourceUrl = getImageUrl(source);
|
|
612
|
+
const sourceAlt = source.getAttribute("alt") || "";
|
|
613
|
+
viewer.src = sourceUrl;
|
|
614
|
+
viewer.alt = sourceAlt;
|
|
615
|
+
caption.textContent = sourceAlt || "Image";
|
|
616
|
+
counter.textContent = String(activeIndex + 1) + " / " + String(galleryImages.length);
|
|
617
|
+
const canFlip = galleryImages.length > 1;
|
|
618
|
+
prevBtn.disabled = !canFlip;
|
|
619
|
+
nextBtn.disabled = !canFlip;
|
|
620
|
+
thumbButtons.forEach((thumb, index) => {
|
|
621
|
+
const isActive = index === activeIndex;
|
|
622
|
+
thumb.classList.toggle("is-active", isActive);
|
|
623
|
+
if (isActive) {
|
|
624
|
+
thumb.setAttribute("aria-current", "true");
|
|
625
|
+
thumb.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
|
|
626
|
+
} else {
|
|
627
|
+
thumb.removeAttribute("aria-current");
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
markActiveSource();
|
|
631
|
+
scrollSourceIntoView();
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
const openAtIndex = (index, triggerElement = null) => {
|
|
635
|
+
if (!Number.isInteger(index) || index < 0 || index >= galleryImages.length) return;
|
|
636
|
+
activeIndex = index;
|
|
637
|
+
lastFocusedElement = triggerElement || document.activeElement;
|
|
638
|
+
modal.hidden = false;
|
|
639
|
+
modal.setAttribute("aria-hidden", "false");
|
|
640
|
+
renderActiveImage();
|
|
641
|
+
closeBtn.focus();
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const closeModal = () => {
|
|
645
|
+
if (activeIndex < 0) return;
|
|
646
|
+
activeIndex = -1;
|
|
647
|
+
modal.hidden = true;
|
|
648
|
+
modal.setAttribute("aria-hidden", "true");
|
|
649
|
+
viewer.removeAttribute("src");
|
|
650
|
+
viewer.alt = "";
|
|
651
|
+
caption.textContent = "";
|
|
652
|
+
counter.textContent = "";
|
|
653
|
+
thumbButtons.forEach((thumb) => {
|
|
654
|
+
thumb.classList.remove("is-active");
|
|
655
|
+
thumb.removeAttribute("aria-current");
|
|
656
|
+
});
|
|
657
|
+
clearActiveSource();
|
|
658
|
+
if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
|
|
659
|
+
lastFocusedElement.focus();
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const stepImage = (direction) => {
|
|
664
|
+
if (activeIndex < 0 || galleryImages.length < 2) return;
|
|
665
|
+
const max = galleryImages.length;
|
|
666
|
+
activeIndex = (activeIndex + direction + max) % max;
|
|
667
|
+
renderActiveImage();
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
triggerImages.forEach((img) => {
|
|
671
|
+
const imageIndex = imageIndexMap.get(img);
|
|
672
|
+
if (!Number.isInteger(imageIndex)) return;
|
|
673
|
+
img.classList.add("doc-gallery-trigger");
|
|
674
|
+
if (!img.hasAttribute("tabindex")) img.setAttribute("tabindex", "0");
|
|
675
|
+
if (!img.hasAttribute("role")) img.setAttribute("role", "button");
|
|
676
|
+
img.addEventListener("click", (evt) => {
|
|
677
|
+
evt.preventDefault();
|
|
678
|
+
openAtIndex(imageIndex, img);
|
|
679
|
+
});
|
|
680
|
+
img.addEventListener("keydown", (evt) => {
|
|
681
|
+
if (evt.key !== "Enter" && evt.key !== " ") return;
|
|
682
|
+
evt.preventDefault();
|
|
683
|
+
openAtIndex(imageIndex, img);
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
thumbButtons.forEach((thumb, index) => {
|
|
688
|
+
thumb.addEventListener("click", (evt) => {
|
|
689
|
+
evt.preventDefault();
|
|
690
|
+
if (activeIndex < 0) {
|
|
691
|
+
openAtIndex(index, thumb);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (activeIndex !== index) {
|
|
695
|
+
activeIndex = index;
|
|
696
|
+
renderActiveImage();
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
backdrop.addEventListener("click", closeModal);
|
|
702
|
+
closeBtn.addEventListener("click", closeModal);
|
|
703
|
+
prevBtn.addEventListener("click", () => stepImage(-1));
|
|
704
|
+
nextBtn.addEventListener("click", () => stepImage(1));
|
|
705
|
+
|
|
706
|
+
document.addEventListener("keydown", (evt) => {
|
|
707
|
+
if (activeIndex < 0) return;
|
|
708
|
+
if (evt.key === "Escape") {
|
|
709
|
+
evt.preventDefault();
|
|
710
|
+
closeModal();
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (evt.key === "ArrowLeft") {
|
|
714
|
+
evt.preventDefault();
|
|
715
|
+
stepImage(-1);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (evt.key === "ArrowRight") {
|
|
719
|
+
evt.preventDefault();
|
|
720
|
+
stepImage(1);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
initCodeCopyButtons();
|
|
726
|
+
initImageGallery();
|
|
727
|
+
})();
|
|
728
|
+
</script>
|
|
729
|
+
</body>
|
|
730
|
+
</html>
|