Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(graphene): multiscale meshes #626

Draft
wants to merge 25 commits into
base: spelunker
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5dc1e8f
added custom error handler support for credentials provider so middle…
chrisj Jan 12, 2024
7811377
feat(segmentation_user_layer): add individual segment color picker tool
chrisj Feb 22, 2024
5647d6d
feat(tools): custom keybinds and layer independent tool keybinds
chrisj Feb 22, 2024
82f0a42
added custom bindings config
chrisj May 15, 2023
00cbf47
added ctrl+shift+x hide segments (same as seung lab neuroglancer)
chrisj Mar 13, 2024
10eec22
temp fix for our placeholder precomputed scales
chrisj Apr 22, 2024
e1926e3
feat(datasource/graphene) bumped bulk merge limit to 20
chrisj May 21, 2024
90d5ff1
add registerLayerEvent/dispatchLayerEvent
chrisj Sep 30, 2024
d5f0cab
add keybinds (alt+up arrow, alt+down arrow) to iterate through the cu…
chrisj Oct 1, 2024
000ad7e
added annotation tagging feature
chrisj Oct 1, 2024
378157b
displaying annotation tags in annotation list
chrisj Oct 2, 2024
d72677d
rebinding brackets [] to select-prev/select-next
chrisj Sep 19, 2024
7d05fde
quick fix for tags breaking precomputed annotations
chrisj Oct 2, 2024
df04aa7
feat(datasource/graphene) added timestamp tool to graphene, timestamp…
chrisj Oct 22, 2024
d306785
added ability to upgrade state in UrlHashBinding.updateFromUrlHash fo…
chrisj Oct 22, 2024
887c3c1
fix(datasource/graphene) disabling hide segment zero when activating …
chrisj Nov 4, 2024
b4d7759
temp fix for layertools not rendering in tool palette
chrisj Jan 10, 2025
283c126
added appspot and state share config
chrisj Jan 6, 2023
f9ce7df
added custom error handler support for credentials provider so middle…
chrisj Jan 12, 2024
e74ad87
feat(graphene): multiscale meshes
akhileshh Jan 26, 2025
675320f
virutal nodes
akhileshh Jan 27, 2025
4c173ac
custom deployment for test branch
chrisj Nov 4, 2024
cff0430
fix(auth): do not pass invalid credentials
akhileshh Jan 29, 2025
f13c8b4
fix: rebase conflicts
akhileshh Feb 16, 2025
605f3cb
combine fragments per chunk
akhileshh Feb 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
name: Build

on:
push:
branches:
- spelunker
tags:
- v**
on: [push, pull_request]

jobs:
build-and-deploy:
Expand All @@ -30,6 +25,7 @@ jobs:
cache-dependency-path: |
package-lock.json
examples/**/package-lock.json
fetch-depth: 0
- run: npm install
- name: Typecheck with TypeScript
run: npm run typecheck
Expand Down Expand Up @@ -69,9 +65,9 @@ jobs:
- id: deploy
uses: google-github-actions/deploy-appengine@main
with:
version: ${{ env.GITHUB_SHA }}
version: ${{ env.BRANCH_NAME_URL }}
deliverables: appengine/frontend/app.yaml
promote: true
promote: false
- name: update deployment status
uses: bobheadxi/deployments@v1
if: always()
Expand Down
2 changes: 1 addition & 1 deletion appengine/frontend/app.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
runtime: python312

service: spelunker
service: neuroglancer

handlers:
# Handle the main page by serving the index page.
Expand Down
21 changes: 21 additions & 0 deletions config/custom-keybinds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"keym": {
"layer": "segmentation",
"tool": "grapheneMergeSegments",
"provider": "graphene"
},
"keyc": {
"layer": "segmentation",
"tool": "grapheneMulticutSegments",
"provider": "graphene"
},
"keyf": {
"layer": "segmentation",
"tool": "grapheneFindPath",
"provider": "graphene"
},
"keyx": false,
"control+shift+keyx": "clear-segments",
"bracketleft": "select-previous",
"bracketright": "select-next"
}
3 changes: 2 additions & 1 deletion src/annotation/annotation_layer_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ void main() {

export class AnnotationDisplayState extends RefCounted {
annotationProperties = new WatchableValue<
AnnotationPropertySpec[] | undefined
readonly Readonly<AnnotationPropertySpec>[] | undefined
>(undefined);
shader = makeTrackableFragmentMain(DEFAULT_FRAGMENT_MAIN);
shaderControls = new ShaderControlState(
Expand All @@ -159,6 +159,7 @@ export class AnnotationDisplayState extends RefCounted {
new WatchableAnnotationRelationshipStates(),
);
ignoreNullSegmentFilter = new TrackableBoolean(true);
swapVisibleSegmentsOnMove = new TrackableBoolean(true);
disablePicking = new WatchableValue(false);
displayUnfiltered = makeCachedLazyDerivedWatchableValue(
(map, ignoreNullSegmentFilter) => {
Expand Down
9 changes: 6 additions & 3 deletions src/annotation/frontend_source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
SliceViewChunkSource,
} from "#src/sliceview/frontend.js";
import { StatusMessage } from "#src/status.js";
import { WatchableValue } from "#src/trackable_value.js";
import type { Borrowed, Owned } from "#src/util/disposable.js";
import { ENDIANNESS, Endianness } from "#src/util/endian.js";
import * as matrix from "#src/util/matrix.js";
Expand Down Expand Up @@ -515,7 +516,9 @@ export class MultiscaleAnnotationSource
spatiallyIndexedSources = new Set<Borrowed<AnnotationGeometryChunkSource>>();
rank: number;
readonly relationships: readonly string[];
readonly properties: Readonly<AnnotationPropertySpec>[];
readonly properties: WatchableValue<
readonly Readonly<AnnotationPropertySpec>[]
>;
readonly annotationPropertySerializers: AnnotationPropertySerializer[];
constructor(
public chunkManager: Borrowed<ChunkManager>,
Expand All @@ -530,10 +533,10 @@ export class MultiscaleAnnotationSource
new AnnotationMetadataChunkSource(this.chunkManager, this),
);
this.rank = options.rank;
this.properties = options.properties;
this.properties = new WatchableValue(options.properties);
this.annotationPropertySerializers = makeAnnotationPropertySerializers(
this.rank,
this.properties,
this.properties.value,
);
const segmentFilteredSources: Owned<AnnotationSubsetGeometryChunkSource>[] =
(this.segmentFilteredSources = []);
Expand Down
93 changes: 81 additions & 12 deletions src/annotation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
CoordinateSpaceTransform,
WatchableCoordinateSpaceTransform,
} from "#src/coordinate_transform.js";
import { WatchableValue } from "#src/trackable_value.js";
import { arraysEqual } from "#src/util/array.js";
import {
packColor,
Expand Down Expand Up @@ -106,6 +107,13 @@ export interface AnnotationNumericPropertySpec
min?: number;
max?: number;
step?: number;
tag?: string;
}

export interface AnnotationTagPropertySpec
extends AnnotationNumericPropertySpec {
type: "int8";
tag: string;
}

export const propertyTypeDataType: Record<
Expand All @@ -127,6 +135,18 @@ export type AnnotationPropertySpec =
| AnnotationColorPropertySpec
| AnnotationNumericPropertySpec;

export function isAnnotationNumericPropertySpec(
spec: AnnotationPropertySpec,
): spec is AnnotationNumericPropertySpec {
return spec.type !== "rgb" && spec.type !== "rgba";
}

export function isAnnotationTagPropertySpec(
spec: AnnotationPropertySpec,
): spec is AnnotationTagPropertySpec {
return spec.type === "uint8" && spec.tag !== undefined;
}

export interface AnnotationPropertyTypeHandler {
serializedBytes(rank: number): number;
alignment(rank: number): number;
Expand Down Expand Up @@ -569,6 +589,7 @@ function parseAnnotationPropertySpec(obj: unknown): AnnotationPropertySpec {
);
let enumValues: number[] | undefined;
let enumLabels: string[] | undefined;
let tag: string | undefined;
switch (type) {
case "rgb":
case "rgba":
Expand All @@ -593,6 +614,7 @@ function parseAnnotationPropertySpec(obj: unknown): AnnotationPropertySpec {
),
);
}
tag = verifyOptionalObjectProperty(obj, "tag", verifyString);
}
}
return {
Expand All @@ -602,15 +624,23 @@ function parseAnnotationPropertySpec(obj: unknown): AnnotationPropertySpec {
default: defaultValue,
enumValues,
enumLabels,
tag,
} as AnnotationPropertySpec;
}

function annotationPropertySpecToJson(spec: AnnotationPropertySpec) {
const defaultValue = spec.default;
const isNumeric = isAnnotationNumericPropertySpec(spec);
const tag = isNumeric ? spec.tag : undefined;
const enum_values = isNumeric ? spec.enumValues : undefined;
const enum_labels = isNumeric ? spec.enumLabels : undefined;
return {
id: spec.identifier,
description: spec.description,
type: spec.type,
tag,
enum_values,
enum_labels,
default:
defaultValue === 0
? undefined
Expand Down Expand Up @@ -1000,7 +1030,7 @@ export const annotationTypeHandlers: Record<
export interface AnnotationSchema {
rank: number;
relationships: readonly string[];
properties: readonly AnnotationPropertySpec[];
properties: WatchableValue<readonly Readonly<AnnotationPropertySpec>[]>;
}

export function annotationToJson(
Expand All @@ -1020,8 +1050,8 @@ export function annotationToJson(
segments.map((x) => x.toString()),
);
}
if (schema.properties.length !== 0) {
const propertySpecs = schema.properties;
const propertySpecs = schema.properties.value;
if (propertySpecs.length !== 0) {
result.props = annotation.properties.map((prop, i) =>
annotationPropertyTypeHandlers[propertySpecs[i].type].serializeJson(prop),
);
Expand Down Expand Up @@ -1061,9 +1091,9 @@ function restoreAnnotation(
);
});
const properties = verifyObjectProperty(obj, "props", (propsObj) => {
const propSpecs = schema.properties;
const propSpecs = schema.properties.value;
if (propsObj === undefined) return propSpecs.map((x) => x.default);
return parseArray(expectArray(propsObj, schema.properties.length), (x, i) =>
return parseArray(expectArray(propsObj, propSpecs.length), (x, i) =>
annotationPropertyTypeHandlers[propSpecs[i].type].deserializeJson(x),
);
});
Expand Down Expand Up @@ -1111,13 +1141,15 @@ export class AnnotationSource
constructor(
rank: number,
public readonly relationships: readonly string[] = [],
public readonly properties: Readonly<AnnotationPropertySpec>[] = [],
public readonly properties: WatchableValue<
readonly Readonly<AnnotationPropertySpec>[]
> = new WatchableValue([]),
) {
super();
this.rank_ = rank;
this.annotationPropertySerializers = makeAnnotationPropertySerializers(
rank,
properties,
properties.value,
);
}

Expand Down Expand Up @@ -1261,16 +1293,56 @@ export class LocalAnnotationSource extends AnnotationSource {

constructor(
public watchableTransform: WatchableCoordinateSpaceTransform,
properties: AnnotationPropertySpec[],
public readonly properties: WatchableValue<
AnnotationPropertySpec[]
> = new WatchableValue([]),
relationships: string[],
) {
super(watchableTransform.value.sourceRank, relationships, properties);
this.curCoordinateTransform = watchableTransform.value;
this.registerDisposer(
watchableTransform.changed.add(() => this.ensureUpdated()),
);

this.registerDisposer(
properties.changed.add(() => {
this.updateAnnotationPropertySerializers();
this.changed.dispatch();
}),
);
}

updateAnnotationPropertySerializers() {
this.annotationPropertySerializers = makeAnnotationPropertySerializers(
this.rank_,
this.properties.value,
);
}

addProperty(property: AnnotationPropertySpec) {
this.properties.value.push(property);
for (const annotation of this) {
annotation.properties.push(property.default);
}
this.properties.changed.dispatch();
}

removeProperty(identifier: string) {
const propertyIndex = this.properties.value.findIndex(
(x) => x.identifier === identifier,
);
this.properties.value.splice(propertyIndex, 1);
for (const annotation of this) {
annotation.properties.splice(propertyIndex, 1);
}
this.properties.changed.dispatch();
}

getTagProperties = () => {
const { properties } = this;
return properties.value.filter(isAnnotationTagPropertySpec);
};

ensureUpdated() {
const transform = this.watchableTransform.value;
const { curCoordinateTransform } = this;
Expand Down Expand Up @@ -1325,10 +1397,7 @@ export class LocalAnnotationSource extends AnnotationSource {
}
if (this.rank_ !== sourceRank) {
this.rank_ = sourceRank;
this.annotationPropertySerializers = makeAnnotationPropertySerializers(
this.rank_,
this.properties,
);
this.updateAnnotationPropertySerializers();
}
this.changed.dispatch();
}
Expand Down
18 changes: 14 additions & 4 deletions src/annotation/renderlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,16 +477,18 @@ function AnnotationRenderLayer<
private renderHelpers: AnnotationRenderHelper[] = [];
private tempChunkPosition: Float32Array;

handleRankChanged() {
handleRankChanged(force = false) {
const { rank } = this.base.source;
if (rank === this.curRank) return;
if (!force && rank === this.curRank) return;
this.curRank = rank;
this.tempChunkPosition = new Float32Array(rank);
const { renderHelpers, gl } = this;
for (const oldHelper of renderHelpers) {
oldHelper.dispose();
}
const { properties } = this.base.source;
const {
properties: { value: properties },
} = this.base.source;
const { displayState } = this.base.state;
for (const annotationType of annotationTypes) {
const handler = getAnnotationTypeRenderHandler(annotationType);
Expand Down Expand Up @@ -525,6 +527,12 @@ function AnnotationRenderLayer<
});
this.role = base.state.role;
this.registerDisposer(base.redrawNeeded.add(this.redrawNeeded.dispatch));
this.registerDisposer(
base.source.properties.changed.add(() => {
// todo, does it make sense to run this whole function? Or should we pass the watchable value to renderHelperConstructor?
this.handleRankChanged(true);
}),
);
this.handleRankChanged();
this.registerDisposer(
this.base.state.displayState.shaderControls.histogramSpecifications.producerVisibility.add(
Expand Down Expand Up @@ -783,7 +791,9 @@ function AnnotationRenderLayer<
transformPickedValue(pickState: PickState) {
const { pickedAnnotationBuffer } = pickState;
if (pickedAnnotationBuffer === undefined) return undefined;
const { properties } = this.base.source;
const {
properties: { value: properties },
} = this.base.source;
if (properties.length === 0) return undefined;
const {
pickedAnnotationBufferBaseOffset,
Expand Down
9 changes: 7 additions & 2 deletions src/credentials_provider/http_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export async function fetchOkWithCredentials<Credentials>(
credentials: Credentials,
requestInit: RequestInit & { progressListener?: ProgressListener },
) => RequestInit & { progressListener?: ProgressListener },
errorHandler: (httpError: HttpError, credentials: Credentials) => "refresh",
errorHandler: (
httpError: HttpError,
credentials: Credentials,
) => "refresh" | Promise<"refresh">,
): Promise<Response> {
let credentials: CredentialsWithGeneration<Credentials> | undefined;
for (let credentialsAttempt = 0; ; ) {
Expand All @@ -56,7 +59,9 @@ export async function fetchOkWithCredentials<Credentials>(
);
} catch (error) {
if (error instanceof HttpError) {
if (errorHandler(error, credentials.credentials) === "refresh") {
if (
(await errorHandler(error, credentials.credentials)) === "refresh"
) {
if (++credentialsAttempt === maxCredentialsAttempts) throw error;
continue;
}
Expand Down
Loading
Loading