cncjs-conversational 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +55 -2
- package/package.json +39 -4
- package/web/index.html +1144 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pedro Paulo Vezza Campos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cncjs-conversational
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Conversational programming for [CNCjs](https://github.com/cncjs/cncjs) as a
|
|
4
|
+
custom widget. Each operation is a dimensioned sketch: the number boxes sit on
|
|
5
|
+
the dimension lines they control, and the drawing redraws to scale as you type.
|
|
6
|
+
The toolpath and the G-code come from the same calculation.
|
|
7
|
+
|
|
8
|
+
Operations: square up stock (two setups), face, side face, rectangular pocket,
|
|
9
|
+
circular pocket and bolt circle. Output is metric Grbl G-code (G21, G54); peck
|
|
10
|
+
drilling is expanded because Grbl has no canned cycles.
|
|
11
|
+
|
|
12
|
+
## Use with CNCjs
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install cncjs-conversational
|
|
16
|
+
cncjs --mount /conversational:node_modules/cncjs-conversational/web
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
In CNCjs, turn on **Custom Widget** under **Manage Widgets** and set its URL to
|
|
20
|
+
`/conversational/`. [cncjs-whb04b6-el400-integration](https://github.com/pedropaulovc/cncjs-whb04b6-el400-integration)
|
|
21
|
+
mounts it for you.
|
|
22
|
+
|
|
23
|
+
Open a port in CNCjs, then click **Load in CNCjs**: the program replaces the one
|
|
24
|
+
loaded in CNCjs's workspace, ready for the Visualizer and **Run**. Loading is
|
|
25
|
+
refused while a program is running or paused, since it would stop the job.
|
|
26
|
+
Inside CNCjs the widget uses CNCjs's session token and `/api/gcode`, so it must
|
|
27
|
+
be served by CNCjs (`--mount`) rather than from another address. CNCjs's widget
|
|
28
|
+
sandbox blocks file downloads, so **Download .nc** only appears when the page is
|
|
29
|
+
opened on its own (for example `web/index.html` in a browser).
|
|
30
|
+
|
|
31
|
+
## Checks
|
|
32
|
+
|
|
33
|
+
Inputs are validated as you type. Errors (a pocket smaller than the tool, a cut
|
|
34
|
+
deeper than the stock, a side cut reaching the vise jaws) outline the boxes
|
|
35
|
+
involved and disable output. Warnings, such as a corner radius close to the tool
|
|
36
|
+
radius, leave output enabled. Check every program in a simulator and with an air
|
|
37
|
+
cut before cutting material.
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm ci
|
|
43
|
+
npx playwright install chromium
|
|
44
|
+
npm test
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Release
|
|
48
|
+
|
|
49
|
+
Bump `version` in `package.json`, add a `## [x.y.z]` section to `CHANGELOG.md`,
|
|
50
|
+
merge, then push the tag `vx.y.z`. The release workflow tests the package,
|
|
51
|
+
publishes it to npm through trusted publishing, and creates the GitHub release
|
|
52
|
+
from the changelog section.
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
|
|
56
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cncjs-conversational",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Conversational programming CNCjs custom widget: face, side face, square up, pockets and bolt circles laid out as a dimensioned sketch.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cncjs",
|
|
7
|
+
"cnc",
|
|
8
|
+
"conversational",
|
|
9
|
+
"g-code",
|
|
10
|
+
"grbl",
|
|
11
|
+
"widget"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Pedro Paulo Vezza Campos <pedro@vezza.com.br>",
|
|
15
|
+
"homepage": "https://github.com/pedropaulovc/cncjs-conversational#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/pedropaulovc/cncjs-conversational/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/pedropaulovc/cncjs-conversational.git"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
"./index.html": "./web/index.html",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"web"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"pack:check": "npm pack --dry-run",
|
|
35
|
+
"test": "node --test"
|
|
36
|
+
},
|
|
37
|
+
"type": "module",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"playwright": "1.64.1"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/web/index.html
ADDED
|
@@ -0,0 +1,1144 @@
|
|
|
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>Conversational programming</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* Hallmark · v2.0.0 · fast · scope: app · theme: custom (drafting sheet) · signature: every parameter is the dimension text of the sketch it changes */
|
|
9
|
+
/* critique: one sheet of paper, ink geometry, blue dimension work, orange only ever means toolpath; inputs live where a draftsman would write the number */
|
|
10
|
+
:root {
|
|
11
|
+
--paper: #ece8df;
|
|
12
|
+
--sheet: #faf8f3;
|
|
13
|
+
--field: #ffffff;
|
|
14
|
+
--ink: #1d2126;
|
|
15
|
+
--ink-2: #5a6068;
|
|
16
|
+
--rule: #d3cdbf;
|
|
17
|
+
--hatch: #a7a294;
|
|
18
|
+
--dim: #1f56a8;
|
|
19
|
+
--dim-soft: #1f56a826;
|
|
20
|
+
--path: #cf5a1c;
|
|
21
|
+
--path-soft: #cf5a1c2e;
|
|
22
|
+
--error: #b3261e;
|
|
23
|
+
--error-soft: #b3261e14;
|
|
24
|
+
--warn: #8a5a00;
|
|
25
|
+
--warn-soft: #8a5a0014;
|
|
26
|
+
--ok: #2c6b3c;
|
|
27
|
+
--font-ui: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
|
|
28
|
+
--font-num: "IBM Plex Mono", "Cascadia Mono", Consolas, ui-monospace, monospace;
|
|
29
|
+
--r: 6px;
|
|
30
|
+
}
|
|
31
|
+
* { box-sizing: border-box; }
|
|
32
|
+
html, body { overflow-x: clip; }
|
|
33
|
+
body { margin: 0; background: var(--paper); color: var(--ink); font: 15px/1.45 var(--font-ui); }
|
|
34
|
+
.app { max-width: 1440px; margin: 0 auto; padding: 16px 20px 28px; display: grid; grid-template-columns: 196px minmax(0, 1fr) 360px; gap: 18px; }
|
|
35
|
+
header { grid-column: 1 / -1; display: flex; align-items: baseline; gap: 16px; }
|
|
36
|
+
header h1 { margin: 0; font-size: 20px; font-weight: 600; }
|
|
37
|
+
header p { margin: 0; color: var(--ink-2); font-size: 14px; }
|
|
38
|
+
h2 { margin: 0 0 8px; font-size: 14px; font-weight: 600; color: var(--ink-2); }
|
|
39
|
+
|
|
40
|
+
/* operation rail */
|
|
41
|
+
.ops { display: flex; flex-direction: column; gap: 4px; }
|
|
42
|
+
.ops button { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border: 1px solid transparent; border-radius: var(--r); background: none; color: var(--ink); font: inherit; text-align: left; cursor: pointer; transition: background-color .12s, border-color .12s; }
|
|
43
|
+
.ops button svg { width: 26px; height: 26px; flex: none; color: var(--ink-2); }
|
|
44
|
+
.ops button:hover { background: var(--sheet); }
|
|
45
|
+
.ops button[aria-pressed="true"] { background: var(--sheet); border-color: var(--rule); font-weight: 600; }
|
|
46
|
+
.ops button[aria-pressed="true"] svg { color: var(--path); }
|
|
47
|
+
.checks { margin: 22px 0 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 6px; font-size: 14px; }
|
|
48
|
+
.checks li { padding: 7px 9px; border-radius: var(--r); border-left: 3px solid var(--ok); background: var(--sheet); }
|
|
49
|
+
.checks li[data-lv="warn"] { border-color: var(--warn); background: var(--warn-soft); color: var(--warn); }
|
|
50
|
+
.checks li[data-lv="error"] { border-color: var(--error); background: var(--error-soft); color: var(--error); }
|
|
51
|
+
|
|
52
|
+
/* the sheet */
|
|
53
|
+
.sheet { background: var(--sheet); border: 1px solid var(--rule); border-radius: var(--r); padding: 14px 16px 16px; min-width: 0; }
|
|
54
|
+
.view { position: relative; width: 100%; }
|
|
55
|
+
.view svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
|
|
56
|
+
.view figcaption, .cap { font-size: 13px; color: var(--ink-2); }
|
|
57
|
+
figure { margin: 0; }
|
|
58
|
+
figure + figure { margin-top: 6px; border-top: 1px dashed var(--rule); padding-top: 10px; }
|
|
59
|
+
.overlay { position: absolute; inset: 0; pointer-events: none; }
|
|
60
|
+
|
|
61
|
+
/* dimension text = input */
|
|
62
|
+
.fld { position: absolute; transform: translate(-50%, -50%); display: inline-flex; align-items: center; gap: 3px; padding: 1px 3px; border-radius: 4px; background: var(--sheet); color: var(--dim); font: 13px/1 var(--font-num); white-space: nowrap; pointer-events: auto; }
|
|
63
|
+
.fld input { width: 6.2ch; padding: 3px 4px; border: 1px solid var(--rule); border-radius: 3px; background: var(--field); color: var(--ink); font: inherit; text-align: right; -moz-appearance: textfield; appearance: textfield; transition: border-color .12s; }
|
|
64
|
+
.fld input::-webkit-outer-spin-button, .fld input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
|
65
|
+
.fld input:hover { border-color: var(--dim); }
|
|
66
|
+
.fld input:focus-visible { outline: 2px solid var(--dim); outline-offset: 1px; border-color: var(--dim); }
|
|
67
|
+
.fld .u { color: var(--ink-2); font-size: 12px; }
|
|
68
|
+
.fld[data-state="warn"] input { border-color: var(--warn); color: var(--warn); }
|
|
69
|
+
.fld[data-state="error"] input { border-color: var(--error); color: var(--error); background: var(--error-soft); }
|
|
70
|
+
.fld[hidden] { display: none; }
|
|
71
|
+
|
|
72
|
+
/* svg drafting */
|
|
73
|
+
.stock { fill: var(--field); stroke: var(--ink); stroke-width: 1.4; }
|
|
74
|
+
.sect { fill: url(#hatch); stroke: var(--ink); stroke-width: 1.4; }
|
|
75
|
+
.void { fill: var(--sheet); stroke: none; }
|
|
76
|
+
.jaw { fill: var(--rule); stroke: var(--ink-2); stroke-width: 1; }
|
|
77
|
+
.allow { fill: url(#hatch); fill-rule: evenodd; stroke: none; opacity: .8; }
|
|
78
|
+
.fin { fill: var(--field); stroke: var(--ink); stroke-width: 2; }
|
|
79
|
+
.raw { fill: none; stroke: var(--ink-2); stroke-width: 1; stroke-dasharray: 6 4; }
|
|
80
|
+
.tp-faint { fill: none; stroke: var(--path); stroke-width: 1; opacity: .35; }
|
|
81
|
+
.dimtxt { fill: var(--dim); font: 12px var(--font-num); }
|
|
82
|
+
.edge { fill: none; stroke: var(--ink); stroke-width: 1.4; }
|
|
83
|
+
.hidden-l { fill: none; stroke: var(--ink); stroke-width: 1; stroke-dasharray: 4 3; }
|
|
84
|
+
.feat { fill: var(--sheet); stroke: var(--ink); stroke-width: 1.6; }
|
|
85
|
+
.hatch { stroke: var(--hatch); stroke-width: 1; }
|
|
86
|
+
.tp { fill: none; stroke: var(--path); stroke-width: 1.2; }
|
|
87
|
+
.tp-rapid { fill: none; stroke: var(--path); stroke-width: 1; stroke-dasharray: 2 4; opacity: .8; }
|
|
88
|
+
.toolmark { fill: var(--path-soft); stroke: var(--path); stroke-width: 1.2; }
|
|
89
|
+
.layer { stroke: var(--path); stroke-width: 1; stroke-dasharray: 5 3; opacity: .7; }
|
|
90
|
+
.ctr { stroke: var(--ink-2); stroke-width: .8; stroke-dasharray: 10 3 2 3; fill: none; }
|
|
91
|
+
.dl { stroke: var(--dim); stroke-width: 1; fill: none; }
|
|
92
|
+
.ext { stroke: var(--dim); stroke-width: .8; opacity: .7; }
|
|
93
|
+
.ah { fill: var(--dim); }
|
|
94
|
+
.ahp { fill: var(--path); }
|
|
95
|
+
.safe { stroke: var(--dim); stroke-width: 1; stroke-dasharray: 6 4; }
|
|
96
|
+
.lbl { fill: var(--ink-2); font: 12px var(--font-ui); }
|
|
97
|
+
.dg.on .dl, .dg.on .ext { stroke: var(--dim); stroke-width: 2.2; opacity: 1; }
|
|
98
|
+
.dg.on .feat, .dg.on .edge { stroke: var(--dim); }
|
|
99
|
+
|
|
100
|
+
/* right column */
|
|
101
|
+
.side { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
|
|
102
|
+
.toolrow { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; }
|
|
103
|
+
#f-step { display: grid; grid-template-columns: 200px minmax(0, 1fr); align-items: center; gap: 12px; }
|
|
104
|
+
#f-step[hidden] { display: none; }
|
|
105
|
+
.code { display: flex; flex-direction: column; min-height: 0; }
|
|
106
|
+
.code-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
107
|
+
.code-head h2 { margin: 0; margin-right: auto; }
|
|
108
|
+
.stats { font-size: 13px; color: var(--ink-2); margin: 0 0 8px; }
|
|
109
|
+
.stats b { color: var(--ink); font-weight: 600; }
|
|
110
|
+
pre { margin: 0; height: 400px; overflow: auto; padding: 10px 12px; background: var(--field); border: 1px solid var(--rule); border-radius: var(--r); font: 12.5px/1.5 var(--font-num); color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
111
|
+
pre .c { color: var(--ink-2); }
|
|
112
|
+
pre .g0 { color: var(--dim); }
|
|
113
|
+
pre .arc { color: var(--path); }
|
|
114
|
+
.btn { padding: 6px 12px; border: 1px solid var(--rule); border-radius: var(--r); background: var(--field); color: var(--ink); font: 14px var(--font-ui); cursor: pointer; transition: border-color .12s, background-color .12s; }
|
|
115
|
+
.btn:hover { border-color: var(--ink-2); }
|
|
116
|
+
.btn:active { background: var(--paper); }
|
|
117
|
+
.btn:disabled { opacity: .45; cursor: not-allowed; }
|
|
118
|
+
.btn:focus-visible, .ops button:focus-visible { outline: 2px solid var(--dim); outline-offset: 2px; }
|
|
119
|
+
.btn.primary { background: var(--ink); border-color: var(--ink); color: var(--sheet); }
|
|
120
|
+
.setup { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 12px; padding-bottom: 12px; border-bottom: 1px dashed var(--rule); }
|
|
121
|
+
.setup[hidden] { display: none; }
|
|
122
|
+
.setup > span { font-size: 14px; color: var(--ink-2); margin-right: 4px; }
|
|
123
|
+
.setup button { padding: 6px 12px; border: 1px solid var(--rule); border-radius: var(--r); background: var(--field); color: var(--ink); font: 14px var(--font-ui); cursor: pointer; transition: border-color .12s, background-color .12s; }
|
|
124
|
+
.setup button:hover { border-color: var(--ink-2); }
|
|
125
|
+
.setup button[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--sheet); }
|
|
126
|
+
.setup button:focus-visible { outline: 2px solid var(--dim); outline-offset: 2px; }
|
|
127
|
+
.setup p { flex-basis: 100%; margin: 4px 0 0; font-size: 13px; color: var(--ink-2); }
|
|
128
|
+
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
|
129
|
+
@media (max-width: 1100px) { .app { grid-template-columns: minmax(0, 1fr); } .ops { flex-direction: row; flex-wrap: wrap; } }
|
|
130
|
+
</style>
|
|
131
|
+
</head>
|
|
132
|
+
<body>
|
|
133
|
+
<div class="app">
|
|
134
|
+
<header>
|
|
135
|
+
<h1>Conversational</h1>
|
|
136
|
+
<p>Units mm, work offset G54 with X0 Y0 at the front-left corner and Z0 on top of stock, Grbl output.</p>
|
|
137
|
+
</header>
|
|
138
|
+
|
|
139
|
+
<nav aria-label="Operation">
|
|
140
|
+
<h2>Operation</h2>
|
|
141
|
+
<div class="ops" id="ops">
|
|
142
|
+
<button data-op="square"><svg viewBox="0 0 26 26" aria-hidden="true"><rect x="2" y="4" width="22" height="18" fill="none" stroke="currentColor" stroke-width="1" stroke-dasharray="2.5 2"/><rect x="6" y="8" width="14" height="10" fill="none" stroke="currentColor" stroke-width="1.8"/></svg>Square up stock</button>
|
|
143
|
+
<button data-op="face"><svg viewBox="0 0 26 26" aria-hidden="true"><rect x="3" y="6" width="20" height="14" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M1 9h24M25 9v4M25 13H1M1 13v4M1 17h24" fill="none" stroke="currentColor" stroke-width="1.2"/></svg>Face</button>
|
|
144
|
+
<button data-op="side"><svg viewBox="0 0 26 26" aria-hidden="true"><rect x="3" y="5" width="20" height="16" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M3 17h20" fill="none" stroke="currentColor" stroke-width="1" stroke-dasharray="2 2"/><path d="M1 23h24" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>Side face</button>
|
|
145
|
+
<button data-op="rect"><svg viewBox="0 0 26 26" aria-hidden="true"><rect x="3" y="5" width="20" height="16" fill="none" stroke="currentColor" stroke-width="1.4"/><rect x="7" y="9" width="12" height="8" rx="2" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>Rectangular pocket</button>
|
|
146
|
+
<button data-op="circ"><svg viewBox="0 0 26 26" aria-hidden="true"><rect x="3" y="5" width="20" height="16" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="13" cy="13" r="5.5" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>Circular pocket</button>
|
|
147
|
+
<button data-op="bolt"><svg viewBox="0 0 26 26" aria-hidden="true"><circle cx="13" cy="13" r="8" fill="none" stroke="currentColor" stroke-width=".9" stroke-dasharray="3 2"/><circle cx="21" cy="13" r="2" fill="currentColor"/><circle cx="9" cy="19.9" r="2" fill="currentColor"/><circle cx="9" cy="6.1" r="2" fill="currentColor"/></svg>Bolt circle</button>
|
|
148
|
+
</div>
|
|
149
|
+
<ul class="checks" id="checks" aria-live="polite"></ul>
|
|
150
|
+
</nav>
|
|
151
|
+
|
|
152
|
+
<main class="sheet" aria-label="Sketch">
|
|
153
|
+
<div class="setup" id="setup" hidden>
|
|
154
|
+
<span>Setup</span>
|
|
155
|
+
<button data-setup="1">1: face top, cut sides</button>
|
|
156
|
+
<button data-setup="2">2: flip, face to thickness</button>
|
|
157
|
+
<p id="setup-note"></p>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="setup" id="sidepick" hidden>
|
|
160
|
+
<span>Side</span>
|
|
161
|
+
<button data-side="y-">Front (Y−)</button>
|
|
162
|
+
<button data-side="y+">Back (Y+)</button>
|
|
163
|
+
<button data-side="x-">Left (X−)</button>
|
|
164
|
+
<button data-side="x+">Right (X+)</button>
|
|
165
|
+
<p>Climb cut along the side, one radial pass per stepover, retracting to come back.</p>
|
|
166
|
+
</div>
|
|
167
|
+
<figure>
|
|
168
|
+
<figcaption>Plan, looking down Z</figcaption>
|
|
169
|
+
<div class="view" id="v-plan"></div>
|
|
170
|
+
</figure>
|
|
171
|
+
<figure>
|
|
172
|
+
<figcaption id="sec-cap">Section through centre, Z exaggerated</figcaption>
|
|
173
|
+
<div class="view" id="v-sec"></div>
|
|
174
|
+
</figure>
|
|
175
|
+
<noscript><p>This widget draws the sketch and its inputs with JavaScript.</p></noscript>
|
|
176
|
+
</main>
|
|
177
|
+
|
|
178
|
+
<aside class="side">
|
|
179
|
+
<section class="sheet">
|
|
180
|
+
<h2>Tool</h2>
|
|
181
|
+
<div class="toolrow">
|
|
182
|
+
<figure><div class="view" id="v-tool"></div></figure>
|
|
183
|
+
<figure id="f-step"><div class="view" id="v-step"></div><p class="cap">Stepover, % of tool Ø</p></figure>
|
|
184
|
+
</div>
|
|
185
|
+
</section>
|
|
186
|
+
<section class="sheet code">
|
|
187
|
+
<div class="code-head">
|
|
188
|
+
<h2>G-code</h2>
|
|
189
|
+
<button class="btn" id="copy">Copy</button>
|
|
190
|
+
<button class="btn primary" id="dl">Download .nc</button>
|
|
191
|
+
<button class="btn primary" id="load" hidden>Load in CNCjs</button>
|
|
192
|
+
</div>
|
|
193
|
+
<p class="stats" id="stats"></p>
|
|
194
|
+
<p class="stats" id="cnc-note" aria-live="polite" hidden></p>
|
|
195
|
+
<pre id="gcode" aria-label="Generated G-code"></pre>
|
|
196
|
+
</section>
|
|
197
|
+
</aside>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<script>
|
|
201
|
+
const VIEWS = {
|
|
202
|
+
plan: { vb: [800, 470] },
|
|
203
|
+
sec: { vb: [800, 240] },
|
|
204
|
+
tool: { vb: [420, 172] },
|
|
205
|
+
step: { vb: [220, 150] },
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const st = {
|
|
209
|
+
op: 'square',
|
|
210
|
+
stock: { w: 100, h: 60, t: 20 },
|
|
211
|
+
safeZ: 5,
|
|
212
|
+
stepdown: 1.5,
|
|
213
|
+
tool: { d: 6.35, rpm: 12000, feed: 800, plunge: 200, step: 45 },
|
|
214
|
+
face: { depth: 0.5 },
|
|
215
|
+
rect: { cx: 50, cy: 30, w: 50, l: 30, r: 6, depth: 6 },
|
|
216
|
+
circ: { cx: 50, cy: 30, d: 40, depth: 6 },
|
|
217
|
+
bolt: { cx: 50, cy: 30, bcd: 40, n: 6, a0: 0, hd: 5, depth: 8, peck: 2 },
|
|
218
|
+
sq: { setup: 1, w: 90, h: 50, t: 16, top: 1, wall: 17, stick: 18 },
|
|
219
|
+
side: { side: 'y-', amt: 3, depth: 12 },
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// t: visible dimension text, a: accessible name, u: unit suffix
|
|
223
|
+
const F = {
|
|
224
|
+
'stock.w': { t: 'W', a: 'Stock width along X', min: 1 },
|
|
225
|
+
'stock.h': { t: 'H', a: 'Stock height along Y', min: 1 },
|
|
226
|
+
'stock.t': { t: 'T', a: 'Stock thickness', min: 0.1 },
|
|
227
|
+
'safeZ': { t: 'clear', a: 'Clearance height above Z0', min: 0 },
|
|
228
|
+
'stepdown': { t: 'step', a: 'Step down per pass', min: 0.05 },
|
|
229
|
+
'face.depth': { t: 'depth', a: 'Facing depth', min: 0.01 },
|
|
230
|
+
'rect.cx': { t: 'X', a: 'Pocket centre X' }, 'rect.cy': { t: 'Y', a: 'Pocket centre Y' },
|
|
231
|
+
'rect.w': { t: 'W', a: 'Pocket width', min: 0.1 }, 'rect.l': { t: 'L', a: 'Pocket length', min: 0.1 },
|
|
232
|
+
'rect.r': { t: 'R', a: 'Pocket corner radius', min: 0 }, 'rect.depth': { t: 'depth', a: 'Pocket depth', min: 0.01 },
|
|
233
|
+
'circ.cx': { t: 'X', a: 'Pocket centre X' }, 'circ.cy': { t: 'Y', a: 'Pocket centre Y' },
|
|
234
|
+
'circ.d': { t: 'Ø', a: 'Pocket diameter', min: 0.1 }, 'circ.depth': { t: 'depth', a: 'Pocket depth', min: 0.01 },
|
|
235
|
+
'bolt.cx': { t: 'X', a: 'Bolt circle centre X' }, 'bolt.cy': { t: 'Y', a: 'Bolt circle centre Y' },
|
|
236
|
+
'bolt.bcd': { t: 'Ø', a: 'Bolt circle diameter', min: 0.1 }, 'bolt.n': { t: '×', a: 'Hole count', min: 1, step: 1 },
|
|
237
|
+
'bolt.a0': { t: '∠', a: 'Start angle from +X', u: '°', step: 5 }, 'bolt.hd': { t: 'Ø', a: 'Drill diameter', min: 0.1 },
|
|
238
|
+
'bolt.depth': { t: 'depth', a: 'Hole depth to drill tip', min: 0.01 }, 'bolt.peck': { t: 'peck', a: 'Peck depth', min: 0.05 },
|
|
239
|
+
'sq.w': { t: 'W', a: 'Finished width', min: 0.1 }, 'sq.h': { t: 'H', a: 'Finished height', min: 0.1 },
|
|
240
|
+
'sq.t': { t: 'T', a: 'Finished thickness', min: 0.1 }, 'sq.top': { t: 'top', a: 'Take off the top in setup 1', min: 0 },
|
|
241
|
+
'sq.wall': { t: 'sides', a: 'Side cut depth in setup 1', min: 0.1 }, 'sq.stick': { t: 'above jaws', a: 'Stock height above the vise jaws', min: 0.1 },
|
|
242
|
+
'side.amt': { t: 'remove', a: 'Material to remove from the side', min: 0.01 },
|
|
243
|
+
'side.depth': { t: 'depth', a: 'Side depth below Z0', min: 0.01 },
|
|
244
|
+
'tool.d': { t: 'Ø', a: 'Tool diameter', min: 0.1 },
|
|
245
|
+
'tool.rpm': { t: 'S', a: 'Spindle speed', u: 'rpm', step: 100, min: 1 },
|
|
246
|
+
'tool.feed': { t: 'F', a: 'Cutting feed', u: 'mm/min', step: 10, min: 1 },
|
|
247
|
+
'tool.plunge': { t: 'F', a: 'Plunge feed', u: 'mm/min', step: 10, min: 1 },
|
|
248
|
+
'tool.step': { t: '', a: 'Stepover percent of tool diameter', u: '%', step: 1, min: 1, max: 100 },
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const COMMON = ['stock.w', 'stock.h', 'stock.t', 'safeZ'];
|
|
252
|
+
const MILL = ['tool.d', 'tool.rpm', 'tool.feed', 'tool.plunge', 'tool.step'];
|
|
253
|
+
const OPS = {
|
|
254
|
+
face: { name: 'Face', fields: [...COMMON, 'face.depth', ...MILL] },
|
|
255
|
+
rect: { name: 'Rectangular pocket', fields: [...COMMON, 'stepdown', 'rect.cx', 'rect.cy', 'rect.w', 'rect.l', 'rect.r', 'rect.depth', ...MILL] },
|
|
256
|
+
circ: { name: 'Circular pocket', fields: [...COMMON, 'stepdown', 'circ.cx', 'circ.cy', 'circ.d', 'circ.depth', ...MILL] },
|
|
257
|
+
side: { name: 'Side face', fields: [...COMMON, 'stepdown', 'side.amt', 'side.depth', ...MILL] },
|
|
258
|
+
square: { name: 'Square up stock', fields: [...COMMON, 'stepdown', 'sq.w', 'sq.h', 'sq.t', 'sq.top', 'sq.wall', 'sq.stick', ...MILL] },
|
|
259
|
+
bolt: { name: 'Bolt circle', fields: [...COMMON, 'bolt.cx', 'bolt.cy', 'bolt.bcd', 'bolt.n', 'bolt.a0', 'bolt.hd', 'bolt.depth', 'bolt.peck', 'tool.rpm', 'tool.plunge'] },
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const get = p => p.split('.').reduce((o, k) => o[k], st);
|
|
263
|
+
const set = (p, v) => { const k = p.split('.'); const last = k.pop(); k.reduce((o, x) => o[x], st)[last] = v; };
|
|
264
|
+
const n2 = v => +v.toFixed(2);
|
|
265
|
+
const fmt = v => String(Math.round(v * 1000) / 1000);
|
|
266
|
+
const rad = d => d * Math.PI / 180;
|
|
267
|
+
|
|
268
|
+
// ---------- view scaffolding ----------
|
|
269
|
+
const DEFS = `<defs>
|
|
270
|
+
<marker id="a" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path class="ah" d="M0,1.5 L10,5 L0,8.5z"/></marker>
|
|
271
|
+
<marker id="ap" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse"><path class="ahp" d="M0,1.5 L10,5 L0,8.5z"/></marker>
|
|
272
|
+
<pattern id="hatch" width="7" height="7" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><line class="hatch" x1="0" y1="0" x2="0" y2="7"/></pattern>
|
|
273
|
+
</defs>`;
|
|
274
|
+
|
|
275
|
+
for (const [id, v] of Object.entries(VIEWS)) {
|
|
276
|
+
const el = document.getElementById('v-' + id);
|
|
277
|
+
el.style.aspectRatio = `${v.vb[0]} / ${v.vb[1]}`;
|
|
278
|
+
el.innerHTML = `<svg viewBox="0 0 ${v.vb[0]} ${v.vb[1]}" aria-hidden="true">${DEFS}<g></g></svg><div class="overlay"></div>`;
|
|
279
|
+
v.g = el.querySelector('g:last-of-type');
|
|
280
|
+
v.ov = el.querySelector('.overlay');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const inputs = {};
|
|
284
|
+
let active = null;
|
|
285
|
+
|
|
286
|
+
function buildInputs() {
|
|
287
|
+
for (const v of Object.values(VIEWS)) v.ov.replaceChildren();
|
|
288
|
+
for (const k of Object.keys(inputs)) delete inputs[k];
|
|
289
|
+
for (const p of OPS[st.op].fields) {
|
|
290
|
+
const f = F[p];
|
|
291
|
+
const lab = document.createElement('label');
|
|
292
|
+
lab.className = 'fld';
|
|
293
|
+
lab.innerHTML = `${f.t ? `<span>${f.t}</span>` : ''}<input type="number" inputmode="decimal" aria-label="${f.a}" step="${f.step ?? 0.1}" ${f.min != null ? `min="${f.min}"` : ''} ${f.max != null ? `max="${f.max}"` : ''}>${f.u ? `<span class="u">${f.u}</span>` : ''}`;
|
|
294
|
+
const inp = lab.querySelector('input');
|
|
295
|
+
inp.value = get(p);
|
|
296
|
+
inp.addEventListener('input', () => {
|
|
297
|
+
const v = parseFloat(inp.value);
|
|
298
|
+
const bad = !Number.isFinite(v) || (f.min != null && v < f.min) || (f.max != null && v > f.max);
|
|
299
|
+
lab.dataset.state = bad ? 'error' : '';
|
|
300
|
+
if (bad) return;
|
|
301
|
+
set(p, p === 'bolt.n' ? Math.max(1, Math.round(v)) : v);
|
|
302
|
+
render();
|
|
303
|
+
});
|
|
304
|
+
inp.addEventListener('focus', () => { active = p; render(); });
|
|
305
|
+
inp.addEventListener('blur', () => { active = null; render(); });
|
|
306
|
+
inputs[p] = lab;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ---------- svg helpers ----------
|
|
311
|
+
const ln = (c, x1, y1, x2, y2, extra = '') => `<line class="${c}" x1="${n2(x1)}" y1="${n2(y1)}" x2="${n2(x2)}" y2="${n2(y2)}" ${extra}/>`;
|
|
312
|
+
const ext = (x1, y1, x2, y2) => ln('ext', x1, y1, x2, y2);
|
|
313
|
+
const dl = (x1, y1, x2, y2, ends = 'both') => {
|
|
314
|
+
const tiny = Math.hypot(x2 - x1, y2 - y1) < 3;
|
|
315
|
+
const s = !tiny && ends !== 'end' ? 'marker-start="url(#a)"' : '';
|
|
316
|
+
const e = !tiny ? 'marker-end="url(#a)"' : '';
|
|
317
|
+
return ln('dl', x1, y1, x2, y2, `${s} ${e}`);
|
|
318
|
+
};
|
|
319
|
+
const g = (p, inner) => `<g class="dg${p === active ? ' on' : ''}">${inner}</g>`;
|
|
320
|
+
const cross = (x, y, k = 7) => ln('ctr', x - k, y, x + k, y) + ln('ctr', x, y - k, x, y + k);
|
|
321
|
+
const at = (v, x, y) => ({ v, x, y });
|
|
322
|
+
|
|
323
|
+
function planFrame() {
|
|
324
|
+
const [VW, VH] = VIEWS.plan.vb;
|
|
325
|
+
const { w: W, h: H } = st.stock;
|
|
326
|
+
const L = 170, R = 140, T = 66, B = 86;
|
|
327
|
+
const aw = VW - L - R, ah = VH - T - B;
|
|
328
|
+
const s = Math.min(aw / W, ah / H);
|
|
329
|
+
const ox = L + (aw - W * s) / 2, top = T + (ah - H * s) / 2, bot = top + H * s;
|
|
330
|
+
return { s, ox, top, bot, W, H, X: x => ox + x * s, Y: y => bot - y * s };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------- toolpath geometry (mm), shared by sketch and G-code ----------
|
|
334
|
+
const tR = () => st.tool.d / 2;
|
|
335
|
+
const stepMM = () => st.tool.d * st.tool.step / 100;
|
|
336
|
+
const levels = (depth, sd) => { const z = []; for (let d = sd; d < depth - 1e-6; d += sd) z.push(-d); z.push(-depth); return z; };
|
|
337
|
+
|
|
338
|
+
function circRings() {
|
|
339
|
+
const R = st.circ.d / 2 - tR();
|
|
340
|
+
const rs = [];
|
|
341
|
+
for (let r = R; r > 1e-6; r -= stepMM()) rs.unshift(r);
|
|
342
|
+
return rs;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function rectLoops() {
|
|
346
|
+
const { w, l, r } = st.rect;
|
|
347
|
+
const hw = w / 2 - tR(), hh = l / 2 - tR(), rc = Math.max(Math.min(r, w / 2, l / 2) - tR(), 0);
|
|
348
|
+
const loops = [];
|
|
349
|
+
for (let k = 0; hw - k * stepMM() > 1e-6 && hh - k * stepMM() > 1e-6; k++) {
|
|
350
|
+
loops.unshift({ hw: hw - k * stepMM(), hh: hh - k * stepMM(), rc: Math.max(rc - k * stepMM(), 0) });
|
|
351
|
+
}
|
|
352
|
+
return loops;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function rasterPasses(x0, x1, y0, y1) {
|
|
356
|
+
const ys = [];
|
|
357
|
+
for (let y = y1 + tR() - stepMM(); ; y -= stepMM()) { ys.push(y); if (y - tR() <= y0) break; }
|
|
358
|
+
return { ys, xa: x0 - tR() - 2, xb: x1 + tR() + 2 };
|
|
359
|
+
}
|
|
360
|
+
const facePasses = () => rasterPasses(0, st.stock.w, 0, st.stock.h);
|
|
361
|
+
|
|
362
|
+
// square up: setup 1 faces the top and contours the sides down `wall`; setup 2 flips, faces to T and removes any raw lip
|
|
363
|
+
function sqGeom() {
|
|
364
|
+
const { w: W, h: H, t: T } = st.stock;
|
|
365
|
+
const { w, h, t, top, wall, stick } = st.sq;
|
|
366
|
+
const ax = (W - w) / 2, ay = (H - h) / 2;
|
|
367
|
+
const tc = T - top; // thickness after setup 1
|
|
368
|
+
const face2 = tc - t; // taken off in setup 2
|
|
369
|
+
const lip = tc - wall; // raw-width band left after setup 1, measured down from the setup-2 top
|
|
370
|
+
const lipCut = lip > face2 + 1e-6 ? lip + 0.2 : 0;
|
|
371
|
+
return { W, H, T, w, h, t, top, wall, stick, ax, ay, allow: Math.max(ax, ay, 0), tc, face2, lip, lipCut };
|
|
372
|
+
}
|
|
373
|
+
function contourOffsets(allow) {
|
|
374
|
+
const o = [];
|
|
375
|
+
for (let k = allow; k > 1e-6; k -= stepMM()) o.push(k);
|
|
376
|
+
o.push(0);
|
|
377
|
+
return o;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// side facing: (u along the side, d = tool-centre distance in from the raw face) -> stock coordinates
|
|
381
|
+
function sideGeom() {
|
|
382
|
+
const { w: W, h: H } = st.stock;
|
|
383
|
+
const { side, amt, depth } = st.side;
|
|
384
|
+
const alongY = side[0] === 'x';
|
|
385
|
+
const len = alongY ? H : W, span = alongY ? W : H;
|
|
386
|
+
const map = {
|
|
387
|
+
'y-': (u, d) => [u, d], 'y+': (u, d) => [u, H - d],
|
|
388
|
+
'x-': (u, d) => [d, u], 'x+': (u, d) => [W - d, u],
|
|
389
|
+
}[side];
|
|
390
|
+
// clockwise around the part = climb with M3
|
|
391
|
+
const climbPlus = side === 'y+' || side === 'x-';
|
|
392
|
+
const ends = climbPlus ? [-tR() - 2, len + tR() + 2] : [len + tR() + 2, -tR() - 2];
|
|
393
|
+
// equal radial passes no wider than the stepover
|
|
394
|
+
const n = Math.max(1, Math.ceil(amt / stepMM() - 1e-6));
|
|
395
|
+
const ks = Array.from({ length: n }, (_, i) => amt * (i + 1) / n);
|
|
396
|
+
const passes = ks.map(k => ({ a: map(ends[0], k - tR()), b: map(ends[1], k - tR()) }));
|
|
397
|
+
return { W, H, side, amt, depth, len, span, map, passes };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function boltHoles() {
|
|
401
|
+
const { cx, cy, bcd, n, a0 } = st.bolt;
|
|
402
|
+
return Array.from({ length: n }, (_, i) => {
|
|
403
|
+
const a = rad(a0 + i * 360 / n);
|
|
404
|
+
return { x: cx + bcd / 2 * Math.cos(a), y: cy + bcd / 2 * Math.sin(a), a };
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------- plan view ----------
|
|
409
|
+
function centreDims(o, pos, fr, key, cx, cy) {
|
|
410
|
+
const { X, Y, bot, W } = fr;
|
|
411
|
+
const C = [X(cx), Y(cy)];
|
|
412
|
+
const yX = bot + 42, xY = X(W) + 66;
|
|
413
|
+
o.push(g(key + '.cx', ext(X(0), bot + 6, X(0), yX + 6) + ln('ctr', C[0], C[1], C[0], yX + 6) + dl(X(0), yX, C[0], yX)));
|
|
414
|
+
o.push(g(key + '.cy', ext(X(W) + 6, Y(0), xY + 6, Y(0)) + ln('ctr', C[0], C[1], xY + 6, C[1]) + dl(xY, Y(0), xY, C[1])));
|
|
415
|
+
pos[key + '.cx'] = at('plan', (X(0) + C[0]) / 2, yX);
|
|
416
|
+
pos[key + '.cy'] = at('plan', xY, (Y(0) + C[1]) / 2);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const PLAN = {
|
|
420
|
+
square(o, pos, fr) {
|
|
421
|
+
const { X, Y, s } = fr;
|
|
422
|
+
const q = sqGeom(), one = st.sq.setup === 1;
|
|
423
|
+
const x0 = X(q.ax), x1 = X(q.ax + q.w), y0 = Y(q.ay + q.h), y1 = Y(q.ay);
|
|
424
|
+
const cut = one || q.lipCut;
|
|
425
|
+
if (cut) o.push(`<path class="allow" d="M${n2(X(0))},${n2(Y(q.H))} H${n2(X(q.W))} V${n2(Y(0))} H${n2(X(0))}Z M${n2(x0)},${n2(y0)} H${n2(x1)} V${n2(y1)} H${n2(x0)}Z"/>`);
|
|
426
|
+
o.push(g('sq.w', `<rect class="fin" x="${n2(x0)}" y="${n2(y0)}" width="${n2(x1 - x0)}" height="${n2(y1 - y0)}"/>`));
|
|
427
|
+
if (one ? q.top > 0 : q.face2 > 0) {
|
|
428
|
+
const { ys, xa, xb } = rasterPasses(0, q.W, 0, q.H);
|
|
429
|
+
const pts = ys.flatMap((y, i) => i % 2 ? [[xb, y], [xa, y]] : [[xa, y], [xb, y]]);
|
|
430
|
+
o.push(`<polyline class="tp-faint" points="${pts.map(([x, y]) => `${n2(X(x))},${n2(Y(y))}`).join(' ')}"/>`);
|
|
431
|
+
}
|
|
432
|
+
if (cut) {
|
|
433
|
+
const offs = contourOffsets(q.allow);
|
|
434
|
+
for (const off of offs) {
|
|
435
|
+
const d = tR() + off;
|
|
436
|
+
o.push(`<rect class="tp" x="${n2(X(q.ax - d))}" y="${n2(Y(q.ay + q.h + d))}" width="${n2((q.w + 2 * d) * s)}" height="${n2((q.h + 2 * d) * s)}" rx="${n2(d * s)}"/>`);
|
|
437
|
+
}
|
|
438
|
+
o.push(`<circle class="toolmark" cx="${n2((x0 + x1) / 2)}" cy="${n2(Y(q.ay - tR() - offs[0]))}" r="${n2(tR() * s)}"/>`);
|
|
439
|
+
}
|
|
440
|
+
const yd = y0 + 24, xd = x1 - 60;
|
|
441
|
+
o.push(g('sq.w', dl(x0, yd, x1, yd)));
|
|
442
|
+
pos['sq.w'] = at('plan', (x0 + x1) / 2, yd);
|
|
443
|
+
o.push(g('sq.h', dl(xd, y0, xd, y1)));
|
|
444
|
+
pos['sq.h'] = at('plan', xd, (y0 + y1) / 2);
|
|
445
|
+
if (q.ax > 0) {
|
|
446
|
+
const ya = (y0 + y1) / 2 + 36;
|
|
447
|
+
o.push(dl(X(0), ya, x0, ya) + `<text class="dimtxt" x="${n2(x0 + 8)}" y="${n2(ya + 4)}">${fmt(q.ax)} per side</text>`);
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
side(o, pos, fr) {
|
|
451
|
+
const { X, Y, s } = fr;
|
|
452
|
+
const q = sideGeom();
|
|
453
|
+
const P = ([x, y]) => [X(x), Y(y)];
|
|
454
|
+
const [r0, r1] = [P(q.map(0, 0)), P(q.map(q.len, q.amt))];
|
|
455
|
+
const bx = Math.min(r0[0], r1[0]), by = Math.min(r0[1], r1[1]);
|
|
456
|
+
o.push(g('side.amt', `<rect class="allow" x="${n2(bx)}" y="${n2(by)}" width="${n2(Math.abs(r1[0] - r0[0]))}" height="${n2(Math.abs(r1[1] - r0[1]))}"/>`));
|
|
457
|
+
const [f0, f1] = [P(q.map(0, q.amt)), P(q.map(q.len, q.amt))];
|
|
458
|
+
o.push(ln('edge', f0[0], f0[1], f1[0], f1[1]));
|
|
459
|
+
for (const p of q.passes) {
|
|
460
|
+
const [a, b] = [P(p.a), P(p.b)];
|
|
461
|
+
o.push(`<line class="tp" x1="${n2(a[0])}" y1="${n2(a[1])}" x2="${n2(b[0])}" y2="${n2(b[1])}" marker-end="url(#ap)"/>`);
|
|
462
|
+
}
|
|
463
|
+
const first = P(q.passes[0].a);
|
|
464
|
+
o.push(`<circle class="toolmark" cx="${n2(first[0])}" cy="${n2(first[1])}" r="${n2(tR() * s)}"/>`);
|
|
465
|
+
// remove: dimension across the band at a quarter of the side, box just inside the finished face
|
|
466
|
+
const [d0, d1] = [P(q.map(q.len * 0.25, 0)), P(q.map(q.len * 0.25, q.amt))];
|
|
467
|
+
o.push(g('side.amt', dl(d0[0], d0[1], d1[0], d1[1])));
|
|
468
|
+
const inward = q.side[0] === 'y' ? [0, q.side === 'y-' ? -1 : 1] : [q.side === 'x-' ? 1 : -1, 0];
|
|
469
|
+
pos['side.amt'] = at('plan', d1[0] + inward[0] * 62, d1[1] + inward[1] * 20);
|
|
470
|
+
},
|
|
471
|
+
face(o, pos, fr) {
|
|
472
|
+
const { X, Y, s } = fr;
|
|
473
|
+
const { ys, xa, xb } = facePasses();
|
|
474
|
+
const pts = ys.flatMap((y, i) => i % 2 ? [[xb, y], [xa, y]] : [[xa, y], [xb, y]]);
|
|
475
|
+
o.push(`<polyline class="tp" points="${pts.map(([x, y]) => `${n2(X(x))},${n2(Y(y))}`).join(' ')}"/>`);
|
|
476
|
+
o.push(`<circle class="toolmark" cx="${n2(X(xa))}" cy="${n2(Y(ys[0]))}" r="${n2(tR() * s)}"/>`);
|
|
477
|
+
},
|
|
478
|
+
rect(o, pos, fr) {
|
|
479
|
+
const { X, Y, s } = fr;
|
|
480
|
+
const { cx, cy, w, l, r } = st.rect;
|
|
481
|
+
const x0 = X(cx - w / 2), x1 = X(cx + w / 2), y0 = Y(cy + l / 2), y1 = Y(cy - l / 2);
|
|
482
|
+
const rx = Math.min(r, w / 2, l / 2) * s;
|
|
483
|
+
o.push(g('rect.r', `<rect class="feat" x="${n2(x0)}" y="${n2(y0)}" width="${n2(x1 - x0)}" height="${n2(y1 - y0)}" rx="${n2(rx)}"/>`));
|
|
484
|
+
for (const lp of rectLoops()) {
|
|
485
|
+
o.push(`<rect class="tp" x="${n2(X(cx - lp.hw))}" y="${n2(Y(cy + lp.hh))}" width="${n2(2 * lp.hw * s)}" height="${n2(2 * lp.hh * s)}" rx="${n2(lp.rc * s)}"/>`);
|
|
486
|
+
}
|
|
487
|
+
const last = rectLoops().at(-1);
|
|
488
|
+
if (last) o.push(`<circle class="toolmark" cx="${n2(X(cx))}" cy="${n2(Y(cy - last.hh))}" r="${n2(tR() * s)}"/>`);
|
|
489
|
+
o.push(cross(X(cx), Y(cy)));
|
|
490
|
+
centreDims(o, pos, fr, 'rect', cx, cy);
|
|
491
|
+
const yd = y0 + 22, xd = x1 - 58;
|
|
492
|
+
o.push(g('rect.w', dl(x0, yd, x1, yd)));
|
|
493
|
+
pos['rect.w'] = at('plan', (x0 + x1) / 2, yd);
|
|
494
|
+
o.push(g('rect.l', dl(xd, y0, xd, y1)));
|
|
495
|
+
pos['rect.l'] = at('plan', xd, (y0 + y1) / 2);
|
|
496
|
+
const cc = [x0 + rx, y0 + rx], k = Math.SQRT1_2;
|
|
497
|
+
o.push(g('rect.r', dl(cc[0] + 18 * k, cc[1] + 18 * k, cc[0] - rx * k, cc[1] - rx * k, 'end')));
|
|
498
|
+
pos['rect.r'] = at('plan', cc[0] + 18 * k + 40, cc[1] + 18 * k + 14);
|
|
499
|
+
},
|
|
500
|
+
circ(o, pos, fr) {
|
|
501
|
+
const { X, Y, s } = fr;
|
|
502
|
+
const { cx, cy, d } = st.circ;
|
|
503
|
+
const C = [X(cx), Y(cy)], r = d / 2 * s;
|
|
504
|
+
o.push(g('circ.d', `<circle class="feat" cx="${n2(C[0])}" cy="${n2(C[1])}" r="${n2(r)}"/>`));
|
|
505
|
+
const rings = circRings();
|
|
506
|
+
for (const rr of rings) o.push(`<circle class="tp" cx="${n2(C[0])}" cy="${n2(C[1])}" r="${n2(rr * s)}"/>`);
|
|
507
|
+
if (rings.length) o.push(`<circle class="toolmark" cx="${n2(X(cx + rings.at(-1)))}" cy="${n2(C[1])}" r="${n2(tR() * s)}"/>`);
|
|
508
|
+
o.push(cross(C[0], C[1]));
|
|
509
|
+
centreDims(o, pos, fr, 'circ', cx, cy);
|
|
510
|
+
// diameter dimension drawn across the circle; the value sits inside when it fits
|
|
511
|
+
const u = [Math.cos(rad(35)), -Math.sin(rad(35))];
|
|
512
|
+
o.push(g('circ.d', dl(C[0] - u[0] * r, C[1] - u[1] * r, C[0] + u[0] * r, C[1] + u[1] * r)));
|
|
513
|
+
if (r >= 64) {
|
|
514
|
+
pos['circ.d'] = at('plan', C[0] + u[0] * r * 0.5, C[1] + u[1] * r * 0.5);
|
|
515
|
+
} else {
|
|
516
|
+
const p = [C[0] + u[0] * (r + 30), C[1] + u[1] * (r + 30)];
|
|
517
|
+
o.push(g('circ.d', ln('dl', C[0] + u[0] * r, C[1] + u[1] * r, p[0], p[1]) + ln('dl', p[0], p[1], p[0] + 16, p[1])));
|
|
518
|
+
pos['circ.d'] = at('plan', p[0] + 52, p[1]);
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
bolt(o, pos, fr) {
|
|
522
|
+
const { X, Y, s } = fr;
|
|
523
|
+
const { cx, cy, bcd, n, a0, hd } = st.bolt;
|
|
524
|
+
const C = [X(cx), Y(cy)], R = bcd / 2 * s, hr = hd / 2 * s;
|
|
525
|
+
o.push(g('bolt.bcd', `<circle class="ctr" cx="${n2(C[0])}" cy="${n2(C[1])}" r="${n2(R)}"/>`));
|
|
526
|
+
const holes = boltHoles();
|
|
527
|
+
o.push(`<polyline class="tp-rapid" points="${holes.map(h => `${n2(X(h.x))},${n2(Y(h.y))}`).join(' ')}"/>`);
|
|
528
|
+
holes.forEach((h, i) => {
|
|
529
|
+
o.push((i ? '' : `<g class="dg${active === 'bolt.hd' ? ' on' : ''}">`) + `<circle class="feat" cx="${n2(X(h.x))}" cy="${n2(Y(h.y))}" r="${n2(hr)}"/>` + (i ? '' : '</g>'));
|
|
530
|
+
o.push(cross(X(h.x), Y(h.y), hr + 4));
|
|
531
|
+
});
|
|
532
|
+
o.push(`<circle class="toolmark" cx="${n2(X(holes[0].x))}" cy="${n2(Y(holes[0].y))}" r="${n2(hr)}"/>`);
|
|
533
|
+
o.push(cross(C[0], C[1]));
|
|
534
|
+
centreDims(o, pos, fr, 'bolt', cx, cy);
|
|
535
|
+
// bolt circle diameter, drawn between holes
|
|
536
|
+
const tb = rad(a0 + 180 / n + 180), u = [Math.cos(tb), -Math.sin(tb)];
|
|
537
|
+
o.push(g('bolt.bcd', dl(C[0] - u[0] * R, C[1] - u[1] * R, C[0] + u[0] * R, C[1] + u[1] * R)));
|
|
538
|
+
pos['bolt.bcd'] = at('plan', C[0] + u[0] * R * 0.55, C[1] + u[1] * R * 0.55);
|
|
539
|
+
pos['bolt.n'] = at('plan', C[0], C[1] - R * 0.45);
|
|
540
|
+
// start angle from +X
|
|
541
|
+
const ra = R * 0.36;
|
|
542
|
+
let ang = ln('ctr', C[0], C[1], C[0] + R + 22, C[1]);
|
|
543
|
+
const a = ((a0 % 360) + 360) % 360;
|
|
544
|
+
if (a > 0.5) {
|
|
545
|
+
const ex = C[0] + ra * Math.cos(rad(a)), ey = C[1] - ra * Math.sin(rad(a));
|
|
546
|
+
ang += `<path class="dl" d="M${n2(C[0] + ra)},${n2(C[1])} A${n2(ra)},${n2(ra)} 0 ${a > 180 ? 1 : 0} 0 ${n2(ex)},${n2(ey)}" marker-end="url(#a)"/>`;
|
|
547
|
+
}
|
|
548
|
+
o.push(g('bolt.a0', ang));
|
|
549
|
+
pos['bolt.a0'] = at('plan', C[0] + (ra + 30) * Math.cos(rad(a / 2)), C[1] - (ra + 10) * Math.sin(rad(a / 2)) + 26);
|
|
550
|
+
// drill diameter, leader off the first hole
|
|
551
|
+
const h0 = holes[0], v = [Math.cos(h0.a), -Math.sin(h0.a)], H0 = [X(h0.x), Y(h0.y)];
|
|
552
|
+
const p = [H0[0] + v[0] * (hr + 26), H0[1] + v[1] * (hr + 26) - 18];
|
|
553
|
+
const side = v[0] >= 0 ? 1 : -1;
|
|
554
|
+
o.push(g('bolt.hd', ln('dl', p[0], p[1], H0[0] + v[0] * hr, H0[1] + v[1] * hr, 'marker-end="url(#a)"') + ln('dl', p[0], p[1], p[0] + side * 14, p[1])));
|
|
555
|
+
pos['bolt.hd'] = at('plan', p[0] + side * 52, p[1]);
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
function drawPlan(o, pos) {
|
|
560
|
+
const fr = planFrame();
|
|
561
|
+
const { X, Y, ox, top, bot, s, W, H } = fr;
|
|
562
|
+
const bare = st.op === 'square' && st.sq.setup === 2 && !sqGeom().lipCut;
|
|
563
|
+
o.push(`<rect class="${bare ? 'raw' : 'stock'}" x="${n2(ox)}" y="${n2(top)}" width="${n2(W * s)}" height="${n2(H * s)}"/>`);
|
|
564
|
+
PLAN[st.op](o, pos, fr);
|
|
565
|
+
// origin: setup 2 of square-up probes the finished corner
|
|
566
|
+
const sq2 = st.op === 'square' && st.sq.setup === 2;
|
|
567
|
+
const gx = sq2 ? X(sqGeom().ax) : ox, gy = sq2 ? Y(sqGeom().ay) : bot;
|
|
568
|
+
o.push(`<line class="dl" x1="${n2(gx)}" y1="${n2(gy)}" x2="${n2(gx + 30)}" y2="${n2(gy)}" marker-end="url(#ap)" style="stroke:var(--path)"/>`);
|
|
569
|
+
o.push(`<line class="dl" x1="${n2(gx)}" y1="${n2(gy)}" x2="${n2(gx)}" y2="${n2(gy - 30)}" marker-end="url(#ap)" style="stroke:var(--path)"/>`);
|
|
570
|
+
o.push(`<text class="lbl" x="${n2(gx + 34)}" y="${n2(gy - 4)}">X</text><text class="lbl" x="${n2(gx + 5)}" y="${n2(gy - 34)}">Y</text>`);
|
|
571
|
+
// stock size
|
|
572
|
+
const yW = top - 36, xH = ox - 66;
|
|
573
|
+
o.push(g('stock.w', ext(X(0), top - 6, X(0), yW - 6) + ext(X(W), top - 6, X(W), yW - 6) + dl(X(0), yW, X(W), yW)));
|
|
574
|
+
o.push(g('stock.h', ext(ox - 6, top, xH - 6, top) + ext(ox - 6, bot, xH - 6, bot) + dl(xH, bot, xH, top)));
|
|
575
|
+
pos['stock.w'] = at('plan', (X(0) + X(W)) / 2, yW);
|
|
576
|
+
pos['stock.h'] = at('plan', xH, (top + bot) / 2);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ---------- section view ----------
|
|
580
|
+
const DEPTH = { face: 'face.depth', rect: 'rect.depth', circ: 'circ.depth', bolt: 'bolt.depth' };
|
|
581
|
+
|
|
582
|
+
function drawSecSide(o, pos) {
|
|
583
|
+
const { ox, s } = planFrame();
|
|
584
|
+
const q = sideGeom(), T = st.stock.t;
|
|
585
|
+
const zt = 76, zs = Math.min(132 / T, s * 4);
|
|
586
|
+
const Z = z => zt - z * zs, zb = Z(-T);
|
|
587
|
+
// section across the faced side, drawn to the plan's scale; the faced side is on its own side of the sheet
|
|
588
|
+
const minus = q.side.endsWith('-');
|
|
589
|
+
const xl = ox, xr = ox + q.span * s;
|
|
590
|
+
const fx = minus ? xl : xr, ix = minus ? xl + q.amt * s : xr - q.amt * s;
|
|
591
|
+
const zd = Z(-Math.min(q.depth, T));
|
|
592
|
+
o.push(`<rect class="sect" x="${n2(xl)}" y="${zt}" width="${n2(xr - xl)}" height="${n2(T * zs)}"/>`);
|
|
593
|
+
o.push(`<rect class="void" x="${n2(Math.min(fx, ix) - 1)}" y="${zt - 1}" width="${n2(Math.abs(ix - fx) + 2)}" height="${n2(zd - zt + 1)}"/>`);
|
|
594
|
+
o.push(`<path class="edge" d="M${n2(fx)},${n2(zd)} H${n2(ix)} V${zt}"/>`);
|
|
595
|
+
o.push(`<rect class="raw" x="${n2(Math.min(fx, ix))}" y="${zt}" width="${n2(Math.abs(ix - fx))}" height="${n2(zd - zt)}"/>`);
|
|
596
|
+
for (const z of levels(Math.min(q.depth, T), st.stepdown).slice(0, -1)) o.push(ln('layer', fx, Z(z), ix, Z(z)));
|
|
597
|
+
|
|
598
|
+
const dir = minus ? -1 : 1;
|
|
599
|
+
const xc = fx + dir * 66, edge = fx + dir * 6;
|
|
600
|
+
const ys = Math.max(Z(st.safeZ), 16);
|
|
601
|
+
o.push(ln('safe', xl - 26, ys, xr + 26, ys));
|
|
602
|
+
o.push(g('safeZ', ext(fx + dir * 30, ys, xc + dir * 6, ys) + ext(edge, zt, xc + dir * 6, zt) + dl(xc, ys, xc, zt)));
|
|
603
|
+
pos['safeZ'] = at('sec', xc, Math.min((ys + zt) / 2, zt - 17));
|
|
604
|
+
o.push(g('side.depth', ext(fx + dir * 2, zd, xc + dir * 6, zd) + dl(xc, zt, xc, zd)));
|
|
605
|
+
pos['side.depth'] = at('sec', xc, Math.max((zt + zd) / 2, zt + 17));
|
|
606
|
+
const xs = ix - dir * 24, sdz = Z(-Math.min(st.stepdown, q.depth));
|
|
607
|
+
o.push(g('stepdown', dl(xs, zt, xs, sdz)));
|
|
608
|
+
pos['stepdown'] = at('sec', xs - dir * 52, zt + 17);
|
|
609
|
+
const ox2 = minus ? xr : xl, xt = ox2 - dir * 66;
|
|
610
|
+
o.push(g('stock.t', ext(ox2 - dir * 6, zt, xt - dir * 6, zt) + ext(ox2 - dir * 6, zb, xt - dir * 6, zb) + dl(xt, zt, xt, zb)));
|
|
611
|
+
pos['stock.t'] = at('sec', xt, (zt + zb) / 2);
|
|
612
|
+
o.push(`<text class="lbl" text-anchor="${minus ? 'start' : 'end'}" x="${n2(minus ? xr + 34 : xl - 34)}" y="${n2(ys - 6)}">clearance</text>`);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function drawSecSquare(o, pos) {
|
|
616
|
+
const { X, ox, s } = planFrame();
|
|
617
|
+
const q = sqGeom(), one = st.sq.setup === 1;
|
|
618
|
+
const zt = 76, zs = Math.min(132 / q.T, s * 4);
|
|
619
|
+
const Z = z => zt - z * zs;
|
|
620
|
+
const xl = X(0), xr = X(q.W), xa = X(q.ax), xb = X(q.ax + q.w);
|
|
621
|
+
const zb = Z(-(one ? q.T : q.tc));
|
|
622
|
+
const lipZ = Z(-Math.max(q.lip, 0));
|
|
623
|
+
const body = one || q.lip <= 0
|
|
624
|
+
? `M${n2(one ? xl : xa)},${zt} H${n2(one ? xr : xb)} V${n2(zb)} H${n2(one ? xl : xa)}Z`
|
|
625
|
+
: `M${n2(xl)},${zt} H${n2(xr)} V${n2(lipZ)} H${n2(xb)} V${n2(zb)} H${n2(xa)} V${n2(lipZ)} H${n2(xl)}Z`;
|
|
626
|
+
o.push(`<path class="sect" d="${body}"/>`);
|
|
627
|
+
|
|
628
|
+
// material removed in this setup
|
|
629
|
+
const band = (a, b, d) => `<rect class="void" x="${n2(a)}" y="${zt - 2}" width="${n2(b - a)}" height="${n2(d * zs + 2)}"/>`;
|
|
630
|
+
const top = one ? q.top : Math.max(q.face2, 0);
|
|
631
|
+
const side = one ? q.wall : q.lipCut;
|
|
632
|
+
const wide = one || q.lip > 0;
|
|
633
|
+
if (top > 0) o.push(band(wide ? xl : xa, wide ? xr : xb, top));
|
|
634
|
+
if (side) o.push(band(xl, xa, side), band(xb, xr, side));
|
|
635
|
+
o.push(`<path class="edge" d="${side ? `M${n2(xl)},${n2(Z(-side))} H${n2(xa)} V${n2(Z(-top))} H${n2(xb)} V${n2(Z(-side))} H${n2(xr)}` : `M${n2(wide ? xl : xa)},${n2(Z(-top))} H${n2(wide ? xr : xb)}`}"/>`);
|
|
636
|
+
o.push(`<path class="raw" d="${body}"/>`);
|
|
637
|
+
|
|
638
|
+
// vise jaws: raw sides in setup 1, finished sides in setup 2
|
|
639
|
+
const jt = Z(-q.stick), jb = Math.max(zb, jt) + 16;
|
|
640
|
+
const gl = one ? xl : xa, gr = one ? xr : xb;
|
|
641
|
+
o.push(`<rect class="jaw" x="${n2(gl - 26)}" y="${n2(jt)}" width="26" height="${n2(jb - jt)}"/><rect class="jaw" x="${n2(gr)}" y="${n2(jt)}" width="26" height="${n2(jb - jt)}"/>`);
|
|
642
|
+
|
|
643
|
+
// clearance and the top cut on the left chain
|
|
644
|
+
const ys = Math.max(Z(st.safeZ), 16), xc = ox - 66, xs = ox - 128;
|
|
645
|
+
o.push(ln('safe', xs - 20, ys, xr + 26, ys));
|
|
646
|
+
o.push(g('safeZ', ext(ox - 30, ys, xc - 6, ys) + ext(ox - 30, zt, xc - 6, zt) + dl(xc, ys, xc, zt)));
|
|
647
|
+
pos['safeZ'] = at('sec', xc, Math.min((ys + zt) / 2, zt - 17));
|
|
648
|
+
const topDim = ext(xl - 4, Z(-top), xc - 6, Z(-top)) + dl(xc, zt, xc, Z(-top));
|
|
649
|
+
if (one) {
|
|
650
|
+
o.push(g('sq.top', topDim));
|
|
651
|
+
pos['sq.top'] = at('sec', xc, Math.max((zt + Z(-top)) / 2, zt + 17));
|
|
652
|
+
} else {
|
|
653
|
+
o.push(topDim + `<text class="dimtxt" text-anchor="end" x="${n2(xc - 8)}" y="${n2(Math.max((zt + Z(-top)) / 2, zt + 17) + 4)}">face ${fmt(top)}</text>`);
|
|
654
|
+
}
|
|
655
|
+
// stick-out above the jaws
|
|
656
|
+
o.push(g('sq.stick', ext(gl - 30, jt, xs - 6, jt) + ext(xc - 10, zt, xs - 6, zt) + dl(xs, zt, xs, jt)));
|
|
657
|
+
pos['sq.stick'] = at('sec', xs, (zt + jt) / 2 + 10);
|
|
658
|
+
// side cut, measured in the right-hand band
|
|
659
|
+
if (side) {
|
|
660
|
+
const xw = (xb + xr) / 2, mid = (zt + Z(-side)) / 2;
|
|
661
|
+
if (one) {
|
|
662
|
+
o.push(g('sq.wall', dl(xw, zt, xw, Z(-side))));
|
|
663
|
+
pos['sq.wall'] = at('sec', xb - 58, mid);
|
|
664
|
+
} else {
|
|
665
|
+
o.push(dl(xw, zt, xw, Z(-side)) + `<text class="dimtxt" text-anchor="end" x="${n2(xb - 8)}" y="${n2(Z(-side) + 16)}">lip ${fmt(side)}</text>`);
|
|
666
|
+
}
|
|
667
|
+
const xm = (xl + xa) / 2, sdz = Z(-Math.min(st.stepdown, side));
|
|
668
|
+
o.push(g('stepdown', dl(xm, zt, xm, sdz)));
|
|
669
|
+
pos['stepdown'] = at('sec', xa + 58, zt + 17);
|
|
670
|
+
}
|
|
671
|
+
// thickness on the right: raw in setup 1, finished in setup 2
|
|
672
|
+
const xt = xr + 70;
|
|
673
|
+
if (one) {
|
|
674
|
+
o.push(g('stock.t', ext(xr + 30, zt, xt + 6, zt) + ext(xr + 30, zb, xt + 6, zb) + dl(xt, zt, xt, zb)));
|
|
675
|
+
pos['stock.t'] = at('sec', xt, (zt + zb) / 2);
|
|
676
|
+
} else {
|
|
677
|
+
o.push(g('sq.t', ext(xb + 30, Z(-top), xt + 6, Z(-top)) + ext(xb + 30, zb, xt + 6, zb) + dl(xt, Z(-top), xt, zb)));
|
|
678
|
+
pos['sq.t'] = at('sec', xt, (Z(-top) + zb) / 2);
|
|
679
|
+
}
|
|
680
|
+
o.push(`<text class="lbl" x="${n2(xr + 34)}" y="${n2(ys - 6)}">clearance</text><text class="lbl" x="${n2(xr + 34)}" y="${zt - 6}">Z0</text>`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function drawSec(o, pos) {
|
|
684
|
+
if (st.op === 'square') return drawSecSquare(o, pos);
|
|
685
|
+
if (st.op === 'side') return drawSecSide(o, pos);
|
|
686
|
+
const { X, ox, W, s } = planFrame();
|
|
687
|
+
const T = st.stock.t;
|
|
688
|
+
const zt = 76, zs = Math.min(132 / T, s * 4);
|
|
689
|
+
const Z = z => zt - z * zs, zb = Z(-T);
|
|
690
|
+
const depth = get(DEPTH[st.op]);
|
|
691
|
+
o.push(`<rect class="sect" x="${n2(ox)}" y="${zt}" width="${n2(W * s)}" height="${n2(T * zs)}"/>`);
|
|
692
|
+
|
|
693
|
+
let first = null; // the cut that carries the in-feature dimensions
|
|
694
|
+
if (st.op === 'bolt') {
|
|
695
|
+
const tip = 0.3 * st.bolt.hd;
|
|
696
|
+
for (const h of boltHoles()) {
|
|
697
|
+
const a = X(h.x - st.bolt.hd / 2), b = X(h.x + st.bolt.hd / 2), m = X(h.x);
|
|
698
|
+
const wallZ = Z(-Math.max(depth - tip, 0));
|
|
699
|
+
o.push(`<path class="void" d="M${n2(a)},${zt - 1} V${n2(wallZ)} L${n2(m)},${n2(Z(-depth))} L${n2(b)},${n2(wallZ)} V${zt - 1}Z"/>`);
|
|
700
|
+
o.push(`<path class="edge" d="M${n2(a)},${zt} V${n2(wallZ)} L${n2(m)},${n2(Z(-depth))} L${n2(b)},${n2(wallZ)} V${zt}"/>`);
|
|
701
|
+
}
|
|
702
|
+
const h0 = boltHoles().reduce((p, h) => (h.x > p.x ? h : p));
|
|
703
|
+
first = { a: X(h0.x - st.bolt.hd / 2), b: X(h0.x + st.bolt.hd / 2) };
|
|
704
|
+
} else {
|
|
705
|
+
const iv = st.op === 'face' ? [0, W] : st.op === 'rect' ? [st.rect.cx - st.rect.w / 2, st.rect.cx + st.rect.w / 2] : [st.circ.cx - st.circ.d / 2, st.circ.cx + st.circ.d / 2];
|
|
706
|
+
const a = X(Math.max(iv[0], 0)), b = X(Math.min(iv[1], W));
|
|
707
|
+
o.push(`<rect class="void" x="${n2(a)}" y="${zt - 2}" width="${n2(b - a)}" height="${n2(depth * zs + 2)}"/>`);
|
|
708
|
+
o.push(`<path class="edge" d="M${n2(a)},${zt} V${n2(Z(-depth))} H${n2(b)} V${zt}"/>`);
|
|
709
|
+
first = { a, b };
|
|
710
|
+
if (st.op !== 'face') {
|
|
711
|
+
for (const z of levels(depth, st.stepdown).slice(0, -1)) o.push(ln('layer', a + 3, Z(z), b - 3, Z(z)));
|
|
712
|
+
const xm = (a + b) / 2, sdz = Z(-Math.min(st.stepdown, depth));
|
|
713
|
+
o.push(g('stepdown', dl(xm, zt, xm, sdz)));
|
|
714
|
+
pos['stepdown'] = at('sec', xm + 62, Math.max((zt + sdz) / 2, zt + 15));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// clearance plane
|
|
719
|
+
const ys = Math.max(Z(st.safeZ), 16);
|
|
720
|
+
o.push(ln('safe', ox - 26, ys, X(W) + 26, ys));
|
|
721
|
+
// left chain: clearance above, depth below
|
|
722
|
+
const xc = ox - 66;
|
|
723
|
+
o.push(g('safeZ', ext(ox - 30, ys, xc - 6, ys) + ext(ox - 6, zt, xc - 6, zt) + dl(xc, ys, xc, zt)));
|
|
724
|
+
o.push(g(DEPTH[st.op], ext(first.a - 4, Z(-depth), xc - 6, Z(-depth)) + dl(xc, zt, xc, Z(-depth))));
|
|
725
|
+
const ySafe = (ys + zt) / 2;
|
|
726
|
+
pos['safeZ'] = at('sec', xc, Math.min(ySafe, zt - 17));
|
|
727
|
+
pos[DEPTH[st.op]] = at('sec', xc, Math.max((zt + Z(-depth)) / 2, zt + 17));
|
|
728
|
+
// thickness on the right
|
|
729
|
+
const xt = X(W) + 66;
|
|
730
|
+
o.push(g('stock.t', ext(X(W) + 6, zt, xt + 6, zt) + ext(X(W) + 6, zb, xt + 6, zb) + dl(xt, zt, xt, zb)));
|
|
731
|
+
pos['stock.t'] = at('sec', xt, (zt + zb) / 2);
|
|
732
|
+
// peck bracket beside the right-most hole
|
|
733
|
+
if (st.op === 'bolt') {
|
|
734
|
+
const xp = first.b + 12, pz = Z(-Math.min(st.bolt.peck, depth));
|
|
735
|
+
o.push(g('bolt.peck', ext(first.b + 2, zt, xp + 6, zt) + dl(xp, zt, xp, pz)));
|
|
736
|
+
pos['bolt.peck'] = at('sec', xp + 54, Math.max((zt + pz) / 2, zt + 15));
|
|
737
|
+
}
|
|
738
|
+
o.push(`<text class="lbl" x="${n2(X(W) + 30)}" y="${n2(ys - 6)}">clearance</text><text class="lbl" x="${n2(X(W) + 30)}" y="${zt - 6}">Z0</text>`);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ---------- tool views ----------
|
|
742
|
+
function drawTool(o, pos) {
|
|
743
|
+
const drill = st.op === 'bolt';
|
|
744
|
+
o.push(`<rect class="feat" x="132" y="12" width="36" height="62"/>`);
|
|
745
|
+
if (drill) {
|
|
746
|
+
o.push(`<path class="feat" d="M137,74 V132 L150,148 L163,132 V74Z"/><path class="ctr" d="M137,90 L163,104 M137,108 L163,122"/>`);
|
|
747
|
+
} else {
|
|
748
|
+
o.push(`<path class="feat" d="M136,74 V142 H164 V74Z"/><path class="ctr" d="M136,84 L164,100 M136,104 L164,120 M136,124 L164,140"/>`);
|
|
749
|
+
o.push(g('tool.d', ext(136, 146, 136, 166) + ext(164, 146, 164, 166) + dl(136, 160, 164, 160) + ln('dl', 164, 160, 186, 160)));
|
|
750
|
+
pos['tool.d'] = at('tool', 232, 160);
|
|
751
|
+
o.push(g('tool.feed', `<line class="dl" x1="184" y1="122" x2="232" y2="122" marker-end="url(#a)"/>`));
|
|
752
|
+
pos['tool.feed'] = at('tool', 322, 122);
|
|
753
|
+
}
|
|
754
|
+
o.push(g('tool.rpm', `<path class="dl" d="M124,28 A28,9 0 1 0 176,28" marker-end="url(#a)"/>`));
|
|
755
|
+
pos['tool.rpm'] = at('tool', 58, 30);
|
|
756
|
+
o.push(g('tool.plunge', `<line class="dl" x1="192" y1="36" x2="192" y2="84" marker-end="url(#a)"/>`));
|
|
757
|
+
pos['tool.plunge'] = at('tool', 292, 60);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function drawStep(o, pos) {
|
|
761
|
+
const r = 38, x1 = 62, x2 = x1 + 2 * r * st.tool.step / 100, y = 92;
|
|
762
|
+
o.push(`<circle class="toolmark" cx="${x1}" cy="${y}" r="${r}"/><circle class="tp" cx="${n2(x2)}" cy="${y}" r="${r}"/>`);
|
|
763
|
+
o.push(cross(x1, y, 5) + cross(x2, y, 5));
|
|
764
|
+
o.push(g('tool.step', ext(x1, y - 8, x1, 30) + ext(x2, y - 8, x2, 30) + dl(x1, 36, x2, 36)));
|
|
765
|
+
pos['tool.step'] = at('step', Math.max((x1 + x2) / 2, 80), 36);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// ---------- checks ----------
|
|
769
|
+
function check() {
|
|
770
|
+
const I = [];
|
|
771
|
+
const add = (lv, text, ...f) => I.push({ lv, text, f });
|
|
772
|
+
const { w: W, h: H, t: T } = st.stock;
|
|
773
|
+
const out = (x0, x1, y0, y1) => x0 < 0 || y0 < 0 || x1 > W || y1 > H;
|
|
774
|
+
if (st.op === 'circ') {
|
|
775
|
+
const { cx, cy, d, depth } = st.circ;
|
|
776
|
+
if (d <= st.tool.d) add('error', 'Pocket Ø must be larger than tool Ø.', 'circ.d', 'tool.d');
|
|
777
|
+
if (out(cx - d / 2, cx + d / 2, cy - d / 2, cy + d / 2)) add('warn', 'Pocket breaks out of the stock.', 'circ.cx', 'circ.cy', 'circ.d');
|
|
778
|
+
if (depth >= T) add('error', 'Depth goes through the stock.', 'circ.depth', 'stock.t');
|
|
779
|
+
}
|
|
780
|
+
if (st.op === 'rect') {
|
|
781
|
+
const { cx, cy, w, l, r, depth } = st.rect;
|
|
782
|
+
if (Math.min(w, l) <= st.tool.d) add('error', 'Pocket W and L must exceed tool Ø.', 'rect.w', 'rect.l', 'tool.d');
|
|
783
|
+
if (r > Math.min(w, l) / 2) add('error', 'Corner R is larger than half the pocket.', 'rect.r');
|
|
784
|
+
else if (r < tR()) add('warn', `Corner R below tool radius: corners will come out R${fmt(tR())}.`, 'rect.r', 'tool.d');
|
|
785
|
+
else if (r < tR() * 1.15) add('warn', 'Corner R is close to tool radius: the tool will rub in the corners.', 'rect.r', 'tool.d');
|
|
786
|
+
if (out(cx - w / 2, cx + w / 2, cy - l / 2, cy + l / 2)) add('warn', 'Pocket breaks out of the stock.', 'rect.cx', 'rect.cy', 'rect.w', 'rect.l');
|
|
787
|
+
if (depth >= T) add('error', 'Depth goes through the stock.', 'rect.depth', 'stock.t');
|
|
788
|
+
}
|
|
789
|
+
if (st.op === 'face' && st.face.depth > 1) add('warn', 'More than 1 mm in a single facing pass.', 'face.depth');
|
|
790
|
+
if (st.op === 'square') {
|
|
791
|
+
const q = sqGeom(), one = st.sq.setup === 1;
|
|
792
|
+
if (q.w > q.W || q.h > q.H) add('error', 'Finished size is larger than the raw stock.', 'sq.w', 'sq.h', 'stock.w', 'stock.h');
|
|
793
|
+
else if (q.allow < 0.2) add('warn', 'Under 0.2 mm per side to clean up.', 'sq.w', 'sq.h');
|
|
794
|
+
if (q.t > q.tc) add('error', 'Finished T is thicker than the stock left after setup 1.', 'sq.t', 'sq.top', 'stock.t');
|
|
795
|
+
if (q.stick >= q.T) add('error', 'Height above the jaws is more than the stock thickness.', 'sq.stick', 'stock.t');
|
|
796
|
+
if (one && q.wall > q.stick - 0.5) add('error', 'Side cut reaches the vise jaws: cut shallower or raise the stock.', 'sq.wall', 'sq.stick');
|
|
797
|
+
if (one && q.lipCut) add('warn', `Sides cut ${fmt(q.wall)} of ${fmt(q.t)} mm: setup 2 will contour the leftover lip.`, 'sq.wall');
|
|
798
|
+
if (!one && q.lipCut > q.stick - 0.5) add('error', 'Leftover lip sits below the jaws: cut deeper sides in setup 1.', 'sq.wall', 'sq.stick');
|
|
799
|
+
}
|
|
800
|
+
if (st.op === 'side') {
|
|
801
|
+
const q = sideGeom();
|
|
802
|
+
if (q.amt >= q.span) add('error', 'Removing more than the stock is wide across this side.', 'side.amt');
|
|
803
|
+
else if (q.amt > st.tool.d) add('warn', 'More than one tool Ø off the side: consider pocketing or a roughing pass.', 'side.amt', 'tool.d');
|
|
804
|
+
if (q.depth > st.stock.t) add('error', 'Side depth is deeper than the stock.', 'side.depth', 'stock.t');
|
|
805
|
+
else if (q.depth > st.stock.t - 0.5) add('warn', 'Full-height side: raise the stock so the tool clears the vise jaws and parallels.', 'side.depth');
|
|
806
|
+
}
|
|
807
|
+
if (st.op === 'bolt') {
|
|
808
|
+
const { bcd, n, hd, depth, cx, cy } = st.bolt;
|
|
809
|
+
if (n > 1 && bcd * Math.sin(Math.PI / n) <= hd) add('error', 'Holes overlap: increase Ø or reduce count.', 'bolt.bcd', 'bolt.n', 'bolt.hd');
|
|
810
|
+
if (boltHoles().some(h => out(h.x - hd / 2, h.x + hd / 2, h.y - hd / 2, h.y + hd / 2))) add('warn', 'A hole breaks out of the stock.', 'bolt.cx', 'bolt.cy', 'bolt.bcd');
|
|
811
|
+
if (depth >= T) add('warn', 'Holes go through the stock: check what is underneath.', 'bolt.depth', 'stock.t');
|
|
812
|
+
}
|
|
813
|
+
if (st.op !== 'bolt' && st.op !== 'face' && st.stepdown > st.tool.d) add('warn', 'Step down deeper than tool Ø.', 'stepdown', 'tool.d');
|
|
814
|
+
if (st.safeZ < 1) add('warn', 'Clearance under 1 mm above the stock.', 'safeZ');
|
|
815
|
+
return I;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// ---------- G-code ----------
|
|
819
|
+
function header(lines) {
|
|
820
|
+
const { w, h, t } = st.stock;
|
|
821
|
+
const sq2 = st.op === 'square' && st.sq.setup === 2;
|
|
822
|
+
lines.push(`(${OPS[st.op].name}${st.op === 'square' ? `, setup ${st.sq.setup} of 2` : ''}, generated by cncjs-conversational)`,
|
|
823
|
+
`(stock ${fmt(w)} x ${fmt(h)} x ${fmt(t)}, X0 Y0 ${sq2 ? 'finished front-left' : 'front-left'}, Z0 top)`,
|
|
824
|
+
st.op === 'bolt' ? `(drill Ø${fmt(st.bolt.hd)})` : `(flat end mill Ø${fmt(st.tool.d)}, stepover ${fmt(st.tool.step)}%)`,
|
|
825
|
+
'G21 G90 G17 G94', 'G54', `M3 S${fmt(st.tool.rpm)}`, `G0 Z${fmt(st.safeZ)}`);
|
|
826
|
+
}
|
|
827
|
+
function footer(lines) { lines.push(`G0 Z${fmt(st.safeZ)}`, 'M5', 'M30'); }
|
|
828
|
+
|
|
829
|
+
function faceCode(L, { ys, xa, xb }, zs) {
|
|
830
|
+
for (const z of zs) {
|
|
831
|
+
L.push(`G0 X${fmt(xa)} Y${fmt(ys[0])}`, `G1 Z${fmt(z)} F${fmt(st.tool.plunge)}`);
|
|
832
|
+
ys.forEach((y, i) => {
|
|
833
|
+
if (i) L.push(`G1 Y${fmt(y)}`);
|
|
834
|
+
L.push(`G1 X${fmt(i % 2 ? xa : xb)}${i ? '' : ` F${fmt(st.tool.feed)}`}`);
|
|
835
|
+
});
|
|
836
|
+
L.push('G0 Z1');
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// outside contour around [x0,x1]x[y0,y1], clockwise (climb with M3), outermost offset first
|
|
841
|
+
function contourCode(L, x0, y0, x1, y1, offs, zs) {
|
|
842
|
+
const P = (x, y) => `X${fmt(x)} Y${fmt(y)}`;
|
|
843
|
+
const xm = (x0 + x1) / 2;
|
|
844
|
+
for (const z of zs) {
|
|
845
|
+
L.push(`(contour Z${fmt(z)})`, `G0 ${P(xm, y0 - tR() - offs[0])}`, `G1 Z${fmt(z)} F${fmt(st.tool.plunge)}`);
|
|
846
|
+
offs.forEach((off, i) => {
|
|
847
|
+
const d = tR() + off;
|
|
848
|
+
L.push(`G1 ${P(xm, y0 - d)}${i ? '' : ` F${fmt(st.tool.feed)}`}`,
|
|
849
|
+
`G1 ${P(x0, y0 - d)}`, `G2 ${P(x0 - d, y0)} I0 J${fmt(d)}`,
|
|
850
|
+
`G1 ${P(x0 - d, y1)}`, `G2 ${P(x0, y1 + d)} I${fmt(d)} J0`,
|
|
851
|
+
`G1 ${P(x1, y1 + d)}`, `G2 ${P(x1 + d, y1)} I0 J${fmt(-d)}`,
|
|
852
|
+
`G1 ${P(x1 + d, y0)}`, `G2 ${P(x1, y0 - d)} I${fmt(-d)} J0`,
|
|
853
|
+
`G1 ${P(xm, y0 - d)}`);
|
|
854
|
+
});
|
|
855
|
+
L.push('G0 Z1');
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const GEN = {
|
|
860
|
+
face(L) {
|
|
861
|
+
faceCode(L, facePasses(), [-st.face.depth]);
|
|
862
|
+
},
|
|
863
|
+
square(L) {
|
|
864
|
+
const q = sqGeom();
|
|
865
|
+
if (st.sq.setup === 1) {
|
|
866
|
+
if (q.top > 1e-6) { L.push('(face the top)'); faceCode(L, rasterPasses(0, q.W, 0, q.H), levels(q.top, st.stepdown)); }
|
|
867
|
+
L.push('(cut the sides to finished W x H)');
|
|
868
|
+
contourCode(L, q.ax, q.ay, q.ax + q.w, q.ay + q.h, contourOffsets(q.allow), levels(q.wall, st.stepdown));
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (q.face2 > 1e-6) { L.push('(face to finished thickness)'); faceCode(L, rasterPasses(-q.ax, q.w + q.ax, -q.ay, q.h + q.ay), levels(q.face2, st.stepdown)); }
|
|
872
|
+
if (q.lipCut) {
|
|
873
|
+
L.push('(remove the leftover lip)');
|
|
874
|
+
contourCode(L, 0, 0, q.w, q.h, contourOffsets(q.allow), levels(q.lipCut, st.stepdown).filter(z => z < -q.face2 + 1e-6));
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
side(L) {
|
|
878
|
+
const q = sideGeom();
|
|
879
|
+
const P = ([x, y]) => `X${fmt(x)} Y${fmt(y)}`;
|
|
880
|
+
for (const z of levels(q.depth, st.stepdown)) {
|
|
881
|
+
L.push(`(level Z${fmt(z)})`);
|
|
882
|
+
q.passes.forEach((p, i) => {
|
|
883
|
+
L.push(`G0 ${P(p.a)}`, `G1 Z${fmt(z)} F${fmt(st.tool.plunge)}`, `G1 ${P(p.b)}${i ? '' : ` F${fmt(st.tool.feed)}`}`, 'G0 Z1');
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
circ(L) {
|
|
888
|
+
const { cx, cy, depth } = st.circ;
|
|
889
|
+
L.push(`G0 X${fmt(cx)} Y${fmt(cy)}`, `G0 Z1`);
|
|
890
|
+
for (const z of levels(depth, st.stepdown)) {
|
|
891
|
+
L.push(`(level Z${fmt(z)})`, `G1 Z${fmt(z)} F${fmt(st.tool.plunge)}`);
|
|
892
|
+
circRings().forEach((r, i) => {
|
|
893
|
+
L.push(`G1 X${fmt(cx + r)} Y${fmt(cy)}${i ? '' : ` F${fmt(st.tool.feed)}`}`, `G3 X${fmt(cx + r)} Y${fmt(cy)} I${fmt(-r)} J0`);
|
|
894
|
+
});
|
|
895
|
+
L.push(`G1 X${fmt(cx)} Y${fmt(cy)}`);
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
rect(L) {
|
|
899
|
+
const { cx, cy, depth } = st.rect;
|
|
900
|
+
const loops = rectLoops();
|
|
901
|
+
if (!loops.length) return;
|
|
902
|
+
L.push(`G0 X${fmt(cx)} Y${fmt(cy - loops[0].hh)}`, 'G0 Z1');
|
|
903
|
+
for (const z of levels(depth, st.stepdown)) {
|
|
904
|
+
L.push(`(level Z${fmt(z)})`, `G1 X${fmt(cx)} Y${fmt(cy - loops[0].hh)}`, `G1 Z${fmt(z)} F${fmt(st.tool.plunge)}`);
|
|
905
|
+
loops.forEach(({ hw, hh, rc }, i) => {
|
|
906
|
+
const P = (x, y) => `X${fmt(x)} Y${fmt(y)}`;
|
|
907
|
+
L.push(`G1 ${P(cx, cy - hh)}${i ? '' : ` F${fmt(st.tool.feed)}`}`);
|
|
908
|
+
L.push(`G1 ${P(cx + hw - rc, cy - hh)}`);
|
|
909
|
+
if (rc) L.push(`G3 ${P(cx + hw, cy - hh + rc)} I0 J${fmt(rc)}`);
|
|
910
|
+
L.push(`G1 ${P(cx + hw, cy + hh - rc)}`);
|
|
911
|
+
if (rc) L.push(`G3 ${P(cx + hw - rc, cy + hh)} I${fmt(-rc)} J0`);
|
|
912
|
+
L.push(`G1 ${P(cx - hw + rc, cy + hh)}`);
|
|
913
|
+
if (rc) L.push(`G3 ${P(cx - hw, cy + hh - rc)} I0 J${fmt(-rc)}`);
|
|
914
|
+
L.push(`G1 ${P(cx - hw, cy - hh + rc)}`);
|
|
915
|
+
if (rc) L.push(`G3 ${P(cx - hw + rc, cy - hh)} I${fmt(rc)} J0`);
|
|
916
|
+
L.push(`G1 ${P(cx, cy - hh)}`);
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
bolt(L) {
|
|
921
|
+
// Grbl has no canned cycles, so pecks are expanded
|
|
922
|
+
const { depth, peck } = st.bolt;
|
|
923
|
+
boltHoles().forEach((h, i) => {
|
|
924
|
+
L.push(`(hole ${i + 1})`, `G0 X${fmt(h.x)} Y${fmt(h.y)}`, 'G0 Z1');
|
|
925
|
+
for (let cur = 0; cur > -depth + 1e-6;) {
|
|
926
|
+
const next = Math.max(cur - peck, -depth);
|
|
927
|
+
if (cur < 0) L.push(`G0 Z${fmt(cur + 0.5)}`);
|
|
928
|
+
L.push(`G1 Z${fmt(next)} F${fmt(st.tool.plunge)}`, 'G0 Z1');
|
|
929
|
+
cur = next;
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
},
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
function estimate(lines) {
|
|
936
|
+
const RAPID = 3000;
|
|
937
|
+
let p = { X: 0, Y: 0, Z: st.safeZ }, mode = 0, F = 0, t = 0, cut = 0;
|
|
938
|
+
for (const raw of lines) {
|
|
939
|
+
const l = raw.replace(/\(.*?\)/g, '');
|
|
940
|
+
const w = Object.fromEntries([...l.matchAll(/([GXYZIJF])(-?[\d.]+)/g)].map(m => [m[1], +m[2]]));
|
|
941
|
+
if ('G' in w && w.G <= 3) mode = w.G;
|
|
942
|
+
if ('F' in w) F = w.F;
|
|
943
|
+
if (!('X' in w || 'Y' in w || 'Z' in w)) continue;
|
|
944
|
+
const q = { X: w.X ?? p.X, Y: w.Y ?? p.Y, Z: w.Z ?? p.Z };
|
|
945
|
+
let d = Math.hypot(q.X - p.X, q.Y - p.Y, q.Z - p.Z);
|
|
946
|
+
if (mode >= 2) {
|
|
947
|
+
const c = [p.X + (w.I ?? 0), p.Y + (w.J ?? 0)], r = Math.hypot(w.I ?? 0, w.J ?? 0);
|
|
948
|
+
let sweep = Math.atan2(q.Y - c[1], q.X - c[0]) - Math.atan2(p.Y - c[1], p.X - c[0]);
|
|
949
|
+
if (mode === 2) sweep = -sweep;
|
|
950
|
+
sweep = ((sweep % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI) || 2 * Math.PI;
|
|
951
|
+
d = r * sweep;
|
|
952
|
+
}
|
|
953
|
+
t += d / (mode === 0 ? RAPID : F || 1);
|
|
954
|
+
if (mode) cut += d;
|
|
955
|
+
p = q;
|
|
956
|
+
}
|
|
957
|
+
return { min: t, cut };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const esc = s => s.replace(/&/g, '&').replace(/</g, '<');
|
|
961
|
+
function paint(lines) {
|
|
962
|
+
return lines.map(l => {
|
|
963
|
+
if (l.startsWith('(')) return `<span class="c">${esc(l)}</span>`;
|
|
964
|
+
if (/^G0\b/.test(l)) return `<span class="g0">${esc(l)}</span>`;
|
|
965
|
+
if (/^G[23]\b/.test(l)) return `<span class="arc">${esc(l)}</span>`;
|
|
966
|
+
return esc(l);
|
|
967
|
+
}).join('\n');
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// ---------- render ----------
|
|
971
|
+
let current = [];
|
|
972
|
+
function render() {
|
|
973
|
+
const out = { plan: [], sec: [], tool: [], step: [] };
|
|
974
|
+
const pos = {};
|
|
975
|
+
drawPlan(out.plan, pos);
|
|
976
|
+
drawSec(out.sec, pos);
|
|
977
|
+
drawTool(out.tool, pos);
|
|
978
|
+
if (st.op !== 'bolt') drawStep(out.step, pos);
|
|
979
|
+
for (const [k, v] of Object.entries(VIEWS)) v.g.innerHTML = out[k].join('');
|
|
980
|
+
document.getElementById('f-step').hidden = st.op === 'bolt';
|
|
981
|
+
document.getElementById('sec-cap').textContent = st.op === 'bolt' ? 'Front view, holes projected, Z exaggerated'
|
|
982
|
+
: st.op === 'square' ? `Section through centre, setup ${st.sq.setup} in the vise, Z exaggerated`
|
|
983
|
+
: st.op === 'side' ? `Section across the ${st.side.side.toUpperCase().replace('-', '−')} side, Z exaggerated` : 'Section through centre, Z exaggerated';
|
|
984
|
+
const setupEl = document.getElementById('setup');
|
|
985
|
+
setupEl.hidden = st.op !== 'square';
|
|
986
|
+
for (const b of setupEl.querySelectorAll('button')) b.setAttribute('aria-pressed', String(+b.dataset.setup === st.sq.setup));
|
|
987
|
+
const sideEl = document.getElementById('sidepick');
|
|
988
|
+
sideEl.hidden = st.op !== 'side';
|
|
989
|
+
for (const b of sideEl.querySelectorAll('button')) b.setAttribute('aria-pressed', String(b.dataset.side === st.side.side));
|
|
990
|
+
document.getElementById('setup-note').textContent = st.sq.setup === 1
|
|
991
|
+
? `Clamp the raw stock ${fmt(st.sq.stick)} mm above the jaws. Probe X0 Y0 on the raw front-left corner, Z0 on top.`
|
|
992
|
+
: `Flip the part and clamp on the finished sides, ${fmt(st.sq.stick)} mm above the jaws. Probe X0 Y0 on the finished front-left corner, Z0 on top.`;
|
|
993
|
+
|
|
994
|
+
const issues = check();
|
|
995
|
+
const state = new Map();
|
|
996
|
+
for (const i of issues) for (const f of i.f) if (state.get(f) !== 'error') state.set(f, i.lv);
|
|
997
|
+
for (const [p, el] of Object.entries(inputs)) {
|
|
998
|
+
const at = pos[p];
|
|
999
|
+
el.hidden = !at;
|
|
1000
|
+
if (!at) continue;
|
|
1001
|
+
const v = VIEWS[at.v];
|
|
1002
|
+
if (el.parentNode !== v.ov) v.ov.append(el);
|
|
1003
|
+
el.style.left = `${at.x / v.vb[0] * 100}%`;
|
|
1004
|
+
el.style.top = `${at.y / v.vb[1] * 100}%`;
|
|
1005
|
+
if (!el.querySelector('input').matches(':invalid')) el.dataset.state = state.get(p) || '';
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const ul = document.getElementById('checks');
|
|
1009
|
+
ul.innerHTML = issues.length
|
|
1010
|
+
? issues.map(i => `<li data-lv="${i.lv}">${esc(i.text)}</li>`).join('')
|
|
1011
|
+
: '<li>Ready to cut.</li>';
|
|
1012
|
+
|
|
1013
|
+
const blocked = issues.some(i => i.lv === 'error');
|
|
1014
|
+
const pre = document.getElementById('gcode'), stats = document.getElementById('stats');
|
|
1015
|
+
document.getElementById('copy').disabled = document.getElementById('dl').disabled = blocked;
|
|
1016
|
+
if (blocked) {
|
|
1017
|
+
current = [];
|
|
1018
|
+
pre.innerHTML = '<span class="c">(fix the errors on the left to generate code)</span>';
|
|
1019
|
+
stats.textContent = '';
|
|
1020
|
+
updateLoad();
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
current = [];
|
|
1024
|
+
header(current);
|
|
1025
|
+
GEN[st.op](current);
|
|
1026
|
+
footer(current);
|
|
1027
|
+
pre.innerHTML = paint(current);
|
|
1028
|
+
const e = estimate(current);
|
|
1029
|
+
const m = Math.floor(e.min), s = Math.round((e.min - m) * 60);
|
|
1030
|
+
stats.innerHTML = `<b>${current.length}</b> lines, <b>${Math.round(e.cut)}</b> mm of feed moves, about <b>${m} min ${s} s</b> at the programmed feeds`;
|
|
1031
|
+
updateLoad();
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// ---------- wiring ----------
|
|
1035
|
+
function selectOp(op) {
|
|
1036
|
+
st.op = op;
|
|
1037
|
+
for (const b of document.querySelectorAll('#ops button')) b.setAttribute('aria-pressed', String(b.dataset.op === op));
|
|
1038
|
+
buildInputs();
|
|
1039
|
+
render();
|
|
1040
|
+
}
|
|
1041
|
+
document.getElementById('ops').addEventListener('click', e => {
|
|
1042
|
+
const b = e.target.closest('button');
|
|
1043
|
+
if (b) selectOp(b.dataset.op);
|
|
1044
|
+
});
|
|
1045
|
+
document.getElementById('setup').addEventListener('click', e => {
|
|
1046
|
+
const b = e.target.closest('button');
|
|
1047
|
+
if (!b) return;
|
|
1048
|
+
st.sq.setup = +b.dataset.setup;
|
|
1049
|
+
render();
|
|
1050
|
+
});
|
|
1051
|
+
document.getElementById('sidepick').addEventListener('click', e => {
|
|
1052
|
+
const b = e.target.closest('button');
|
|
1053
|
+
if (!b) return;
|
|
1054
|
+
st.side.side = b.dataset.side;
|
|
1055
|
+
render();
|
|
1056
|
+
});
|
|
1057
|
+
document.getElementById('copy').addEventListener('click', async e => {
|
|
1058
|
+
await navigator.clipboard.writeText(current.join('\n') + '\n');
|
|
1059
|
+
e.target.textContent = 'Copied';
|
|
1060
|
+
setTimeout(() => { e.target.textContent = 'Copy'; }, 1200);
|
|
1061
|
+
});
|
|
1062
|
+
document.getElementById('dl').addEventListener('click', () => {
|
|
1063
|
+
const a = document.createElement('a');
|
|
1064
|
+
a.href = URL.createObjectURL(new Blob([current.join('\n') + '\n'], { type: 'text/plain' }));
|
|
1065
|
+
a.download = programName();
|
|
1066
|
+
a.click();
|
|
1067
|
+
URL.revokeObjectURL(a.href);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
// ---------- CNCjs custom widget ----------
|
|
1071
|
+
// CNCjs puts its session token on the iframe URL, answers a `connect` message with
|
|
1072
|
+
// { action: { type: 'change', payload: { port, controller } } }, and sizes the
|
|
1073
|
+
// iframe from its body itself. Its sandbox has no allow-downloads, so inside
|
|
1074
|
+
// CNCjs the program goes through /api/gcode instead of a file.
|
|
1075
|
+
const token = new URLSearchParams(location.search).get('token');
|
|
1076
|
+
const sameOriginParent = () => { try { return window.parent.location.origin === location.origin; } catch { return false; } };
|
|
1077
|
+
// standalone: own tab or file; cncjs: served and framed by CNCjs; foreign: framed by CNCjs from another origin
|
|
1078
|
+
const embedding = window.parent === window || !token ? 'standalone' : sameOriginParent() ? 'cncjs' : 'foreign';
|
|
1079
|
+
// load.state: idle | loading | loaded | refused | failed; `program` is the text the message is about
|
|
1080
|
+
const cnc = { token, port: null, load: { state: 'idle', message: '', program: '' } };
|
|
1081
|
+
const programName = () => `${st.op}${st.op === 'square' ? `-setup${st.sq.setup}` : st.op === 'side' ? `-${st.side.side}` : ''}.nc`;
|
|
1082
|
+
const programText = () => current.length ? current.join('\n') + '\n' : '';
|
|
1083
|
+
const setLoad = (state, message = '', program = '') => { cnc.load = { state, message, program }; updateLoad(); };
|
|
1084
|
+
|
|
1085
|
+
function updateLoad() {
|
|
1086
|
+
const load = document.getElementById('load'), note = document.getElementById('cnc-note');
|
|
1087
|
+
load.hidden = note.hidden = embedding === 'standalone';
|
|
1088
|
+
document.getElementById('dl').hidden = embedding !== 'standalone';
|
|
1089
|
+
if (embedding === 'standalone') return;
|
|
1090
|
+
if (embedding === 'foreign') {
|
|
1091
|
+
load.disabled = true;
|
|
1092
|
+
note.textContent = 'Loading into CNCjs needs the widget served by CNCjs itself (same address and port).';
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const { load: L } = cnc;
|
|
1096
|
+
// A result describes one program; editing it makes the result stale.
|
|
1097
|
+
if (L.state !== 'idle' && L.state !== 'loading' && L.program !== programText()) cnc.load = { state: 'idle', message: '', program: '' };
|
|
1098
|
+
load.disabled = !current.length || !cnc.port || cnc.load.state === 'loading';
|
|
1099
|
+
note.textContent = cnc.load.state !== 'idle' ? cnc.load.message
|
|
1100
|
+
: cnc.port ? `Port ${cnc.port}: loading replaces the program loaded in CNCjs.` : 'Open a port in CNCjs to load this program.';
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async function cncjsApi(path, init = {}) {
|
|
1104
|
+
const res = await fetch(path, { ...init, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${cnc.token}` } });
|
|
1105
|
+
const body = await res.json().catch(() => ({}));
|
|
1106
|
+
if (!res.ok) throw new Error(body.msg || `HTTP ${res.status}`);
|
|
1107
|
+
return body;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
async function loadIntoCncjs() {
|
|
1111
|
+
// Snapshot what is being sent; the port and the editor may change while waiting.
|
|
1112
|
+
const port = cnc.port, name = programName(), program = programText();
|
|
1113
|
+
setLoad('loading', `Loading ${name}…`, program);
|
|
1114
|
+
try {
|
|
1115
|
+
// gcode:load stops CNCjs's workflow, so only load when nothing is running or paused,
|
|
1116
|
+
// the same rule CNCjs's own Upload button follows.
|
|
1117
|
+
const workflow = (await cncjsApi('/api/controllers')).find(c => c.port === port)?.workflow?.state;
|
|
1118
|
+
if (!workflow) return setLoad('failed', `Not loaded: ${port} is not open in CNCjs.`, program);
|
|
1119
|
+
if (workflow !== 'idle') return setLoad('refused', `Not loaded: a program is ${workflow} on ${port}. Stop it in CNCjs first.`, program);
|
|
1120
|
+
await cncjsApi('/api/gcode', { method: 'POST', body: JSON.stringify({ port, name, gcode: program }) });
|
|
1121
|
+
setLoad('loaded', `Loaded ${name} into CNCjs on ${port}.`, program);
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
setLoad('failed', `Not loaded: ${error.message}`, program);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (embedding === 'cncjs') {
|
|
1128
|
+
window.addEventListener('message', e => {
|
|
1129
|
+
if (e.source !== window.parent || e.origin !== location.origin) return;
|
|
1130
|
+
const msg = e.data || {};
|
|
1131
|
+
if (typeof msg.token === 'string' && msg.token) cnc.token = msg.token;
|
|
1132
|
+
const a = msg.action || {};
|
|
1133
|
+
if (a.type !== 'change' || !a.payload || !Object.hasOwn(a.payload, 'port')) return;
|
|
1134
|
+
cnc.port = a.payload.port || null;
|
|
1135
|
+
if (cnc.load.state !== 'loading') cnc.load = { state: 'idle', message: '', program: '' };
|
|
1136
|
+
updateLoad();
|
|
1137
|
+
});
|
|
1138
|
+
document.getElementById('load').addEventListener('click', loadIntoCncjs);
|
|
1139
|
+
window.parent.postMessage({ token: cnc.token, action: { type: 'connect' } }, location.origin);
|
|
1140
|
+
}
|
|
1141
|
+
selectOp(st.op);
|
|
1142
|
+
</script>
|
|
1143
|
+
</body>
|
|
1144
|
+
</html>
|