diff --git a/app.js b/app.js index 5de08939..57ea3add 100644 --- a/app.js +++ b/app.js @@ -281,7 +281,7 @@ function initModule(mod, file, dir) { logger.log({ module: file, dir }); require_fresh(file)({ // express functions added here show up at "/api/" url root - api: api, + api, adm: { setver(ver) { oversion = ver }, crossOrigin(bool) { crossOrigin = bool } diff --git a/docs/void/plan-face-provenance.md b/docs/void/plan-face-provenance.md new file mode 100644 index 00000000..b8ed08c4 --- /dev/null +++ b/docs/void/plan-face-provenance.md @@ -0,0 +1,172 @@ +# Void Face Provenance Plan (Boundary-First, Split Faces) + +## Goal + +Track which portions of resulting solids come from which sketch extrude regions, including after union/subtract, while keeping storage compact and spline-ready. + +## Decisions + +1. Primary provenance is `boundary/region/surface-patch`, not raw triangle ownership. +2. Triangle ownership is derived runtime index only (`tri -> surface_patch_id`) and can be rebuilt. +3. Multi-source output faces must be split into multiple bounded surface patches so each patch has one canonical source region. +4. Line/arc support ships first; segment model must support future spline kinds without schema redesign. + +## Scope + +In scope: + +1. Extrude + boolean provenance tracking through rebuild pipeline. +2. Face splitting by source-region boundaries. +3. GeometryStore schema extension for stable patch-level IDs and source refs. +4. Runtime mapping from picks (`face/edge`) to canonical patch and source region. + +Out of scope (initial pass): + +1. Native spline feature authoring. +2. Long-lived persisted triangle provenance tables. +3. Non-planar sketch-on-surface expansion beyond current behavior. + +## Data Model Changes + +Add/extend document `geometry_store` entities: + +1. `segments[]` +2. `kind: line | arc | spline` +3. `geom`: kind-specific payload +4. `sampled_polyline` (optional cache for hit testing/partitioning) + +5. `boundaries[]` +6. Ordered `segment_ids` +7. `closed`, orientation, optional parent/child nesting + +8. `regions[]` +9. `outer_boundary_id` +10. `hole_boundary_ids[]` +11. `source`: canonical source ref (`profile::`) + +12. New `surface_patches[]` +13. `id` +14. `surface_id` (geometric carrier face) +15. `boundary_ids[]` (outer + holes) +16. `source_region_id` (single canonical owner) +17. `source_feature_id` (extrude feature) +18. `solid_id` +19. `status` (`direct`, `boolean-derived`, `rebound`) + +20. New runtime-only `topology.patch_tri_index` +21. Maps mesh triangles to `surface_patch_id` for selection/render acceleration. + +## Kernel Boundary Changes (Manifold) + +Current kernel adapter only round-trips positions/indices; relation metadata is dropped. + +Planned update: + +1. Preserve Manifold mesh relation fields where available (`runOriginalID`, `faceID`, related run metadata). +2. Carry relation metadata through `extrudePolygons()` and `booleanMeshes()`. +3. Emit relation-aware intermediate records to rebuild stage (not directly persisted). + +This enables deterministic attribution from boolean output back to input generated solids/regions before patch splitting. + +## Provenance Build Pipeline + +### Stage A: Sketch Region Capture + +1. Keep current closed-loop profile extraction for line/arc. +2. Emit canonical `region_id = profile::`. +3. Record region boundaries using generic segment schema (`line|arc` now, `spline` later). + +### Stage B: Extrude Seed Patches + +1. Extrude each selected sketch region. +2. Seed cap/side patch candidates with direct source region refs. +3. Preserve manifold relation fields in intermediate mesh record. + +### Stage C: Boolean Attribution + +1. Perform add/subtract/intersect with relation-carrying meshes. +2. Build attribution map from output primitives/runs to source seed patches. +3. Mark ambiguous/mixed carrier surfaces for partitioning. + +### Stage D: Face Partitioning (Split Multi-Source Faces) + +1. For each mixed carrier surface, project contributing source boundaries to surface-local space. +2. Build planar arrangement, split into disjoint bounded cells. +3. Assign each cell a single `source_region_id` by relation majority + geometric tie-break. +4. Emit one `surface_patch` per bounded cell. + +### Stage E: Runtime Topology Index + +1. Build `tri -> surface_patch_id` map from patch partition output. +2. Use map for selection hit resolution and hover highlighting. +3. Rebuild index each solids rebuild; do not persist large triangle maps. + +## Selection/Interaction Integration + +1. Face pick resolves to `surface_patch_id` first, then `source_region_id`. +2. Edge/boundary pick resolves to `boundary_id`/`segment_id` that belongs to a patch. +3. Extrude-profile hover/highlight uses `source_region_id -> surface_patch[]` mapping. +4. Remove fallback heuristics that infer provenance only from coarse `source.profile_keys`. + +## Storage and Performance + +1. Persist compact canonical graph (`segments/boundaries/regions/surface_patches`). +2. Keep triangle-level maps runtime-only to avoid doc bloat and instability across remeshes. +3. Cache partition signatures per carrier surface to avoid full repartition when unchanged. + +## Migration Plan + +### Phase 1: Schema and Adapters + +1. Add `surface_patches` schema and runtime index container. +2. Introduce generic segment schema (`kind + geom`) with current line/arc emitters. + +### Phase 2: Kernel Metadata Plumbing + +1. Extend solid kernel adapter to preserve manifold relation metadata. +2. Pass relation metadata through worker/main rebuild paths. + +### Phase 3: Patch Builder + +1. Implement mixed-face detection. +2. Implement local-space boundary arrangement and patch emission. +3. Add deterministic patch IDs/signatures. + +### Phase 4: Resolver Cutover + +1. Switch face selection from coarse face groups to `surface_patch` entities. +2. Update properties/tree hover mapping to patch/source-region links. + +### Phase 5: Cleanup + +1. Remove coarse provenance fallbacks once parity is validated. +2. Keep compatibility reader for older docs without `surface_patches`. + +## Validation Plan + +Unit tests: + +1. Region extraction determinism (line/arc). +2. Mixed-face partitioning into disjoint bounded patches. +3. Single-owner assignment per patch. +4. Deterministic patch IDs under stable input. + +Integration tests: + +1. Two extrudes unioned: top face splits by source boundary and highlights per profile. +2. Subtract operation: surviving walls/caps retain correct source region refs. +3. Edit upstream sketch profile: downstream patch mapping updates without manual repair. +4. Rebuild in worker vs main thread yields identical patch/source mapping. + +Regression guardrails: + +1. No persisted triangle tables in doc snapshots. +2. No schema changes required to add spline segment kind later. +3. Selection never reports mixed-source face entities. + +## Acceptance Criteria + +1. Every selectable resulting face area maps to exactly one `source_region_id`. +2. Multi-source carrier faces are visibly and topologically split at source boundaries. +3. Extrude profile hover/select maps accurately to resulting solid patches after booleans. +4. GeometryStore stays compact and stable; triangle mapping is derived at runtime. diff --git a/mods/proxy/.debug b/mods/proxy/.debug new file mode 100644 index 00000000..e69de29b diff --git a/mods/proxy/.electron b/mods/proxy/.electron new file mode 100644 index 00000000..e69de29b diff --git a/mods/proxy/init.js b/mods/proxy/init.js new file mode 100644 index 00000000..3a24fe01 --- /dev/null +++ b/mods/proxy/init.js @@ -0,0 +1,54 @@ +module.exports = async (server) => { + + const { api, env, path, util } = server; + + if (!(env.debug || env.electron)) { + util.log('not a valid context for proxy'); + return; + } + + path.full({ + "/printer/print/start": proxy_post, + "/server/files/upload": proxy_post, + "/api/files/local": proxy_post, + }); + + server.inject("kiri", "main.js"); +}; + +function proxy_post(req, res, next) { + if (req.method === 'POST') { + let { url, headers } = req; + let chunks = []; + let host = headers['x-host']; + let apik = headers['x-api-key'] ?? ''; + let cont = headers['content-type'] ?? 'application/binary'; + req + .on('data', data => chunks.push(data) ) + .on('end', () => { + req.app.post = Buffer.concat(chunks); + if (host) { + fetch(host + url, { + method: 'POST', + headers: { + 'Content-Type': cont, + 'X-Api-Key': apik + }, + body: req.app.post + }).then(result => { + console.log({ result }); + if (result.ok) { + res.writeHead(200, 'OK'); + } else { + res.writeHead(500, 'Failed to proxy'); + } + res.end(); + }); + } else { + console.log('drop proxy due to lack of host'); + } + }); + } else { + next(); + } +} diff --git a/mods/proxy/main.js b/mods/proxy/main.js new file mode 100644 index 00000000..77858c9b --- /dev/null +++ b/mods/proxy/main.js @@ -0,0 +1,3 @@ +self.kiri.load(api => { + api.feature.proxy = true; +}, 'Proxy'); diff --git a/src/kiri/app/export.js b/src/kiri/app/export.js index 1479f36f..02b3c222 100644 --- a/src/kiri/app/export.js +++ b/src/kiri/app/export.js @@ -247,7 +247,8 @@ function exportGCodeDialog(gcode, sections, info, names) { ajax = new XMLHttpRequest(), host = octo_host.value.toLowerCase(), apik = octo_apik.value, - type = octo_type.value; + type = octo_type.value, + proxy = api.feature.proxy ? host : undefined; if (host.indexOf("http") !== 0) { api.show.alert("host missing protocol (http:// or https://)"); @@ -262,6 +263,11 @@ function exportGCodeDialog(gcode, sections, info, names) { localSet('octo-apik', apik.trim()); localSet('octo-type', type.trim()); + if (proxy) { + console.log('proxying request to', host); + host = ''; + } + filename = $('print-filename').value + "." + fileext; form.append("file", getBlob(), filename); ajax.onreadystatechange = function() { @@ -297,6 +303,9 @@ function exportGCodeDialog(gcode, sections, info, names) { } else { ajax.open("POST", host + "/api/files/local"); } + if (proxy) { + ajax.setRequestHeader("X-Host", proxy); + } if (apik) { ajax.setRequestHeader("X-Api-Key", apik); }