fluidcad 0.0.35 → 0.0.37
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.txt +21 -504
- package/README.md +1 -1
- package/bin/commands/login.js +33 -5
- package/bin/commands/mcp.js +3 -2
- package/bin/commands/publish.js +103 -8
- package/bin/lib/api-client.js +8 -0
- package/bin/lib/model-config.js +27 -4
- package/bin/lib/prompt.js +97 -0
- package/lib/dist/common/edge.d.ts +1 -1
- package/lib/dist/common/face.d.ts +1 -1
- package/lib/dist/common/scene-object.d.ts +6 -0
- package/lib/dist/common/scene-object.js +8 -0
- package/lib/dist/common/shape-factory.d.ts +1 -1
- package/lib/dist/common/shape-history-tracker.d.ts +1 -1
- package/lib/dist/common/shape.d.ts +1 -1
- package/lib/dist/common/solid.d.ts +1 -1
- package/lib/dist/common/transformable-primitive.d.ts +12 -1
- package/lib/dist/common/transformable-primitive.js +27 -0
- package/lib/dist/common/vertex.d.ts +1 -1
- package/lib/dist/common/wire.d.ts +1 -1
- package/lib/dist/core/2d/index.d.ts +1 -0
- package/lib/dist/core/2d/index.js +1 -0
- package/lib/dist/core/2d/text.d.ts +30 -0
- package/lib/dist/core/2d/text.js +37 -0
- package/lib/dist/core/helix.d.ts +20 -0
- package/lib/dist/core/helix.js +36 -0
- package/lib/dist/core/index.d.ts +3 -1
- package/lib/dist/core/index.js +2 -0
- package/lib/dist/core/interfaces.d.ts +180 -0
- package/lib/dist/core/wrap.d.ts +17 -0
- package/lib/dist/core/wrap.js +39 -0
- package/lib/dist/features/2d/text.d.ts +67 -0
- package/lib/dist/features/2d/text.js +320 -0
- package/lib/dist/features/cylinder.d.ts +3 -1
- package/lib/dist/features/cylinder.js +5 -2
- package/lib/dist/features/extrude-base.d.ts +1 -0
- package/lib/dist/features/extrude-to-face.d.ts +1 -0
- package/lib/dist/features/extrude-to-face.js +6 -0
- package/lib/dist/features/fillet.d.ts +1 -1
- package/lib/dist/features/helix.d.ts +41 -0
- package/lib/dist/features/helix.js +337 -0
- package/lib/dist/features/select.js +32 -8
- package/lib/dist/features/simple-extruder.d.ts +1 -1
- package/lib/dist/features/simple-extruder.js +7 -2
- package/lib/dist/features/sphere.d.ts +3 -1
- package/lib/dist/features/sphere.js +5 -2
- package/lib/dist/features/sweep.js +7 -2
- package/lib/dist/features/wrap.d.ts +39 -0
- package/lib/dist/features/wrap.js +116 -0
- package/lib/dist/filters/edge/belongs-to-face.d.ts +3 -1
- package/lib/dist/filters/edge/belongs-to-face.js +14 -10
- package/lib/dist/filters/filter.d.ts +1 -1
- package/lib/dist/filters/from-object.d.ts +1 -1
- package/lib/dist/filters/tangent-expander.d.ts +1 -1
- package/lib/dist/filters/tangent-expander.js +57 -40
- package/lib/dist/helpers/scene-helpers.d.ts +2 -0
- package/lib/dist/helpers/scene-helpers.js +1 -1
- package/lib/dist/index.d.ts +2 -0
- package/lib/dist/index.js +3 -1
- package/lib/dist/io/file-import.d.ts +7 -0
- package/lib/dist/io/file-import.js +28 -1
- package/lib/dist/io/font-registry.d.ts +45 -0
- package/lib/dist/io/font-registry.js +272 -0
- package/lib/dist/math/bspline-interpolation.d.ts +29 -0
- package/lib/dist/math/bspline-interpolation.js +194 -0
- package/lib/dist/oc/boolean-ops.d.ts +3 -1
- package/lib/dist/oc/boolean-ops.js +15 -1
- package/lib/dist/oc/color-transfer.d.ts +1 -1
- package/lib/dist/oc/constraints/constraint-helpers.d.ts +4 -4
- package/lib/dist/oc/constraints/curve/tangent-circle-solver.js +10 -9
- package/lib/dist/oc/constraints/curve/tangent-line-solver.js +5 -6
- package/lib/dist/oc/convert.d.ts +1 -1
- package/lib/dist/oc/draft-ops.d.ts +1 -1
- package/lib/dist/oc/edge-ops.d.ts +2 -2
- package/lib/dist/oc/edge-ops.js +13 -14
- package/lib/dist/oc/edge-props.d.ts +1 -1
- package/lib/dist/oc/edge-query.d.ts +1 -1
- package/lib/dist/oc/edge-query.js +3 -8
- package/lib/dist/oc/errors.d.ts +8 -0
- package/lib/dist/oc/errors.js +27 -0
- package/lib/dist/oc/explorer.d.ts +2 -2
- package/lib/dist/oc/extrude-ops.d.ts +28 -2
- package/lib/dist/oc/extrude-ops.js +56 -7
- package/lib/dist/oc/face-ops.d.ts +2 -1
- package/lib/dist/oc/face-ops.js +11 -0
- package/lib/dist/oc/face-props.d.ts +1 -1
- package/lib/dist/oc/face-query.d.ts +12 -1
- package/lib/dist/oc/face-query.js +39 -0
- package/lib/dist/oc/fillet-ops.d.ts +1 -1
- package/lib/dist/oc/fillet-ops.js +4 -4
- package/lib/dist/oc/geometry.d.ts +1 -1
- package/lib/dist/oc/geometry.js +12 -14
- package/lib/dist/oc/helix-ops.d.ts +37 -0
- package/lib/dist/oc/helix-ops.js +88 -0
- package/lib/dist/oc/hit-test.d.ts +1 -1
- package/lib/dist/oc/index.d.ts +4 -0
- package/lib/dist/oc/index.js +2 -0
- package/lib/dist/oc/init.d.ts +1 -1
- package/lib/dist/oc/init.js +1 -1
- package/lib/dist/oc/intersection.js +1 -1
- package/lib/dist/oc/io.d.ts +6 -6
- package/lib/dist/oc/io.js +31 -24
- package/lib/dist/oc/measure/classify.d.ts +34 -0
- package/lib/dist/oc/measure/classify.js +246 -0
- package/lib/dist/oc/measure/measure-ops.d.ts +9 -0
- package/lib/dist/oc/measure/measure-ops.js +210 -0
- package/lib/dist/oc/measure/measure-types.d.ts +39 -0
- package/lib/dist/oc/measure/measure-types.js +1 -0
- package/lib/dist/oc/measure/sampling.d.ts +9 -0
- package/lib/dist/oc/measure/sampling.js +77 -0
- package/lib/dist/oc/measure/vec.d.ts +13 -0
- package/lib/dist/oc/measure/vec.js +23 -0
- package/lib/dist/oc/mesh.d.ts +1 -1
- package/lib/dist/oc/mesh.js +40 -28
- package/lib/dist/oc/path-sampler.d.ts +29 -0
- package/lib/dist/oc/path-sampler.js +63 -0
- package/lib/dist/oc/props.d.ts +1 -1
- package/lib/dist/oc/props.js +4 -1
- package/lib/dist/oc/shape-hash.d.ts +26 -0
- package/lib/dist/oc/shape-hash.js +32 -0
- package/lib/dist/oc/shape-ops.d.ts +5 -3
- package/lib/dist/oc/shape-ops.js +6 -5
- package/lib/dist/oc/sweep-ops.d.ts +22 -1
- package/lib/dist/oc/sweep-ops.js +206 -18
- package/lib/dist/oc/text-outline.d.ts +62 -0
- package/lib/dist/oc/text-outline.js +212 -0
- package/lib/dist/oc/topology-index.d.ts +1 -1
- package/lib/dist/oc/vertex-ops.d.ts +1 -1
- package/lib/dist/oc/wire-ops.d.ts +1 -1
- package/lib/dist/oc/wire-ops.js +1 -1
- package/lib/dist/oc/wrap-development.d.ts +105 -0
- package/lib/dist/oc/wrap-development.js +179 -0
- package/lib/dist/oc/wrap-ops.d.ts +100 -0
- package/lib/dist/oc/wrap-ops.js +406 -0
- package/lib/dist/rendering/render-solid.js +10 -2
- package/lib/dist/scene-manager.d.ts +2 -0
- package/lib/dist/scene-manager.js +29 -0
- package/lib/dist/tests/features/cylinder-curve-filter.test.js +3 -3
- package/lib/dist/tests/features/extrude-to-face.test.js +38 -1
- package/lib/dist/tests/features/helix.test.d.ts +1 -0
- package/lib/dist/tests/features/helix.test.js +295 -0
- package/lib/dist/tests/features/repeat-primitive.test.d.ts +1 -0
- package/lib/dist/tests/features/repeat-primitive.test.js +60 -0
- package/lib/dist/tests/features/rib.test.js +6 -1
- package/lib/dist/tests/features/sweep.test.js +125 -1
- package/lib/dist/tests/features/text.test.d.ts +1 -0
- package/lib/dist/tests/features/text.test.js +347 -0
- package/lib/dist/tests/features/wrap-development.test.d.ts +1 -0
- package/lib/dist/tests/features/wrap-development.test.js +130 -0
- package/lib/dist/tests/features/wrap-extruded-target.test.d.ts +1 -0
- package/lib/dist/tests/features/wrap-extruded-target.test.js +106 -0
- package/lib/dist/tests/features/wrap-repeat.test.d.ts +1 -0
- package/lib/dist/tests/features/wrap-repeat.test.js +93 -0
- package/lib/dist/tests/features/wrap.test.d.ts +1 -0
- package/lib/dist/tests/features/wrap.test.js +331 -0
- package/lib/dist/tests/math/bspline-interpolation.test.d.ts +1 -0
- package/lib/dist/tests/math/bspline-interpolation.test.js +119 -0
- package/lib/dist/tests/measure.test.d.ts +1 -0
- package/lib/dist/tests/measure.test.js +288 -0
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/llm-docs/api/helix.md +64 -0
- package/llm-docs/api/index.json +11 -2
- package/llm-docs/api/text.md +52 -0
- package/llm-docs/api/types/helix.md +105 -0
- package/llm-docs/api/types/text.md +138 -0
- package/llm-docs/api/types/wrap.md +131 -0
- package/llm-docs/api/wrap.md +62 -0
- package/llm-docs/index.json +121 -1
- package/mcp/dist/server.js +20 -1
- package/mcp/dist/tools/inspection.d.ts +17 -0
- package/mcp/dist/tools/inspection.js +14 -0
- package/package.json +7 -3
- package/server/dist/fluidcad-server.d.ts +29 -0
- package/server/dist/fluidcad-server.js +40 -0
- package/server/dist/index.js +4 -2
- package/server/dist/model-package/pack.js +7 -6
- package/server/dist/model-package/types.d.ts +4 -3
- package/server/dist/preferences.d.ts +4 -0
- package/server/dist/preferences.js +2 -0
- package/server/dist/routes/measure.d.ts +3 -0
- package/server/dist/routes/measure.js +32 -0
- package/server/dist/routes/preferences.js +6 -0
- package/server/dist/routes/sketch-edits.js +2 -1
- package/ui/dist/assets/{index-CDJmUpFI.css → index-dAFdg2Un.css} +1 -1
- package/ui/dist/assets/{index-MRqwG9Vh.js → index-no7mtr5s.js} +149 -102
- package/ui/dist/index.html +2 -2
package/bin/commands/publish.js
CHANGED
|
@@ -2,7 +2,8 @@ import { resolve } from 'path';
|
|
|
2
2
|
import { getHubUrl, readCredentials } from '../lib/config.js';
|
|
3
3
|
import { HubClient } from '../lib/api-client.js';
|
|
4
4
|
import { findEntry, readPackageVersion, readWorkspacePackage } from '../lib/workspace.js';
|
|
5
|
-
import {
|
|
5
|
+
import { readModelIdentity, writeModelConfig } from '../lib/model-config.js';
|
|
6
|
+
import { isInteractive, select } from '../lib/prompt.js';
|
|
6
7
|
import { openBrowser } from '../lib/browser.js';
|
|
7
8
|
|
|
8
9
|
async function runPublish(opts) {
|
|
@@ -20,7 +21,31 @@ async function runPublish(opts) {
|
|
|
20
21
|
const pkg = readWorkspacePackage(workspace);
|
|
21
22
|
const name = opts.name ?? pkg.name;
|
|
22
23
|
const description = opts.description ?? pkg.description;
|
|
23
|
-
const modelId =
|
|
24
|
+
const { modelId: priorModelId, name: priorName } = readModelIdentity(workspace);
|
|
25
|
+
|
|
26
|
+
// Surface who we are and where this is going *before* any bytes leave the
|
|
27
|
+
// machine, so a wrong account or hub is caught before the upload starts.
|
|
28
|
+
// (The model's own page URL is minted by the hub and printed afterwards.)
|
|
29
|
+
console.log('Publishing to the FluidCAD hub:');
|
|
30
|
+
console.log(` account: ${creds.email || '(unknown account)'}`);
|
|
31
|
+
console.log(` url: ${hubUrl}`);
|
|
32
|
+
console.log('');
|
|
33
|
+
|
|
34
|
+
// Decide new-model vs new-version BEFORE the heavy build, so the user makes
|
|
35
|
+
// the call (and we do any model-list lookup) without first waiting ~110ms for
|
|
36
|
+
// the engine to load. A null target ⇒ the hub mints a fresh model.
|
|
37
|
+
const targetModelId = await resolveTargetModel({
|
|
38
|
+
opts,
|
|
39
|
+
hubUrl,
|
|
40
|
+
token: creds.token,
|
|
41
|
+
priorModelId,
|
|
42
|
+
priorName,
|
|
43
|
+
});
|
|
44
|
+
console.log(
|
|
45
|
+
targetModelId
|
|
46
|
+
? `Publishing a new version${priorName ? ` of ${priorName}` : ''}.\n`
|
|
47
|
+
: 'Publishing as a new model.\n',
|
|
48
|
+
);
|
|
24
49
|
|
|
25
50
|
// Render once to capture the full param schema for the manifest. This also
|
|
26
51
|
// acts as a build gate — a compile/runtime error fails the publish here,
|
|
@@ -66,8 +91,8 @@ async function runPublish(opts) {
|
|
|
66
91
|
|
|
67
92
|
const form = new FormData();
|
|
68
93
|
form.append('fluidpkg', new Blob([zip], { type: 'application/zip' }), 'model.fluidpkg');
|
|
69
|
-
if (
|
|
70
|
-
form.append('modelId',
|
|
94
|
+
if (targetModelId) {
|
|
95
|
+
form.append('modelId', targetModelId);
|
|
71
96
|
}
|
|
72
97
|
if (name) {
|
|
73
98
|
form.append('name', name);
|
|
@@ -90,10 +115,12 @@ async function runPublish(opts) {
|
|
|
90
115
|
throw new Error(body.error || `Publish failed (HTTP ${status})`);
|
|
91
116
|
}
|
|
92
117
|
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
118
|
+
// Persist the hub-authoritative id (and the name we used) whenever it changed
|
|
119
|
+
// or the workspace had no config — covers the first publish, a deliberate new
|
|
120
|
+
// model, and re-attaching a deleted fluidcad.json (the user picked an existing
|
|
121
|
+
// model from the list). An owned-match with an unchanged name is a no-op.
|
|
122
|
+
if (body.modelId && (body.modelId !== priorModelId || (name && name !== priorName))) {
|
|
123
|
+
writeModelConfig(workspace, { modelId: body.modelId, name });
|
|
97
124
|
}
|
|
98
125
|
|
|
99
126
|
console.log('');
|
|
@@ -113,6 +140,72 @@ async function runPublish(opts) {
|
|
|
113
140
|
}
|
|
114
141
|
}
|
|
115
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Decide which model this publish targets: an existing model id (→ a new
|
|
145
|
+
* version) or null (→ the hub mints a new model). Honors --new-model /
|
|
146
|
+
* --new-version; otherwise asks when interactive; and with no TTY falls back to
|
|
147
|
+
* today's behavior (a saved fluidcad.json id ⇒ a version, else a new model).
|
|
148
|
+
*/
|
|
149
|
+
async function resolveTargetModel({ opts, hubUrl, token, priorModelId, priorName }) {
|
|
150
|
+
if (opts.newModel && opts.newVersion) {
|
|
151
|
+
throw new Error('Pass only one of --new-model / --new-version.');
|
|
152
|
+
}
|
|
153
|
+
if (opts.newModel) return null;
|
|
154
|
+
if (opts.newVersion) {
|
|
155
|
+
if (priorModelId) return priorModelId;
|
|
156
|
+
if (isInteractive()) return pickExistingModel(hubUrl, token);
|
|
157
|
+
throw new Error(
|
|
158
|
+
'No fluidcad.json here, so there is no model to version. Drop --new-version to ' +
|
|
159
|
+
'publish a new model, or run interactively to pick an existing one.',
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// No explicit flag.
|
|
164
|
+
if (!isInteractive()) {
|
|
165
|
+
// Non-interactive (CI, piped input): keep the historical default.
|
|
166
|
+
return priorModelId;
|
|
167
|
+
}
|
|
168
|
+
if (priorModelId) {
|
|
169
|
+
const label = priorName ? `${priorName} (${priorModelId})` : priorModelId;
|
|
170
|
+
return select('How should this publish go up?', [
|
|
171
|
+
{ label: `Publish a new version of ${label}`, value: priorModelId },
|
|
172
|
+
{ label: 'Publish as a new model', value: null },
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
const choice = await select('How should this publish go up?', [
|
|
176
|
+
{ label: 'Publish a new version of an existing model', value: '__existing__' },
|
|
177
|
+
{ label: 'Publish as a new model', value: null },
|
|
178
|
+
]);
|
|
179
|
+
return choice === '__existing__' ? pickExistingModel(hubUrl, token) : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Fetch the user's own models from the hub and let them pick which one this is a
|
|
184
|
+
* new version of. Returns the chosen model id, or null when they have none yet
|
|
185
|
+
* (the caller then mints a new model).
|
|
186
|
+
*/
|
|
187
|
+
async function pickExistingModel(hubUrl, token) {
|
|
188
|
+
const { status, body } = await new HubClient(hubUrl, token).getJson('/api/cli/models');
|
|
189
|
+
if (status === 401) {
|
|
190
|
+
throw new Error('Your session has expired. Run `fluidcad login` again.');
|
|
191
|
+
}
|
|
192
|
+
if (status !== 200) {
|
|
193
|
+
throw new Error(body.error || `Could not list your models (HTTP ${status})`);
|
|
194
|
+
}
|
|
195
|
+
const models = Array.isArray(body.models) ? body.models : [];
|
|
196
|
+
if (models.length === 0) {
|
|
197
|
+
console.log('\nYou have no models on the hub yet — publishing this as a new model.\n');
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return select(
|
|
201
|
+
'Which model is this a new version of?',
|
|
202
|
+
models.map((m) => ({
|
|
203
|
+
label: `${m.name} · ${m.latestVersion ? 'v' + m.latestVersion : 'no versions yet'}`,
|
|
204
|
+
value: m.id,
|
|
205
|
+
})),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
116
209
|
export function registerPublishCommand(program) {
|
|
117
210
|
program
|
|
118
211
|
.command('publish')
|
|
@@ -121,6 +214,8 @@ export function registerPublishCommand(program) {
|
|
|
121
214
|
.option('-e, --entry <file>', 'Entry .fluid.js file (auto-detected if only one exists)')
|
|
122
215
|
.option('-n, --name <name>', 'Model name (defaults to the package name)')
|
|
123
216
|
.option('-d, --description <text>', 'Optional human description')
|
|
217
|
+
.option('--new-model', 'Publish as a new model, ignoring any saved model id')
|
|
218
|
+
.option('--new-version', 'Publish a new version of the saved (or chosen) model')
|
|
124
219
|
.option('--visibility <visibility>', 'public | unlisted | private (default: unlisted)')
|
|
125
220
|
.option('--hub <url>', 'Hub base URL (default: the hub you logged into)')
|
|
126
221
|
.action((opts) => {
|
package/bin/lib/api-client.js
CHANGED
|
@@ -20,6 +20,14 @@ export class HubClient {
|
|
|
20
20
|
return { status: res.status, body };
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
async getJson(path) {
|
|
24
|
+
const res = await fetch(this.base + path, {
|
|
25
|
+
method: 'GET',
|
|
26
|
+
headers: this.#authHeaders(),
|
|
27
|
+
});
|
|
28
|
+
return this.#result(res);
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
async postJson(path, body) {
|
|
24
32
|
const res = await fetch(this.base + path, {
|
|
25
33
|
method: 'POST',
|
package/bin/lib/model-config.js
CHANGED
|
@@ -28,11 +28,34 @@ export function readModelId(workspace) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
* `
|
|
31
|
+
* The persisted identity for this workspace: the hub model `id` and its
|
|
32
|
+
* last-known `name`. Both come back null when absent — first publish, or a
|
|
33
|
+
* `fluidcad.json` the user deleted. The name is shown in the publish prompt so
|
|
34
|
+
* we can name the model offline without a round-trip.
|
|
33
35
|
*/
|
|
34
|
-
export function
|
|
36
|
+
export function readModelIdentity(workspace) {
|
|
37
|
+
const cfg = readModelConfig(workspace);
|
|
38
|
+
return {
|
|
39
|
+
modelId: typeof cfg.modelId === 'string' && cfg.modelId ? cfg.modelId : null,
|
|
40
|
+
name: typeof cfg.name === 'string' && cfg.name ? cfg.name : null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Shallow-merge `patch` into `fluidcad.json`, preserving any other fields, and
|
|
46
|
+
* write it back (creating the file if needed). `undefined`/`null` values in the
|
|
47
|
+
* patch are skipped, so callers can pass a partial identity without clobbering
|
|
48
|
+
* what's already there.
|
|
49
|
+
*/
|
|
50
|
+
export function writeModelConfig(workspace, patch) {
|
|
35
51
|
const cfg = readModelConfig(workspace);
|
|
36
|
-
|
|
52
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
53
|
+
if (value !== undefined && value !== null) cfg[key] = value;
|
|
54
|
+
}
|
|
37
55
|
writeFileSync(modelConfigPath(workspace), JSON.stringify(cfg, null, 2) + '\n');
|
|
38
56
|
}
|
|
57
|
+
|
|
58
|
+
/** Persist just the hub-minted model id (thin wrapper over `writeModelConfig`). */
|
|
59
|
+
export function writeModelId(workspace, modelId) {
|
|
60
|
+
writeModelConfig(workspace, { modelId });
|
|
61
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { emitKeypressEvents } from 'readline';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal interactive prompts for the CLI, built on Node's built-in `readline`
|
|
5
|
+
* keypress events — no dependency (the CLI only ships `commander`). Used by
|
|
6
|
+
* `publish` to choose between a new model and a new version.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Whether we can run an interactive prompt — both stdin and stdout must be a
|
|
11
|
+
* TTY. CI and piped input are not, so callers fall back to flags/defaults there.
|
|
12
|
+
*/
|
|
13
|
+
export function isInteractive() {
|
|
14
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const HIDE_CURSOR = '\x1b[?25l';
|
|
18
|
+
const SHOW_CURSOR = '\x1b[?25h';
|
|
19
|
+
const CLEAR_LINE = '\x1b[2K';
|
|
20
|
+
const CYAN = '\x1b[36m';
|
|
21
|
+
const DIM = '\x1b[2m';
|
|
22
|
+
const RESET = '\x1b[0m';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Arrow-key (or j/k) single-select. Renders a menu with one highlighted row,
|
|
26
|
+
* moves the highlight on ↑/↓/k/j, confirms on Enter, and aborts on Ctrl-C.
|
|
27
|
+
* Resolves to the chosen entry's `value`. Assumes `isInteractive()` — it puts
|
|
28
|
+
* stdin in raw mode, so don't call it without a TTY.
|
|
29
|
+
*
|
|
30
|
+
* `choices`: `[{ label, value }]`.
|
|
31
|
+
*/
|
|
32
|
+
export function select(message, choices) {
|
|
33
|
+
return new Promise((resolveChoice) => {
|
|
34
|
+
const input = process.stdin;
|
|
35
|
+
const output = process.stdout;
|
|
36
|
+
let active = 0;
|
|
37
|
+
|
|
38
|
+
// Truncate labels so a long one never wraps — a wrapped row would occupy two
|
|
39
|
+
// terminal lines and throw off the cursor-up redraw math below.
|
|
40
|
+
const width = Math.max(8, (output.columns || 80) - 2);
|
|
41
|
+
const fit = (s) => (s.length > width ? s.slice(0, width - 1) + '…' : s);
|
|
42
|
+
|
|
43
|
+
output.write(`${message}\n`);
|
|
44
|
+
output.write(`${DIM} ↑/↓ or j/k to move · Enter to confirm${RESET}\n`);
|
|
45
|
+
output.write(HIDE_CURSOR);
|
|
46
|
+
|
|
47
|
+
const draw = (initial) => {
|
|
48
|
+
if (!initial) output.write(`\x1b[${choices.length}A`); // back up to the first row
|
|
49
|
+
choices.forEach((c, i) => {
|
|
50
|
+
const on = i === active;
|
|
51
|
+
const row = `${on ? '❯' : ' '} ${fit(c.label)}`;
|
|
52
|
+
output.write(`${CLEAR_LINE}${on ? `${CYAN}${row}${RESET}` : row}\n`);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
draw(true);
|
|
56
|
+
|
|
57
|
+
emitKeypressEvents(input);
|
|
58
|
+
const wasRaw = Boolean(input.isRaw);
|
|
59
|
+
input.setRawMode(true);
|
|
60
|
+
input.resume();
|
|
61
|
+
|
|
62
|
+
const restore = () => {
|
|
63
|
+
input.removeListener('keypress', onKey);
|
|
64
|
+
input.setRawMode(wasRaw);
|
|
65
|
+
input.pause();
|
|
66
|
+
output.write(SHOW_CURSOR);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const onKey = (_str, key) => {
|
|
70
|
+
if (!key) return;
|
|
71
|
+
if (key.ctrl && key.name === 'c') {
|
|
72
|
+
restore();
|
|
73
|
+
output.write('\n');
|
|
74
|
+
process.exit(130); // 128 + SIGINT, the shell convention for Ctrl-C
|
|
75
|
+
}
|
|
76
|
+
switch (key.name) {
|
|
77
|
+
case 'up':
|
|
78
|
+
case 'k':
|
|
79
|
+
active = (active - 1 + choices.length) % choices.length;
|
|
80
|
+
draw(false);
|
|
81
|
+
break;
|
|
82
|
+
case 'down':
|
|
83
|
+
case 'j':
|
|
84
|
+
active = (active + 1) % choices.length;
|
|
85
|
+
draw(false);
|
|
86
|
+
break;
|
|
87
|
+
case 'return':
|
|
88
|
+
case 'enter':
|
|
89
|
+
restore();
|
|
90
|
+
resolveChoice(choices[active].value);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
input.on('keypress', onKey);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ShapeType } from "./shape-type.js";
|
|
2
|
-
import type { TopoDS_Edge } from "
|
|
2
|
+
import type { TopoDS_Edge } from "ocjs-fluidcad";
|
|
3
3
|
import { Shape } from "./shape.js";
|
|
4
4
|
import { Vertex } from "./vertex.js";
|
|
5
5
|
export declare class Edge extends Shape<TopoDS_Edge> {
|
|
@@ -93,6 +93,12 @@ export declare abstract class SceneObject implements Comparable<SceneObject>, Se
|
|
|
93
93
|
clone(): SceneObject[];
|
|
94
94
|
setTransform(matrix: Matrix4 | LazyMatrix): void;
|
|
95
95
|
getTransform(): Matrix4 | null;
|
|
96
|
+
/**
|
|
97
|
+
* The raw clone-transform reference set by cloneWithTransform. All clones
|
|
98
|
+
* belonging to one repeat/mirror instance share the same LazyMatrix, so
|
|
99
|
+
* reference identity distinguishes instances of the same container.
|
|
100
|
+
*/
|
|
101
|
+
getTransformRef(): LazyMatrix | null;
|
|
96
102
|
setCloneSource(source: SceneObject): void;
|
|
97
103
|
getCloneSource(): SceneObject | null;
|
|
98
104
|
getTransformMatrix(): Matrix4 | null;
|
|
@@ -206,6 +206,14 @@ export class SceneObject {
|
|
|
206
206
|
getTransform() {
|
|
207
207
|
return this._transform ? this._transform.resolve() : null;
|
|
208
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* The raw clone-transform reference set by cloneWithTransform. All clones
|
|
211
|
+
* belonging to one repeat/mirror instance share the same LazyMatrix, so
|
|
212
|
+
* reference identity distinguishes instances of the same container.
|
|
213
|
+
*/
|
|
214
|
+
getTransformRef() {
|
|
215
|
+
return this._transform;
|
|
216
|
+
}
|
|
209
217
|
setCloneSource(source) {
|
|
210
218
|
this._cloneSource = source;
|
|
211
219
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TopoDS_Edge, TopoDS_Face, TopoDS_Solid, TopTools_IndexedDataMapOfShapeListOfShape } from "
|
|
1
|
+
import type { TopoDS_Edge, TopoDS_Face, TopoDS_Solid, TopTools_IndexedDataMapOfShapeListOfShape } from "ocjs-fluidcad";
|
|
2
2
|
import { ShapeType } from "./shape-type.js";
|
|
3
3
|
import { Shape } from "./shape.js";
|
|
4
4
|
import { Face } from "./face.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { SceneObject } from "./scene-object.js";
|
|
1
|
+
import { BuildSceneObjectContext, SceneObject } from "./scene-object.js";
|
|
2
|
+
import { Shape } from "./shape.js";
|
|
2
3
|
import { Matrix4 } from "../math/matrix4.js";
|
|
3
4
|
import type { AxisLike } from "../math/axis.js";
|
|
4
5
|
import type { PlaneLike } from "../math/plane.js";
|
|
@@ -6,6 +7,16 @@ import type { PointLike } from "../math/point.js";
|
|
|
6
7
|
import { type NumberParam } from "../core/param.js";
|
|
7
8
|
export declare abstract class TransformablePrimitive extends SceneObject {
|
|
8
9
|
transform(matrix: Matrix4): this;
|
|
10
|
+
/**
|
|
11
|
+
* Adds a primitive's built shape, baking in the clone transform when this
|
|
12
|
+
* primitive is a repeat/mirror copy. The renderer post-applies the user's
|
|
13
|
+
* own transform (translate/rotate/mirror) after build, so the clone
|
|
14
|
+
* transform is conjugated to land outside it:
|
|
15
|
+
* own · (own⁻¹ · clone · own) = clone · own.
|
|
16
|
+
*/
|
|
17
|
+
protected addPrimitiveShape(shape: Shape, context?: BuildSceneObjectContext): void;
|
|
18
|
+
/** Carries the user's own transform onto a repeat/mirror copy. */
|
|
19
|
+
protected syncPrimitiveWith(source: TransformablePrimitive): this;
|
|
9
20
|
translate(x: NumberParam): this;
|
|
10
21
|
translate(x: NumberParam, y: NumberParam): this;
|
|
11
22
|
translate(x: NumberParam, y: NumberParam, z: NumberParam): this;
|
|
@@ -3,12 +3,39 @@ import { Matrix4 } from "../math/matrix4.js";
|
|
|
3
3
|
import { Point } from "../math/point.js";
|
|
4
4
|
import { Vector3d } from "../math/vector3d.js";
|
|
5
5
|
import { rad } from "../helpers/math-helpers.js";
|
|
6
|
+
import { ShapeOps } from "../oc/shape-ops.js";
|
|
6
7
|
import { isNumberParam, resolveParam } from "../core/param.js";
|
|
7
8
|
export class TransformablePrimitive extends SceneObject {
|
|
8
9
|
transform(matrix) {
|
|
9
10
|
this.composeAppliedTransform(matrix);
|
|
10
11
|
return this;
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Adds a primitive's built shape, baking in the clone transform when this
|
|
15
|
+
* primitive is a repeat/mirror copy. The renderer post-applies the user's
|
|
16
|
+
* own transform (translate/rotate/mirror) after build, so the clone
|
|
17
|
+
* transform is conjugated to land outside it:
|
|
18
|
+
* own · (own⁻¹ · clone · own) = clone · own.
|
|
19
|
+
*/
|
|
20
|
+
addPrimitiveShape(shape, context) {
|
|
21
|
+
const cloneTransform = context?.getTransform() ?? null;
|
|
22
|
+
if (cloneTransform) {
|
|
23
|
+
const own = this.getAppliedTransform();
|
|
24
|
+
const matrix = own
|
|
25
|
+
? own.inverse().multiply(cloneTransform).multiply(own)
|
|
26
|
+
: cloneTransform;
|
|
27
|
+
shape = ShapeOps.transform(shape, matrix);
|
|
28
|
+
}
|
|
29
|
+
this.addShape(shape);
|
|
30
|
+
}
|
|
31
|
+
/** Carries the user's own transform onto a repeat/mirror copy. */
|
|
32
|
+
syncPrimitiveWith(source) {
|
|
33
|
+
const applied = source.getAppliedTransform();
|
|
34
|
+
if (applied) {
|
|
35
|
+
this.transform(applied);
|
|
36
|
+
}
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
12
39
|
translate(a, b, c) {
|
|
13
40
|
let x, y, z;
|
|
14
41
|
if (isNumberParam(a)) {
|
|
@@ -21,5 +21,6 @@ export { default as offset } from './offset.js';
|
|
|
21
21
|
export { default as project } from './project.js';
|
|
22
22
|
export { default as intersect } from './intersect.js';
|
|
23
23
|
export { default as bezier } from './bezier.js';
|
|
24
|
+
export { default as text } from './text.js';
|
|
24
25
|
export { default as center } from './center.js';
|
|
25
26
|
export { default as back } from './back.js';
|
|
@@ -21,5 +21,6 @@ export { default as offset } from './offset.js';
|
|
|
21
21
|
export { default as project } from './project.js';
|
|
22
22
|
export { default as intersect } from './intersect.js';
|
|
23
23
|
export { default as bezier } from './bezier.js';
|
|
24
|
+
export { default as text } from './text.js';
|
|
24
25
|
export { default as center } from './center.js';
|
|
25
26
|
export { default as back } from './back.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PlaneLike } from "../../math/plane.js";
|
|
2
|
+
import { IText, ISceneObject } from "../interfaces.js";
|
|
3
|
+
interface TextFunction {
|
|
4
|
+
/**
|
|
5
|
+
* Renders a text string as extrudable outline geometry inside the current
|
|
6
|
+
* sketch, at the sketch cursor.
|
|
7
|
+
* @param text - The string to render.
|
|
8
|
+
*/
|
|
9
|
+
(text: string): IText;
|
|
10
|
+
/**
|
|
11
|
+
* Renders a text string on a specific plane (standalone, outside a sketch).
|
|
12
|
+
* @param plane - The plane (e.g. "xy") or face to render the text on.
|
|
13
|
+
* @param text - The string to render.
|
|
14
|
+
*/
|
|
15
|
+
(plane: PlaneLike | ISceneObject, text: string): IText;
|
|
16
|
+
/**
|
|
17
|
+
* Renders a text string following a planar curve. Each glyph is placed
|
|
18
|
+
* upright along the path's arc length; the text plane is the path's plane.
|
|
19
|
+
* Works inside a sketch (following a curve of that sketch) or standalone.
|
|
20
|
+
* The path is left in place — mark it `.guide()` to keep it out of
|
|
21
|
+
* extruded profiles.
|
|
22
|
+
* @param text - The string to render.
|
|
23
|
+
* @param path - The curve to follow: a sketch curve (line/arc/circle), a
|
|
24
|
+
* whole sketch, a planar primitive, or a selected edge/edge loop
|
|
25
|
+
* (e.g. `select(edge().circle())`).
|
|
26
|
+
*/
|
|
27
|
+
(text: string, path: ISceneObject): IText;
|
|
28
|
+
}
|
|
29
|
+
declare const _default: TextFunction;
|
|
30
|
+
export default _default;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Text } from "../../features/2d/text.js";
|
|
2
|
+
import { registerBuilder } from "../../index.js";
|
|
3
|
+
import { isPlaneLike } from "../../math/plane.js";
|
|
4
|
+
import { SceneObject } from "../../common/scene-object.js";
|
|
5
|
+
import { resolvePlane } from "../../helpers/resolve.js";
|
|
6
|
+
function build(context) {
|
|
7
|
+
return function text() {
|
|
8
|
+
const first = arguments[0];
|
|
9
|
+
const second = arguments[1];
|
|
10
|
+
// A trailing scene object is a path to follow: `text("Hi", path)`.
|
|
11
|
+
// Valid both standalone and inside a sketch (following a sketch curve).
|
|
12
|
+
if (arguments.length >= 2 && second instanceof SceneObject) {
|
|
13
|
+
if (typeof first !== "string") {
|
|
14
|
+
throw new Error("text: when following a path, the first argument must be the text string.");
|
|
15
|
+
}
|
|
16
|
+
const obj = new Text(first, null, second);
|
|
17
|
+
context.addSceneObject(obj);
|
|
18
|
+
return obj;
|
|
19
|
+
}
|
|
20
|
+
// A leading plane/face is only valid standalone and only when a string
|
|
21
|
+
// follows it; `text("xy")` (one arg) renders the literal string "xy".
|
|
22
|
+
const standalone = arguments.length >= 2 && (isPlaneLike(first) || first instanceof SceneObject);
|
|
23
|
+
if (standalone) {
|
|
24
|
+
if (context.getActiveSketch() !== null) {
|
|
25
|
+
throw new Error("text(plane, ...) cannot be used inside a sketch. Use text(...) instead.");
|
|
26
|
+
}
|
|
27
|
+
const planeObj = resolvePlane(first, context);
|
|
28
|
+
const obj = new Text(String(arguments[1] ?? ""), planeObj);
|
|
29
|
+
context.addSceneObject(obj);
|
|
30
|
+
return obj;
|
|
31
|
+
}
|
|
32
|
+
const obj = new Text(String(first ?? ""));
|
|
33
|
+
context.addSceneObject(obj);
|
|
34
|
+
return obj;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export default registerBuilder(build);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AxisLike } from "../math/axis.js";
|
|
2
|
+
import { IHelix, ISceneObject } from "./interfaces.js";
|
|
3
|
+
interface HelixFunction {
|
|
4
|
+
/**
|
|
5
|
+
* Creates a helix wire along the given axis. Use chained methods (.pitch(),
|
|
6
|
+
* .turns(), .height(), .radius(), .endRadius()) to configure geometry.
|
|
7
|
+
* @param axis - The axis to build the helix around.
|
|
8
|
+
*/
|
|
9
|
+
(axis: AxisLike): IHelix;
|
|
10
|
+
/**
|
|
11
|
+
* Creates a helix wire derived from a scene object's geometry.
|
|
12
|
+
* - A cylindrical or conical face: axis + radii + height come from the face.
|
|
13
|
+
* - A line edge: axis = the line, height = line length.
|
|
14
|
+
* - A circular edge: axis = circle normal at center, radius = circle radius.
|
|
15
|
+
* @param source - The scene object whose face/edge defines the helix.
|
|
16
|
+
*/
|
|
17
|
+
(source: ISceneObject): IHelix;
|
|
18
|
+
}
|
|
19
|
+
declare const _default: HelixFunction;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { registerBuilder } from "../index.js";
|
|
2
|
+
import { Helix } from "../features/helix.js";
|
|
3
|
+
import { isAxisLike } from "../math/axis.js";
|
|
4
|
+
import { AxisObject } from "../features/axis.js";
|
|
5
|
+
import { AxisObjectBase } from "../features/axis-renderable-base.js";
|
|
6
|
+
import { SceneObject } from "../common/scene-object.js";
|
|
7
|
+
import { normalizeAxis } from "../helpers/normalize.js";
|
|
8
|
+
function build(context) {
|
|
9
|
+
return function helix() {
|
|
10
|
+
if (arguments.length === 0) {
|
|
11
|
+
throw new Error("helix() requires an axis or scene object argument.");
|
|
12
|
+
}
|
|
13
|
+
const arg = arguments[0];
|
|
14
|
+
let source;
|
|
15
|
+
if (arg instanceof AxisObjectBase) {
|
|
16
|
+
source = arg;
|
|
17
|
+
context.addSceneObject(source);
|
|
18
|
+
}
|
|
19
|
+
else if (isAxisLike(arg)) {
|
|
20
|
+
const axis = normalizeAxis(arg);
|
|
21
|
+
source = new AxisObject(axis);
|
|
22
|
+
context.addSceneObject(source);
|
|
23
|
+
}
|
|
24
|
+
else if (arg instanceof SceneObject) {
|
|
25
|
+
source = arg;
|
|
26
|
+
context.addSceneObject(source);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
throw new Error("helix(): first argument must be an AxisLike or SceneObject.");
|
|
30
|
+
}
|
|
31
|
+
const result = new Helix(source);
|
|
32
|
+
context.addSceneObject(result);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export default registerBuilder(build);
|
package/lib/dist/core/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { ISceneObject, ITransformable, IBooleanOperation, IPlane, IAxis, ISelect, IGeometry, IExtrudableGeometry, IRect, ISlot, IPolygon, ITwoObjectsTangentLine, ITangentArcTwoObjects, IExtrude, ICut, ICommon, ISweep, ILoft, IRevolve, IDraft, IRib } from "./interfaces.js";
|
|
1
|
+
export type { ISceneObject, ITransformable, IBooleanOperation, IPlane, IAxis, ISelect, IGeometry, IExtrudableGeometry, IText, IRect, ISlot, IPolygon, ITwoObjectsTangentLine, ITangentArcTwoObjects, IExtrude, ICut, ICommon, ISweep, ILoft, IRevolve, IDraft, IRib, IHelix, IWrap } from "./interfaces.js";
|
|
2
2
|
export { default as axis } from "./axis.js";
|
|
3
3
|
export { default as local } from "./local.js";
|
|
4
4
|
export { default as plane } from "./plane.js";
|
|
@@ -24,6 +24,8 @@ export { default as load } from "./load.js";
|
|
|
24
24
|
export { default as loft } from "./loft.js";
|
|
25
25
|
export { default as sweep } from "./sweep.js";
|
|
26
26
|
export { default as rib } from "./rib.js";
|
|
27
|
+
export { default as wrap } from "./wrap.js";
|
|
28
|
+
export { default as helix } from "./helix.js";
|
|
27
29
|
export { default as color } from "./color.js";
|
|
28
30
|
export { default as draft } from "./draft.js";
|
|
29
31
|
export { default as remove } from "./remove.js";
|
package/lib/dist/core/index.js
CHANGED
|
@@ -23,6 +23,8 @@ export { default as load } from "./load.js";
|
|
|
23
23
|
export { default as loft } from "./loft.js";
|
|
24
24
|
export { default as sweep } from "./sweep.js";
|
|
25
25
|
export { default as rib } from "./rib.js";
|
|
26
|
+
export { default as wrap } from "./wrap.js";
|
|
27
|
+
export { default as helix } from "./helix.js";
|
|
26
28
|
export { default as color } from "./color.js";
|
|
27
29
|
export { default as draft } from "./draft.js";
|
|
28
30
|
export { default as remove } from "./remove.js";
|