@toolpath/plan 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 +258 -2
- package/dist/index.d.ts +1872 -0
- package/dist/index.js +2934 -0
- package/package.json +48 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Toolpath
|
|
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,259 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @toolpath/plan
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Plan mode for Toolpath: the plan a person states for a part, every edit they can make to it, and the
|
|
4
|
+
plan Toolpath makes from it. A person sets up the ways the part is held, groups work in folders,
|
|
5
|
+
pins features where they want them cut and ignores faces that need no work. These are plain
|
|
6
|
+
functions over plain data, with no React, no DOM and no database, so a server and a browser can
|
|
7
|
+
read the same rules. Use it in any app that plans parts with Toolpath: a quoting app, a planning
|
|
8
|
+
app or a CAM tool.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add @toolpath/plan
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { actionsFor, applyDraftAction, classifyFaces, statementOf } from '@toolpath/plan'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The package is logic only: no React, no DOM and no CSS. Its one dependency is `@toolpath/tool-support`, for the inch. A view over these rules
|
|
19
|
+
belongs in `@toolpath/ui`.
|
|
20
|
+
|
|
21
|
+
## Your app's own fields
|
|
22
|
+
|
|
23
|
+
The draft holds only what Toolpath plans from. Your app keeps its own fields beside it, such as
|
|
24
|
+
when it was saved, who changed it, or the material picked. `applyDraftAction` takes any object that
|
|
25
|
+
holds the constraints and gives it back with your fields as they were:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import type { Constraints } from '@toolpath/plan'
|
|
29
|
+
|
|
30
|
+
interface MyDraft extends Constraints {
|
|
31
|
+
savedAt: string | null
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For example, `@toolpath/quoting/plan` is the quoting app's draft, made this way.
|
|
36
|
+
|
|
37
|
+
## Two layers
|
|
38
|
+
|
|
39
|
+
A page holds the person's edits until they choose to send them: that step is called Apply in
|
|
40
|
+
these docs, and Discard throws the edits away.
|
|
41
|
+
|
|
42
|
+
Plan mode has two layers:
|
|
43
|
+
|
|
44
|
+
- **The draft** (`Constraints`): what a person asks for. It holds setups, the folders inside them,
|
|
45
|
+
the features pinned to each folder, and the faces ignored. It starts empty: with nothing stated,
|
|
46
|
+
Toolpath plans the part itself.
|
|
47
|
+
- **The plan** (`EnginePlan`): what Toolpath planned. It holds setups, each with its direction and
|
|
48
|
+
its actions, and the issues it could not reach.
|
|
49
|
+
|
|
50
|
+
The draft goes to Toolpath, and the plan comes back. A person can start a draft from the plan's
|
|
51
|
+
setups, each read as a `PlannedSetup`: its directions and the features it takes:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
draft ──statementOf──▶ job's setupPlan ──▶ Toolpath ──▶ EnginePlan
|
|
55
|
+
▲ │
|
|
56
|
+
└──────── planFromResults / foldersFromPlan ◀── PlannedSetup
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Each edit is a function from the draft as it is to the draft as it will be. These always answer a draft:
|
|
60
|
+
`addFolder`, `renameFolder`, `removeFolder`, `moveSetup`, `moveWithin`, `unpinFeature`, and, for
|
|
61
|
+
ignored faces, `withIgnored` and `withoutIgnored`. These return `{ refused }` with a reason to show
|
|
62
|
+
when the edit cannot be made: `setSetupProcess`, `setToolOrientation`, `moveFolder`,
|
|
63
|
+
`moveFeature`, `pinInto` and `pinFrom`. Nothing here throws on typed input; read a stored draft
|
|
64
|
+
through `parseDraftSetups` first.
|
|
65
|
+
`parseDraftSetups` and `parseStock` check stored data before it is used.
|
|
66
|
+
|
|
67
|
+
`classifyFaces` reads what the draft and the plan leave of each face: pinned, ignored, open,
|
|
68
|
+
machined or not machined (`COVERAGE_KINDS`). A viewer paints faces from it.
|
|
69
|
+
|
|
70
|
+
## Edits: one menu, one function, one question
|
|
71
|
+
|
|
72
|
+
A page edits the draft through three things, and nothing else:
|
|
73
|
+
|
|
74
|
+
- `DraftAction`: every edit a person can make, as one closed list. Each action carries all it
|
|
75
|
+
needs, so the same action on the same draft gives the same plan: a new id for anything it makes,
|
|
76
|
+
which must not be in the draft yet, and for a feature the direction it was read from.
|
|
77
|
+
- `applyDraftAction(draft, action, rules, saved)`: carries out any action. It answers `{ draft }`,
|
|
78
|
+
with `facts` for the status line and, where Toolpath's own app opens a folder after it, that
|
|
79
|
+
folder's `folderId`; or `{ refused }`, the reason to show the person. An action that changes
|
|
80
|
+
nothing answers the draft as it was. `saved`, the plan as applied, is what a pick among a click's
|
|
81
|
+
readings puts back; the session passes it.
|
|
82
|
+
- `actionsFor(draft, target, rules)`: what can be done here. The target is the part, a folder, a
|
|
83
|
+
feature read from a direction, or faces picked on the part. A page offers these kinds of action,
|
|
84
|
+
and no others. Three follow other controls: the two picks among a click's readings, offered
|
|
85
|
+
where a click read more than one feature, and `fromPlan`, offered while the plan holds no folders
|
|
86
|
+
and Toolpath has planned the part.
|
|
87
|
+
|
|
88
|
+
A drag says why a drop can't be made while it hovers: try the drop with `applyDraftAction` and show
|
|
89
|
+
its `refused`. The tree's top row (the part) is no folder, so a page says "Rows go into a folder."
|
|
90
|
+
for a feature's row dropped there itself. A folder dropped where it already is changes nothing, and the page
|
|
91
|
+
opens nothing for it.
|
|
92
|
+
|
|
93
|
+
After a pick among an Ignore click's readings, the pick is the click: send the next pick with
|
|
94
|
+
`click: { tag, regions: rules.regionsOf(tag) }` for the feature picked.
|
|
95
|
+
|
|
96
|
+
`rules` (`PlanRules`) reads the part: the faces of each feature, and the kernel that numbers them.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { actionsFor, applyDraftAction, type DraftAction } from '@toolpath/plan'
|
|
100
|
+
|
|
101
|
+
const offered = actionsFor(draft, { folder: 'holes' }, rules) // show these as buttons
|
|
102
|
+
|
|
103
|
+
const press = (action: DraftAction) => {
|
|
104
|
+
const result = applyDraftAction(draft, action, rules)
|
|
105
|
+
if ('refused' in result) {
|
|
106
|
+
showMessage(result.refused)
|
|
107
|
+
} else {
|
|
108
|
+
setDraft(result.draft)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Kind | What it does |
|
|
114
|
+
| --------------------- | ------------------------------------------------------------------------------------------------- |
|
|
115
|
+
| `addFolder` | A folder inside another, at the end |
|
|
116
|
+
| `renameFolder` | A new name |
|
|
117
|
+
| `remove` | What Delete removes: folders (setups too) with all they hold, and pins; every setup is Remove all |
|
|
118
|
+
| `moveFolder` | A folder dropped in the tree: reordered, into a folder, or to the top |
|
|
119
|
+
| `moveWithin` | A folder moved up or down beside its neighbors |
|
|
120
|
+
| `addSetup` | A setup facing a direction nothing faces yet |
|
|
121
|
+
| `setProcess` | Hold a setup 3 axis, 3+1 or 3+2 |
|
|
122
|
+
| `setToolOrientation` | A folder in an indexed setup tilted to a direction of its own, or not |
|
|
123
|
+
| `pinInto` | In an open folder: a left click pins the feature there, a right click takes out one pinned there |
|
|
124
|
+
| `pinFrom` | The feature pinned in the folder facing its direction, or a new setup; let go if pinned that way |
|
|
125
|
+
| `moveFeature` | A feature's row dropped on a folder: pinned there |
|
|
126
|
+
| `unpin` | The feature let go: Toolpath decides where it is machined |
|
|
127
|
+
| `ignore` | Faces ignored; a pin whose every face is ignored is let go |
|
|
128
|
+
| `unignore` | Faces no longer ignored |
|
|
129
|
+
| `choosePinReading` | After a click in a folder read several features: another of them meant instead |
|
|
130
|
+
| `chooseIgnoreReading` | After an Ignore click read several features: another of them ignored instead |
|
|
131
|
+
| `fromPlan` | Start from Toolpath's plan: its setups as folders, and with `pins` its features pinned |
|
|
132
|
+
|
|
133
|
+
Each kind makes the plan Toolpath's own app makes for the same click; the package's tests hold each
|
|
134
|
+
to the app's own helper. A pin,
|
|
135
|
+
of any kind, does what pinning does there: the person's pins whose every face it covers are let go,
|
|
136
|
+
and its own faces are no longer ignored. Pinning a feature where it is pinned already lets it go, as
|
|
137
|
+
a right click does. A left click on a feature in the open folder already sends nothing: it stays,
|
|
138
|
+
and a pick among its readings (`choosePinReading`, `target: 'add'`) pins another beside it. With
|
|
139
|
+
Ignore open, `ignoreClick` says what a click does: a left click ignores, a right click lets go.
|
|
140
|
+
|
|
141
|
+
`facts` say what the action did beyond the draft, for the status line: the pins a pin `replaced`,
|
|
142
|
+
the faces it `unignored`, the pins an ignore `unpinned`, whether a pin `added` a setup, whether it
|
|
143
|
+
was a second click that `letGo`, and the folder (`home`) it is pinned in or let go from. Toolpath's
|
|
144
|
+
own app says nothing of the faces Clear gives back, and a pick among an Ignore click's readings
|
|
145
|
+
says only what it ignores (its `unignored` is empty).
|
|
146
|
+
|
|
147
|
+
A page that asks before adding a setup tries the action first, with the same draft, `saved` and
|
|
148
|
+
ids, and sends it only once the person says yes: `applyDraftAction` changes nothing itself.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const pin: DraftAction = { kind: 'pinFrom', tag, direction, newId: crypto.randomUUID() }
|
|
152
|
+
const tried = applyDraftAction(session.draft, pin, rules, session.saved)
|
|
153
|
+
if ('refused' in tried || !tried.facts.added || (await confirm('Add a setup for it?'))) {
|
|
154
|
+
send({ type: 'act', action: pin })
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Undo, Redo, Discard: the session
|
|
159
|
+
|
|
160
|
+
A page holds the person's edits until Apply in a `PlanSession`. One pure function, `planSession`,
|
|
161
|
+
moves it on, so a React page gets Undo, Redo, Discard and the history from one `useReducer`:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import {
|
|
165
|
+
canUndo,
|
|
166
|
+
isHeld,
|
|
167
|
+
planSession,
|
|
168
|
+
startSession,
|
|
169
|
+
type PlanSession,
|
|
170
|
+
type SessionEvent,
|
|
171
|
+
} from '@toolpath/plan'
|
|
172
|
+
|
|
173
|
+
const [session, send] = useReducer(
|
|
174
|
+
(now: PlanSession<MyDraft>, event: SessionEvent<MyDraft>) => planSession(now, event, rules),
|
|
175
|
+
saved, // the draft as applied, read from your server
|
|
176
|
+
startSession,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
send({ type: 'act', action: { kind: 'addSetup', direction: up, id: crypto.randomUUID() } })
|
|
180
|
+
send({ type: 'undo' })
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
| Event | What it does |
|
|
184
|
+
| --------- | ------------------------------------------------------------------------------------- |
|
|
185
|
+
| `act` | A `DraftAction`, as one step. Refused, the draft stays and `session.refused` says why |
|
|
186
|
+
| `edit` | Your app's own fields (a material, a stock), as one step. Never the constraints |
|
|
187
|
+
| `undo` | One step back |
|
|
188
|
+
| `redo` | One step forward |
|
|
189
|
+
| `discard` | The draft as applied again: one step, which Undo takes back |
|
|
190
|
+
| `applied` | Your Apply saved this draft: it is now the draft as applied |
|
|
191
|
+
| `read` | The draft as applied, read again; held edits stay on it |
|
|
192
|
+
|
|
193
|
+
Edits with the same `coalesce` key, one after another, are one step: use one key while a dialog is
|
|
194
|
+
open. Undo reaches back `HISTORY` (100) steps. `isHeld` says whether there is anything to Apply or
|
|
195
|
+
Discard; `canUndo` and `canRedo` say whether the buttons work. `session.refused` says why the last
|
|
196
|
+
action was refused, `session.folderId` names the folder to open after it, and `session.facts` says
|
|
197
|
+
what else it did; the next event clears all three. Copy what they say into the page's own state:
|
|
198
|
+
the status line keeps its note after the next event, Undo included.
|
|
199
|
+
|
|
200
|
+
The session saves nothing. Your app sends `session.draft` on Apply and tells the session how it
|
|
201
|
+
went, with `applied`, or with `read` when the plan was applied elsewhere first. Keep save stamps,
|
|
202
|
+
such as a version, out of the session's draft, or Undo puts an old one back. Pass the version on
|
|
203
|
+
`startSession`, `applied` and `read` instead: a read older than the session's is not taken.
|
|
204
|
+
|
|
205
|
+
- Start the session once the draft as applied is read, and a new one for each part: in React,
|
|
206
|
+
`key={partId}` on the page that holds it. Edits kept from before a reload start held:
|
|
207
|
+
`startSession(saved, { held: kept, version })`.
|
|
208
|
+
- `rules` is `PlanRules` (the part's faces and kernel), and three optional fields. `locked`: the
|
|
209
|
+
plan can't be changed now, so no edit, Undo, Redo or Discard changes it. `same`: whether two
|
|
210
|
+
drafts are the same plan; the default compares values, whatever the order of each object's
|
|
211
|
+
keys. `rebase`: how held edits go onto a plan read again; by default the constraints stay as held,
|
|
212
|
+
and each of your own fields stays as held where it was edited here and follows the plan read
|
|
213
|
+
where it was not.
|
|
214
|
+
- `edit` takes a function of the draft as it is now, and answers the fields it changes
|
|
215
|
+
(`AppFields`). It must be pure, because React may call it twice. Setups, folders, pins and
|
|
216
|
+
ignored faces change only through `act`, where every change is checked: any `edit` answers for
|
|
217
|
+
them is passed over.
|
|
218
|
+
|
|
219
|
+
## Every "no", and why
|
|
220
|
+
|
|
221
|
+
The package says no only where it must. Each refusal is one of these:
|
|
222
|
+
|
|
223
|
+
| Refusal | Kind | Why |
|
|
224
|
+
| ------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------- |
|
|
225
|
+
| "… is cut from that direction already." | Engine | Toolpath takes each direction once |
|
|
226
|
+
| "A plan holds at most 64 setups." | Engine | The most setups Toolpath takes in a stated plan |
|
|
227
|
+
| "It is cut from …’s direction, a setup already." | Engine | As a setup it would face its setup's direction twice |
|
|
228
|
+
| "No folder in … is cut from that direction." | Our choice | Could instead pin the same faces as read from a direction a folder in it faces |
|
|
229
|
+
| "That folder is gone." / "Only a setup has a hold." | Bad input | The action names something that is not there, or not a setup |
|
|
230
|
+
| "That id is taken: each folder needs an id of its own." | Bad input | Two folders with one id could not be told apart |
|
|
231
|
+
| "A folder moves a whole number of places." / "That is not a plan action." | Bad input | The action is not one the list holds |
|
|
232
|
+
| "A folder can’t go inside itself." | Not possible | The tree would loop |
|
|
233
|
+
| "This needs the plan as applied." | Bad input | A pick among readings puts back what the plan as applied had |
|
|
234
|
+
| "Start from Toolpath’s plan while the plan holds no folders." | Our choice | Could instead add Toolpath's setups after the folders there |
|
|
235
|
+
| "Folders sit at most 6 below their setup." | Our choice | `MAX_DEPTH` keeps the tree readable |
|
|
236
|
+
| "Move or delete its tool orientations first." | Our choice | Could instead make the tilts plain folders, or setups of their own |
|
|
237
|
+
| "Only a 3+1 or 3+2 setup has tool orientations." | Our choice | Could instead index the setup, as dropping a folder from another direction does |
|
|
238
|
+
| "Rows stay in folders cut from their own direction." | Our choice | Could instead pin that direction's reading of the same faces |
|
|
239
|
+
| "Only a folder inside a setup can be a tool orientation." | Our choice | A setup's direction is fixed when it is made |
|
|
240
|
+
|
|
241
|
+
An "our choice" refusal can become a yes in a later release. Until then, it stays as it is.
|
|
242
|
+
|
|
243
|
+
## Clicks on the part, and what it shows
|
|
244
|
+
|
|
245
|
+
These are the helpers behind the Plan page of Toolpath's own app. Each is a plain function of plain data:
|
|
246
|
+
|
|
247
|
+
- Pins: `pinEdit`, `withPin`, `folderClick`, `chooseReading`, `takeBack` and `replacedBy`. Ignore:
|
|
248
|
+
`ignoreEdit`, `unignoreEdit`, `ignoreClick` and `ignoreSelection`. `readingsFor` lists the
|
|
249
|
+
features a click on a face can mean.
|
|
250
|
+
- The tree: `draftView` lays out the draft, `planView` and `resultsView` the plan. The setups of
|
|
251
|
+
`resultsView` can be passed to `planFromResults`. `flattenFolders`, `projectFolder` and `gapAt`
|
|
252
|
+
place a folder being dragged, and say where it lands as a `DropAt`.
|
|
253
|
+
- Paint: `planPaint` and `planHatch` color each face from `classifyFaces`
|
|
254
|
+
(`DONE_COLOR`, `UNMACHINED_FACE_COLOR`, `OTHER_FOLDER_COLOR`).
|
|
255
|
+
- Before Apply: `planChecks` and `confirmChecks` list what the person should check.
|
|
256
|
+
`directionAnalysis` says whether a direction has been analyzed.
|
|
257
|
+
|
|
258
|
+
`tests/sdk-contract.test.ts` checks these types against `@toolpath/api`'s. The type checker
|
|
259
|
+
(`pnpm check-types`) runs that check; `vitest` alone does not.
|